From 6bb92aa034b4b63097c91651c0877132b7a44856 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 07:09:21 +0900 Subject: [PATCH 001/236] fix: change api path --- src/APIs/notifications/notifications.controller.ts | 2 +- src/APIs/stickerCategories/stickerCategories.controller.ts | 6 +++--- src/APIs/stickers/stickers.controller.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index a5e1198..8fa4f66 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -19,7 +19,7 @@ export class NotificationsController { summary: '[SSE] kakaoId로 오는 알림을 구독한다.', description: '[swagger 불가능, postman 권장]', }) - @Sse(':kakaoId') + @Sse('sub/:kakaoId') sendClientAlarm(@Param('kakaoId') kakaoId: number, @Req() req: Request) { // this.notificationsService.addStream(this.users$, this.observer, userId); // req.on('close', () => diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index adfa212..30e4a95 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -33,7 +33,7 @@ export class StickerCategoriesController { @ApiOkResponse({ description: '생성 완료', type: StickerCategory }) @ApiCookieAuth() @UseGuards(AuthGuard('jwt')) - @Post(':name') + @Post('create/:name') async createCategory(@Req() req: Request, @Param('name') name: string) { const kakaoId = req.user.userId; return await this.stickerCategoriesService.createCategory({ @@ -48,7 +48,7 @@ export class StickerCategoriesController { }) @ApiCookieAuth() @UseGuards(AuthGuard('jwt')) - @Post() + @Post('map') async mapCategory( @Req() req: Request, @Body() mapCategoryDto: MapCategoryDto, @@ -72,7 +72,7 @@ export class StickerCategoriesController { summary: '카테고리 이름에 해당하는 스티커를 fetchAll', description: '카테고리를 이름으로 찾고, 이에 매핑된 스티커들을 가져온다', }) - @Get(':name') + @Get('fetch/:name') async fetchStickersByCategoryName(@Param('name') name: string) { return await this.stickerCategoriesService.fetchStickersByCategoryName({ name, diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index a4809e9..1ebf3c5 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -95,7 +95,7 @@ export class StickersController { description: '본인이 만든 스티커의 재사용 여부를 토글한다. 보관함 저장 혹은 삭제 용도로 사용할 것', }) - @Post(':id') + @Post('toggle/:id') @UseGuards(AuthGuard('jwt')) @ApiCookieAuth() @HttpCode(200) From 5d78e90c6071f259a230145da2de553e69ce167d Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 10:33:26 +0900 Subject: [PATCH 002/236] feat: sticker bg remove api --- package-lock.json | 461 ++++++++++++++++++++++- package.json | 1 + src/APIs/stickers/dto/remove-bg.dto.ts | 11 + src/APIs/stickers/stickers.controller.ts | 17 + src/APIs/stickers/stickers.service.ts | 5 + src/app.module.ts | 2 +- 6 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 src/APIs/stickers/dto/remove-bg.dto.ts diff --git a/package-lock.json b/package-lock.json index 3878875..99bfee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-s3": "^3.534.0", + "@imgly/background-removal-node": "^1.4.5", "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", @@ -1888,6 +1889,26 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@imgly/background-removal-node": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@imgly/background-removal-node/-/background-removal-node-1.4.5.tgz", + "integrity": "sha512-/s9K88qhKy1jPhrSkBxurUqCVqJ8KHWCc+5yWdppdC4fuSrGC8mK8WQtmULs2ASEr8naY1qpvZu0EL5jr2Hqtg==", + "dependencies": { + "@types/lodash": "~4.14.195", + "@types/ndarray": "~1.0.14", + "@types/node": "~20.3.1", + "lodash": "~4.17.21", + "ndarray": "~1.0.19", + "onnxruntime-node": "~1.17.0", + "sharp": "~0.32.4", + "zod": "~3.21.4" + } + }, + "node_modules/@imgly/background-removal-node/node_modules/@types/node": { + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3899,6 +3920,11 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3918,6 +3944,11 @@ "@types/express": "*" } }, + "node_modules/@types/ndarray": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz", + "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==" + }, "node_modules/@types/node": { "version": "20.11.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.29.tgz", @@ -4645,6 +4676,11 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4766,6 +4802,38 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.3.tgz", + "integrity": "sha512-amG72llr9pstfXOBOHve1WjiuKKAMnebcmMbPWDZ7BCevAoJLpugjuAPRsDINEyjT0a6tbaVx3DctkXIRbLuJw==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "node_modules/bare-os": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.1.tgz", + "integrity": "sha512-OwPyHgBBMkhC29Hl3O4/YfxW9n7mdTr2+SsO29XBWKKJsbgj3mnorDB80r5TiCQgQstgE5ga1qNYrpes6NvX2w==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", + "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4814,7 +4882,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -4825,7 +4892,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4953,7 +5019,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -5338,6 +5403,18 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5354,6 +5431,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -5596,6 +5682,20 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -5610,6 +5710,14 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5828,6 +5936,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", @@ -6208,6 +6324,14 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -6317,6 +6441,11 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -6688,6 +6817,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6850,6 +6984,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -7169,6 +7308,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -7204,6 +7348,11 @@ "node": ">= 0.10" } }, + "node_modules/iota-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", + "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7230,6 +7379,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -8572,6 +8726,17 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -8641,6 +8806,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8735,12 +8905,26 @@ "node": ">=12" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/ndarray": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", + "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", + "dependencies": { + "iota-array": "^1.0.0", + "is-buffer": "^1.0.2" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -8755,6 +8939,17 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-abi": { + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", + "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -8907,6 +9102,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz", + "integrity": "sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw==" + }, + "node_modules/onnxruntime-node": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz", + "integrity": "sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg==", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "1.17.0" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9290,6 +9503,75 @@ "node": ">=4" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9387,6 +9669,15 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9446,6 +9737,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9477,6 +9773,28 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -9964,6 +10282,33 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10070,6 +10415,62 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10164,6 +10565,18 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -10399,6 +10812,29 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -10818,6 +11254,17 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11440,6 +11887,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index e0d4ef3..27a7e25 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.534.0", + "@imgly/background-removal-node": "^1.4.5", "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", diff --git a/src/APIs/stickers/dto/remove-bg.dto.ts b/src/APIs/stickers/dto/remove-bg.dto.ts new file mode 100644 index 0000000..969c5a9 --- /dev/null +++ b/src/APIs/stickers/dto/remove-bg.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class RemoveBgDto { + @ApiProperty({ + description: '이미지가 저장된 url', + type: String, + }) + @IsString() + url: string; +} diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 1ebf3c5..197c2ce 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -1,10 +1,12 @@ import { + Body, Controller, Get, HttpCode, Param, Post, Req, + Res, UploadedFile, UseGuards, UseInterceptors, @@ -25,6 +27,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; +import { RemoveBgDto } from './dto/remove-bg.dto'; @ApiTags('스티커 API') @Controller('stickers') @@ -128,4 +131,18 @@ export class StickersController { async fetchPublicStickers(): Promise { return await this.stickersService.fetchPublicStickers(); } + + @ApiOperation({ + summary: '스티커 배경을 제거한다.', + description: '스티커 배경을 제거하고, png로 받는다.', + }) + @Post('remove') + @HttpCode(201) + async removeBg(@Body() body: RemoveBgDto, @Res() res) { + const blobData = await this.stickersService.removeBg({ url: body.url }); + const arrayBuffer = await blobData.arrayBuffer(); + const bufferData = Buffer.from(arrayBuffer); + res.set('Content-Type', 'image/jpeg'); + res.send(bufferData); + } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 6c7d6f9..5466bd0 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -11,6 +11,7 @@ import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; +import { removeBackground } from '@imgly/background-removal-node'; @Injectable() export class StickersService { @@ -105,4 +106,8 @@ export class StickersService { where: { isDefault: true }, }); } + + async removeBg({ url }) { + return await removeBackground(url); + } } diff --git a/src/app.module.ts b/src/app.module.ts index 9770195..a8e57d7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,7 +46,7 @@ import { ReportsModule } from './APIs/reports/reports.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: true, + synchronize: false, logging: true, }), ], From 9bf69c6b23779a71acc9c0ae008ca6b96a1a4789 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 10:52:37 +0900 Subject: [PATCH 003/236] fix: sticker & comment interaction --- src/APIs/comments/entities/comment.entity.ts | 2 +- src/app.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index ba36f74..5e8db43 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -47,7 +47,7 @@ export class Comment { @JoinColumn() parent: Comment; - @Column() + @Column({ nullable: true }) @RelationId((comment: Comment) => comment.parent) parentId: Comment; diff --git a/src/app.module.ts b/src/app.module.ts index a8e57d7..9770195 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,7 +46,7 @@ import { ReportsModule } from './APIs/reports/reports.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: false, + synchronize: true, logging: true, }), ], From 42c73efd3775be5f102aba6c868c2bc7688f562d Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 11:00:46 +0900 Subject: [PATCH 004/236] fix: comment validcheck logic --- src/APIs/comments/comments.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index fcff7bf..7ec4999 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -40,7 +40,7 @@ export class CommentsService { if (createCommentDto.parentId) await this.postsIdValidCheck({ parentId: createCommentDto.parentId, - postsId: createCommentDto.parentId, + postsId: createCommentDto.postsId, }); await this.dataSource.manager.update(Posts, createCommentDto.postsId, { comment_count: () => 'comment_count +1', From e15270db9e72bf62a5fcfd03acf7579e94d65d28 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 11:15:32 +0900 Subject: [PATCH 005/236] fix: add onDelete Actions --- src/APIs/auth/{dto => dtos}/kakao-user.dto.ts | 0 src/APIs/comments/entities/comment.entity.ts | 7 ++++++- .../{dto => dtos}/fetch-likes-response.dto.ts | 0 .../likes/{dto => dtos}/fetch-likes.dto.ts | 0 .../{dto => dtos}/toggle-like-response.dto.ts | 0 .../likes/{dto => dtos}/toggle-like.dto.ts | 0 src/APIs/likes/entities/like.entity.ts | 8 ++++++-- src/APIs/likes/likes.controller.ts | 6 +++--- src/APIs/likes/likes.service.ts | 6 +++--- .../{dto => dtos}/follow-user.dto.ts | 0 .../neighbors/{dto => dtos}/follow.dto.ts | 0 .../{dto => dtos}/from-user-response.dto.ts | 2 +- .../{dto => dtos}/to-user-response.dto.ts | 2 +- .../neighbors/entities/neighbor.entity.ts | 14 ++++++++++--- src/APIs/neighbors/neighbors.controller.ts | 8 ++++---- src/APIs/neighbors/neighbors.service.ts | 8 ++++---- .../PostCategories.controller.ts | 4 ++-- .../postCategories/PostCategories.service.ts | 2 +- .../create-post-category-response.dto.ts | 0 .../{dto => dtos}/create-post-category.dto.ts | 0 .../entities/postCategory.entity.ts | 3 ++- .../posts/{dto => dtos}/create-post.dto.ts | 0 .../posts/{dto => dtos}/create-post.input.ts | 0 .../{dto => dtos}/fetch-friends-posts.dto.ts | 0 .../posts/{dto => dtos}/fetch-posts.dto.ts | 0 .../{dto => dtos}/page-post-response.dto.ts | 0 .../posts/{dto => dtos}/post-response.dto.ts | 2 +- .../posts/{dto => dtos}/publish-post.dto.ts | 0 .../posts/{dto => dtos}/publish-post.input.ts | 0 src/APIs/posts/entities/posts.entity.ts | 20 +++++++++++++++---- src/APIs/posts/posts.controller.ts | 10 +++++----- src/APIs/posts/posts.service.ts | 8 ++++---- src/APIs/reports/entities/report.entity.ts | 3 ++- .../{dto => dtos}/create-stickerBlock.dto.ts | 0 .../stickerBlocks/stickerBlocks.controller.ts | 2 +- .../stickerBlocks/stickerBlocks.service.ts | 2 +- .../{dto => dtos}/map-category.dto.ts | 0 .../entities/stickerCategoryMapper.entity.ts | 2 ++ .../{dto => dtos}/create-sticker.dto.ts | 0 .../stickers/{dto => dtos}/remove-bg.dto.ts | 0 src/APIs/stickers/entities/sticker.entity.ts | 6 +++++- src/APIs/stickers/stickers.controller.ts | 2 +- src/APIs/stickers/stickers.service.ts | 2 +- .../users/{dto => dtos}/create-user.input.ts | 0 .../users/{dto => dtos}/patch-user.input.ts | 0 .../users/{dto => dtos}/upload-image.dto.ts | 0 .../users/{dto => dtos}/user-response.dto.ts | 0 src/APIs/users/users.controller.ts | 4 ++-- src/APIs/users/users.service.ts | 4 ++-- 49 files changed, 87 insertions(+), 50 deletions(-) rename src/APIs/auth/{dto => dtos}/kakao-user.dto.ts (100%) rename src/APIs/likes/{dto => dtos}/fetch-likes-response.dto.ts (100%) rename src/APIs/likes/{dto => dtos}/fetch-likes.dto.ts (100%) rename src/APIs/likes/{dto => dtos}/toggle-like-response.dto.ts (100%) rename src/APIs/likes/{dto => dtos}/toggle-like.dto.ts (100%) rename src/APIs/neighbors/{dto => dtos}/follow-user.dto.ts (100%) rename src/APIs/neighbors/{dto => dtos}/follow.dto.ts (100%) rename src/APIs/neighbors/{dto => dtos}/from-user-response.dto.ts (78%) rename src/APIs/neighbors/{dto => dtos}/to-user-response.dto.ts (78%) rename src/APIs/postCategories/{dto => dtos}/create-post-category-response.dto.ts (100%) rename src/APIs/postCategories/{dto => dtos}/create-post-category.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/create-post.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/create-post.input.ts (100%) rename src/APIs/posts/{dto => dtos}/fetch-friends-posts.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/fetch-posts.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/page-post-response.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/post-response.dto.ts (94%) rename src/APIs/posts/{dto => dtos}/publish-post.dto.ts (100%) rename src/APIs/posts/{dto => dtos}/publish-post.input.ts (100%) rename src/APIs/stickerBlocks/{dto => dtos}/create-stickerBlock.dto.ts (100%) rename src/APIs/stickerCategories/{dto => dtos}/map-category.dto.ts (100%) rename src/APIs/stickers/{dto => dtos}/create-sticker.dto.ts (100%) rename src/APIs/stickers/{dto => dtos}/remove-bg.dto.ts (100%) rename src/APIs/users/{dto => dtos}/create-user.input.ts (100%) rename src/APIs/users/{dto => dtos}/patch-user.input.ts (100%) rename src/APIs/users/{dto => dtos}/upload-image.dto.ts (100%) rename src/APIs/users/{dto => dtos}/user-response.dto.ts (100%) diff --git a/src/APIs/auth/dto/kakao-user.dto.ts b/src/APIs/auth/dtos/kakao-user.dto.ts similarity index 100% rename from src/APIs/auth/dto/kakao-user.dto.ts rename to src/APIs/auth/dtos/kakao-user.dto.ts diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 5e8db43..c3621b6 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -30,7 +30,11 @@ export class Comment { @RelationId((comment: Comment) => comment.posts) postsId: number; - @ManyToOne(() => Posts, (posts) => posts.id, { nullable: false }) + @ManyToOne(() => Posts, (posts) => posts.id, { + nullable: false, + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', + }) @JoinColumn() posts: Posts; @@ -42,6 +46,7 @@ export class Comment { @ManyToOne(() => Comment, (comment) => comment.children, { nullable: true, + onUpdate: 'NO ACTION', onDelete: 'NO ACTION', }) @JoinColumn() diff --git a/src/APIs/likes/dto/fetch-likes-response.dto.ts b/src/APIs/likes/dtos/fetch-likes-response.dto.ts similarity index 100% rename from src/APIs/likes/dto/fetch-likes-response.dto.ts rename to src/APIs/likes/dtos/fetch-likes-response.dto.ts diff --git a/src/APIs/likes/dto/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts similarity index 100% rename from src/APIs/likes/dto/fetch-likes.dto.ts rename to src/APIs/likes/dtos/fetch-likes.dto.ts diff --git a/src/APIs/likes/dto/toggle-like-response.dto.ts b/src/APIs/likes/dtos/toggle-like-response.dto.ts similarity index 100% rename from src/APIs/likes/dto/toggle-like-response.dto.ts rename to src/APIs/likes/dtos/toggle-like-response.dto.ts diff --git a/src/APIs/likes/dto/toggle-like.dto.ts b/src/APIs/likes/dtos/toggle-like.dto.ts similarity index 100% rename from src/APIs/likes/dto/toggle-like.dto.ts rename to src/APIs/likes/dtos/toggle-like.dto.ts diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index 30621c0..a188688 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -18,7 +18,7 @@ export class Likes { @ApiProperty({ description: '좋아요를 누른 유저', type: User }) @JoinColumn() - @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; @RelationId((like: Likes) => like.user) @@ -29,7 +29,11 @@ export class Likes { type: Posts, }) @JoinColumn() - @ManyToOne(() => Posts, { nullable: false, onDelete: 'CASCADE' }) + @ManyToOne(() => Posts, { + nullable: false, + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', + }) posts: Posts; @RelationId((like: Likes) => like.posts) diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index eed6c79..152587c 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -16,12 +16,12 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ToggleLikeDto } from './dto/toggle-like.dto'; +import { ToggleLikeDto } from './dtos/toggle-like.dto'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; -import { ToggleLikeResponseDto } from './dto/toggle-like-response.dto'; +import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; -import { FetchLikesResponseDto } from './dto/fetch-likes-response.dto'; +import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; @ApiTags('좋아요 API') @Controller('likes') diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 26758ae..ea41554 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -3,9 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Posts } from '../posts/entities/posts.entity'; -import { ToggleLikeResponseDto } from './dto/toggle-like-response.dto'; -import { FetchLikesDto } from './dto/fetch-likes.dto'; -import { USER_SELECT_OPTION } from '../users/dto/user-response.dto'; +import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; +import { FetchLikesDto } from './dtos/fetch-likes.dto'; +import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; @Injectable() export class LikesService { diff --git a/src/APIs/neighbors/dto/follow-user.dto.ts b/src/APIs/neighbors/dtos/follow-user.dto.ts similarity index 100% rename from src/APIs/neighbors/dto/follow-user.dto.ts rename to src/APIs/neighbors/dtos/follow-user.dto.ts diff --git a/src/APIs/neighbors/dto/follow.dto.ts b/src/APIs/neighbors/dtos/follow.dto.ts similarity index 100% rename from src/APIs/neighbors/dto/follow.dto.ts rename to src/APIs/neighbors/dtos/follow.dto.ts diff --git a/src/APIs/neighbors/dto/from-user-response.dto.ts b/src/APIs/neighbors/dtos/from-user-response.dto.ts similarity index 78% rename from src/APIs/neighbors/dto/from-user-response.dto.ts rename to src/APIs/neighbors/dtos/from-user-response.dto.ts index 56e895f..4249ae0 100644 --- a/src/APIs/neighbors/dto/from-user-response.dto.ts +++ b/src/APIs/neighbors/dtos/from-user-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserResponseDto } from 'src/APIs/users/dto/user-response.dto'; +import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; export class FromUserResponseDto { @ApiProperty({ diff --git a/src/APIs/neighbors/dto/to-user-response.dto.ts b/src/APIs/neighbors/dtos/to-user-response.dto.ts similarity index 78% rename from src/APIs/neighbors/dto/to-user-response.dto.ts rename to src/APIs/neighbors/dtos/to-user-response.dto.ts index ce611e6..b37acc0 100644 --- a/src/APIs/neighbors/dto/to-user-response.dto.ts +++ b/src/APIs/neighbors/dtos/to-user-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserResponseDto } from 'src/APIs/users/dto/user-response.dto'; +import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; export class ToUserResponseDto { @ApiProperty({ diff --git a/src/APIs/neighbors/entities/neighbor.entity.ts b/src/APIs/neighbors/entities/neighbor.entity.ts index abf566a..96417c3 100644 --- a/src/APIs/neighbors/entities/neighbor.entity.ts +++ b/src/APIs/neighbors/entities/neighbor.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserResponseDto } from 'src/APIs/users/dto/user-response.dto'; +import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { User } from 'src/APIs/users/entities/user.entity'; import { Entity, @@ -17,12 +17,20 @@ export class Neighbor { @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 받은 유저' }) @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { nullable: false }) + @ManyToOne(() => User, (users) => users.kakaoId, { + nullable: false, + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', + }) to_user: User; @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 한 유저' }) @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { nullable: false }) + @ManyToOne(() => User, (users) => users.kakaoId, { + nullable: false, + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', + }) from_user: User; @RelationId((neighbor: Neighbor) => neighbor.to_user) // you need to specify target relation diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/neighbors/neighbors.controller.ts index de87896..544753d 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/neighbors/neighbors.controller.ts @@ -20,10 +20,10 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { FollowDto } from './dto/follow.dto'; -import { FromUserResponseDto } from './dto/from-user-response.dto'; -import { ToUserResponseDto } from './dto/to-user-response.dto'; -import { FollowUserDto } from './dto/follow-user.dto'; +import { FollowDto } from './dtos/follow.dto'; +import { FromUserResponseDto } from './dtos/from-user-response.dto'; +import { ToUserResponseDto } from './dtos/to-user-response.dto'; +import { FollowUserDto } from './dtos/follow-user.dto'; @ApiTags('이웃 API') @Controller('neighbors') diff --git a/src/APIs/neighbors/neighbors.service.ts b/src/APIs/neighbors/neighbors.service.ts index cf16015..a82426d 100644 --- a/src/APIs/neighbors/neighbors.service.ts +++ b/src/APIs/neighbors/neighbors.service.ts @@ -2,10 +2,10 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Neighbor } from './entities/neighbor.entity'; import { DataSource, Repository } from 'typeorm'; -import { FromUserResponseDto } from './dto/from-user-response.dto'; -import { ToUserResponseDto } from './dto/to-user-response.dto'; -import { FollowUserDto } from './dto/follow-user.dto'; -import { USER_SELECT_OPTION } from '../users/dto/user-response.dto'; +import { FromUserResponseDto } from './dtos/from-user-response.dto'; +import { ToUserResponseDto } from './dtos/to-user-response.dto'; +import { FollowUserDto } from './dtos/follow-user.dto'; +import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; @Injectable() export class NeighborsService { diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 99779ab..96dcfb7 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -19,8 +19,8 @@ import { import { PostCategoriesService } from './PostCategories.service'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; -import { CreatePostCategoryDto } from './dto/create-post-category.dto'; -import { CreatePostCategoryResponseDto } from './dto/create-post-category-response.dto'; +import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; +import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { PostCategory } from './entities/postCategory.entity'; @ApiTags('카테고리 API') diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index df7a826..d402663 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PostCategory } from './entities/postCategory.entity'; import { Repository } from 'typeorm'; -import { CreatePostCategoryResponseDto } from './dto/create-post-category-response.dto'; +import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; @Injectable() export class PostCategoriesService { diff --git a/src/APIs/postCategories/dto/create-post-category-response.dto.ts b/src/APIs/postCategories/dtos/create-post-category-response.dto.ts similarity index 100% rename from src/APIs/postCategories/dto/create-post-category-response.dto.ts rename to src/APIs/postCategories/dtos/create-post-category-response.dto.ts diff --git a/src/APIs/postCategories/dto/create-post-category.dto.ts b/src/APIs/postCategories/dtos/create-post-category.dto.ts similarity index 100% rename from src/APIs/postCategories/dto/create-post-category.dto.ts rename to src/APIs/postCategories/dtos/create-post-category.dto.ts diff --git a/src/APIs/postCategories/entities/postCategory.entity.ts b/src/APIs/postCategories/entities/postCategory.entity.ts index ebfbead..45a4f1f 100644 --- a/src/APIs/postCategories/entities/postCategory.entity.ts +++ b/src/APIs/postCategories/entities/postCategory.entity.ts @@ -20,9 +20,10 @@ export class PostCategory { name: string; @JoinColumn() - @ManyToOne(() => User) + @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; + @Column() @RelationId((postCategory: PostCategory) => postCategory.user) userKakaoId: number; } diff --git a/src/APIs/posts/dto/create-post.dto.ts b/src/APIs/posts/dtos/create-post.dto.ts similarity index 100% rename from src/APIs/posts/dto/create-post.dto.ts rename to src/APIs/posts/dtos/create-post.dto.ts diff --git a/src/APIs/posts/dto/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts similarity index 100% rename from src/APIs/posts/dto/create-post.input.ts rename to src/APIs/posts/dtos/create-post.input.ts diff --git a/src/APIs/posts/dto/fetch-friends-posts.dto.ts b/src/APIs/posts/dtos/fetch-friends-posts.dto.ts similarity index 100% rename from src/APIs/posts/dto/fetch-friends-posts.dto.ts rename to src/APIs/posts/dtos/fetch-friends-posts.dto.ts diff --git a/src/APIs/posts/dto/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts similarity index 100% rename from src/APIs/posts/dto/fetch-posts.dto.ts rename to src/APIs/posts/dtos/fetch-posts.dto.ts diff --git a/src/APIs/posts/dto/page-post-response.dto.ts b/src/APIs/posts/dtos/page-post-response.dto.ts similarity index 100% rename from src/APIs/posts/dto/page-post-response.dto.ts rename to src/APIs/posts/dtos/page-post-response.dto.ts diff --git a/src/APIs/posts/dto/post-response.dto.ts b/src/APIs/posts/dtos/post-response.dto.ts similarity index 94% rename from src/APIs/posts/dto/post-response.dto.ts rename to src/APIs/posts/dtos/post-response.dto.ts index c60c8de..0a0124e 100644 --- a/src/APIs/posts/dto/post-response.dto.ts +++ b/src/APIs/posts/dtos/post-response.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum } from 'class-validator'; import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; -import { UserResponseDto } from 'src/APIs/users/dto/user-response.dto'; +import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; export class PostResponseDto { diff --git a/src/APIs/posts/dto/publish-post.dto.ts b/src/APIs/posts/dtos/publish-post.dto.ts similarity index 100% rename from src/APIs/posts/dto/publish-post.dto.ts rename to src/APIs/posts/dtos/publish-post.dto.ts diff --git a/src/APIs/posts/dto/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts similarity index 100% rename from src/APIs/posts/dto/publish-post.input.ts rename to src/APIs/posts/dtos/publish-post.input.ts diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index 0a3c48e..6886ce6 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -24,18 +24,30 @@ export class Posts { id: number; @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) - @ManyToOne(() => PostCategory, { nullable: true, onDelete: 'CASCADE' }) + @ManyToOne(() => PostCategory, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) @JoinColumn() postCategory: PostCategory; @ApiProperty({ description: '연결된 내지', type: PostBackground }) @JoinColumn() - @ManyToOne(() => PostBackground, { nullable: true, onDelete: 'CASCADE' }) + @ManyToOne(() => PostBackground, { + nullable: false, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) postBackground: PostBackground; @ApiProperty({ description: '작성자', type: User }) @JoinColumn() - @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false }) + @ManyToOne(() => User, { + nullable: false, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) user: User; @IsString() @@ -46,7 +58,7 @@ export class Posts { @IsString() @ApiProperty({ description: '연결된 내지 fk', type: String }) - @Column({ nullable: true }) + @Column({ nullable: false }) @RelationId((posts: Posts) => posts.postBackground) postBackgroundId: string; diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 823fa28..bfbe80d 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -23,12 +23,12 @@ import { } from '@nestjs/swagger'; import { Request } from 'express'; import { PostsService } from './posts.service'; -import { FetchPostsDto } from './dto/fetch-posts.dto'; -import { PublishPostDto } from './dto/publish-post.dto'; +import { FetchPostsDto } from './dtos/fetch-posts.dto'; +import { PublishPostDto } from './dtos/publish-post.dto'; import { Posts } from './entities/posts.entity'; -import { PagePostResponseDto } from './dto/page-post-response.dto'; -import { CreatePostInput } from './dto/create-post.input'; -import { PublishPostInput } from './dto/publish-post.input'; +import { PagePostResponseDto } from './dtos/page-post-response.dto'; +import { CreatePostInput } from './dtos/create-post.input'; +import { PublishPostInput } from './dtos/publish-post.input'; import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index ef9f3bb..021b281 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -9,11 +9,11 @@ import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Page } from '../../utils/pages/page'; -import { FetchPostsDto } from './dto/fetch-posts.dto'; -import { PagePostResponseDto } from './dto/page-post-response.dto'; +import { FetchPostsDto } from './dtos/fetch-posts.dto'; +import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { Neighbor } from '../neighbors/entities/neighbor.entity'; -import { FetchFriendsPostsDto } from './dto/fetch-friends-posts.dto'; -import { CreatePostDto } from './dto/create-post.dto'; +import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; +import { CreatePostDto } from './dtos/create-post.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { User } from '../users/entities/user.entity'; diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index d5c2be9..51ed381 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -24,11 +24,12 @@ export class Report { @CreateDateColumn() date_created: string; + @Column() @RelationId((report: Report) => report.user) userKakaoId: number; @JoinColumn() - @ManyToOne(() => User) + @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @Column() diff --git a/src/APIs/stickerBlocks/dto/create-stickerBlock.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts similarity index 100% rename from src/APIs/stickerBlocks/dto/create-stickerBlock.dto.ts rename to src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index 051fed0..cde7217 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; -import { CreateStickerBlockDto } from './dto/create-stickerBlock.dto'; +import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; @ApiTags('스티커 블록 API') @Controller('stickerBlocks') diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 2e679da..9f2f045 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { StickerBlock } from './entities/stickerblock.entity'; import { Repository } from 'typeorm'; -import { CreateStickerBlockDto } from './dto/create-stickerBlock.dto'; +import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; import { StickersService } from '../stickers/stickers.service'; @Injectable() diff --git a/src/APIs/stickerCategories/dto/map-category.dto.ts b/src/APIs/stickerCategories/dtos/map-category.dto.ts similarity index 100% rename from src/APIs/stickerCategories/dto/map-category.dto.ts rename to src/APIs/stickerCategories/dtos/map-category.dto.ts diff --git a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts index 8bef5f0..7615194 100644 --- a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts @@ -19,6 +19,7 @@ export class StickerCategoryMapper { @JoinColumn() @ManyToOne(() => Sticker, (stickers) => stickers.id, { + onUpdate: 'CASCADE' onDelete: 'CASCADE', }) sticker: Sticker; @@ -35,6 +36,7 @@ export class StickerCategoryMapper { () => StickerCategory, (stickerCategories) => stickerCategories.id, { + onUpdate: 'CASCADE', onDelete: 'CASCADE', }, ) diff --git a/src/APIs/stickers/dto/create-sticker.dto.ts b/src/APIs/stickers/dtos/create-sticker.dto.ts similarity index 100% rename from src/APIs/stickers/dto/create-sticker.dto.ts rename to src/APIs/stickers/dtos/create-sticker.dto.ts diff --git a/src/APIs/stickers/dto/remove-bg.dto.ts b/src/APIs/stickers/dtos/remove-bg.dto.ts similarity index 100% rename from src/APIs/stickers/dto/remove-bg.dto.ts rename to src/APIs/stickers/dtos/remove-bg.dto.ts diff --git a/src/APIs/stickers/entities/sticker.entity.ts b/src/APIs/stickers/entities/sticker.entity.ts index 3bada5a..7a9317f 100644 --- a/src/APIs/stickers/entities/sticker.entity.ts +++ b/src/APIs/stickers/entities/sticker.entity.ts @@ -22,7 +22,11 @@ export class Sticker { // @ApiProperty({ description: '제작한 유저', type: User }) @JoinColumn() - @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false }) + @ManyToOne(() => User, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + nullable: false, + }) user: User; @ApiProperty({ diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 197c2ce..4a9af75 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -27,7 +27,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; -import { RemoveBgDto } from './dto/remove-bg.dto'; +import { RemoveBgDto } from './dtos/remove-bg.dto'; @ApiTags('스티커 API') @Controller('stickers') diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 5466bd0..f17dcb4 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -6,7 +6,7 @@ import { import { Repository } from 'typeorm'; import { Sticker } from './entities/sticker.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { CreateStickerDto } from './dto/create-sticker.dto'; +import { CreateStickerDto } from './dtos/create-sticker.dto'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; diff --git a/src/APIs/users/dto/create-user.input.ts b/src/APIs/users/dtos/create-user.input.ts similarity index 100% rename from src/APIs/users/dto/create-user.input.ts rename to src/APIs/users/dtos/create-user.input.ts diff --git a/src/APIs/users/dto/patch-user.input.ts b/src/APIs/users/dtos/patch-user.input.ts similarity index 100% rename from src/APIs/users/dto/patch-user.input.ts rename to src/APIs/users/dtos/patch-user.input.ts diff --git a/src/APIs/users/dto/upload-image.dto.ts b/src/APIs/users/dtos/upload-image.dto.ts similarity index 100% rename from src/APIs/users/dto/upload-image.dto.ts rename to src/APIs/users/dtos/upload-image.dto.ts diff --git a/src/APIs/users/dto/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts similarity index 100% rename from src/APIs/users/dto/user-response.dto.ts rename to src/APIs/users/dtos/user-response.dto.ts diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index fc6cb5a..61c2a25 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -23,8 +23,8 @@ import { } from '@nestjs/swagger'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; -import { UserResponseDto } from './dto/user-response.dto'; -import { PatchUserInput } from './dto/patch-user.input'; +import { UserResponseDto } from './dtos/user-response.dto'; +import { PatchUserInput } from './dtos/patch-user.input'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 979bd23..a29ed27 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -6,11 +6,11 @@ import { IUsersServiceCreate, IUsersServiceFindUserByKakaoId, } from './interfaces/users.service.interface'; -import { USER_SELECT_OPTION, UserResponseDto } from './dto/user-response.dto'; +import { USER_SELECT_OPTION, UserResponseDto } from './dtos/user-response.dto'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; -import { UploadImageDto } from './dto/upload-image.dto'; +import { UploadImageDto } from './dtos/upload-image.dto'; @Injectable() export class UsersService { From bb7b30d85ef42194973d52fd800837a024a60365 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 11:17:41 +0900 Subject: [PATCH 006/236] fix: directory path dto -> dtos --- src/APIs/auth/auth.service.ts | 2 +- .../stickerCategories/entities/stickerCategoryMapper.entity.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/auth/auth.service.ts b/src/APIs/auth/auth.service.ts index 9e47bc3..72fd9ed 100644 --- a/src/APIs/auth/auth.service.ts +++ b/src/APIs/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { UsersService } from '../users/users.service'; -import { KakaoUserDto } from './dto/kakao-user.dto'; +import { KakaoUserDto } from './dtos/kakao-user.dto'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcrypt'; diff --git a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts index 7615194..9db2e66 100644 --- a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts @@ -19,7 +19,7 @@ export class StickerCategoryMapper { @JoinColumn() @ManyToOne(() => Sticker, (stickers) => stickers.id, { - onUpdate: 'CASCADE' + onUpdate: 'CASCADE', onDelete: 'CASCADE', }) sticker: Sticker; From b431e531bfc6ff7c5422fbd4f2d14a693283d504 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 11:19:25 +0900 Subject: [PATCH 007/236] fix: directory path dto -> dtos --- src/APIs/stickerCategories/stickerCategories.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 30e4a95..970966d 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -16,8 +16,8 @@ import { } from '@nestjs/swagger'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; +import { MapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; -import { MapCategoryDto } from './dto/map-category.dto'; @ApiTags('스티커 카테고리 API') @Controller('stickercg') From fad9bbae44431353232f0aab58e00c9765775d22 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Apr 2024 11:21:56 +0900 Subject: [PATCH 008/236] fix: parent comment onDelete Action --- src/APIs/comments/entities/comment.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index c3621b6..675c1b1 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -47,7 +47,7 @@ export class Comment { @ManyToOne(() => Comment, (comment) => comment.children, { nullable: true, onUpdate: 'NO ACTION', - onDelete: 'NO ACTION', + onDelete: 'CASCADE', }) @JoinColumn() parent: Comment; From 3ecd859ff2a9ef183646f4b3eafbf72c99acfe93 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Apr 2024 10:10:38 +0900 Subject: [PATCH 009/236] add: express-basic-auth --- package-lock.json | 25 +++++++++++++++++++++++++ package.json | 1 + src/app.controller.ts | 17 ++++++++++------- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99bfee7..319a43e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", + "express-basic-auth": "^1.2.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", @@ -4853,6 +4854,22 @@ } ] }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -6389,6 +6406,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dependencies": { + "basic-auth": "^2.0.1" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", diff --git a/package.json b/package.json index 27a7e25..af0722e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", + "express-basic-auth": "^1.2.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", diff --git a/src/app.controller.ts b/src/app.controller.ts index 1e1f4f3..c282842 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,14 +1,17 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpCode, Res } from '@nestjs/common'; import { AppService } from './app.service'; +import { Response } from 'express'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +@ApiTags('root') @Controller() export class AppController { constructor(private readonly appService: AppService) {} - // @UseGuards(AuthGuard('jwt')) - // @Get('/jwtTest') - // get(@Req() req: Request) { - // console.log(req.user.userId); - // return 'JWT 인증 성공'; - // } + @ApiOperation({ summary: 'swagger docs로 redirect' }) + @Get('/') + @HttpCode(301) + get(@Res() res: Response) { + return res.redirect('/api-docs'); + } } From c613a3aa91d334b1d8310d41fa4ab02fb224b2cb Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Apr 2024 10:14:39 +0900 Subject: [PATCH 010/236] add: swagger auth --- src/main.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.ts b/src/main.ts index 176db16..eb7eea7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,10 +4,20 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as cookieParser from 'cookie-parser'; import { HttpExceptionFilter } from './commons/filter/http-exception.filter'; import { ValidationPipe } from '@nestjs/common'; +import * as expressBasicAuth from 'express-basic-auth'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); + app.use( + ['/api-docs'], + expressBasicAuth({ + challenge: true, + users: { + [process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD, // 지정된 ID/비밀번호 + }, + }), + ); app.enableCors(); app.useGlobalPipes( new ValidationPipe({ From ba6973c82ea67ea5a5b814cd0e78d0f98dcfd19d Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Apr 2024 10:32:47 +0900 Subject: [PATCH 011/236] feat: removed_bg image upload to s3 --- src/APIs/stickers/stickers.controller.ts | 10 ++++------ src/APIs/stickers/stickers.service.ts | 14 ++++++++++++-- src/utils/aws/aws.service.ts | 13 +++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 4a9af75..d5b7dd1 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -28,6 +28,7 @@ import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; import { RemoveBgDto } from './dtos/remove-bg.dto'; +import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; @ApiTags('스티커 API') @Controller('stickers') @@ -138,11 +139,8 @@ export class StickersController { }) @Post('remove') @HttpCode(201) - async removeBg(@Body() body: RemoveBgDto, @Res() res) { - const blobData = await this.stickersService.removeBg({ url: body.url }); - const arrayBuffer = await blobData.arrayBuffer(); - const bufferData = Buffer.from(arrayBuffer); - res.set('Content-Type', 'image/jpeg'); - res.send(bufferData); + async removeBg(@Body() body: RemoveBgDto): Promise { + const image_url = await this.stickersService.removeBg({ url: body.url }); + return image_url; } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index f17dcb4..6d4c90a 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -12,6 +12,7 @@ import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; import { removeBackground } from '@imgly/background-removal-node'; +import { buffer } from 'stream/consumers'; @Injectable() export class StickersService { @@ -107,7 +108,16 @@ export class StickersService { }); } - async removeBg({ url }) { - return await removeBackground(url); + async removeBg({ url }): Promise { + const blobData = await removeBackground(url); + const arrayBuffer = await blobData.arrayBuffer(); + const bufferData = Buffer.from(arrayBuffer); + const imageName = this.utilsService.getUUID(); + const image_url = await this.awsService.imageUploadToS3Buffer( + imageName, + bufferData, + 'image/png', + ); + return { image_url }; } } diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index aaff2ac..118780f 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -18,6 +18,19 @@ export class AwsService { }); } + async imageUploadToS3Buffer(fileName: string, file: Buffer, ext: string) { + const command = new PutObjectCommand({ + Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 + Key: fileName, // 업로드될 파일의 이름 + Body: file, // 업로드할 파일 + ACL: 'public-read', // 파일 접근 권한 + ContentType: `image/${ext}`, // 파일 타입 + }); + const result = await this.s3Client.send(command); + console.log(result); + return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; + } + async imageUploadToS3( fileName: string, // 업로드될 파일의 이름 file: Express.Multer.File, // 업로드할 파일 From c981fae9c08d04e34e18d8b1ba341fe5af3771e0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Apr 2024 12:00:20 +0900 Subject: [PATCH 012/236] feat: update & delete s3 image --- src/APIs/stickers/dtos/find-sticker.dto.ts | 13 ++++ src/APIs/stickers/dtos/update-sticker.dto.ts | 17 +++++ src/APIs/stickers/stickers.controller.ts | 79 +++++++++++++++----- src/APIs/stickers/stickers.service.ts | 51 ++++++++++++- src/utils/aws/aws.service.ts | 30 ++++++-- 5 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 src/APIs/stickers/dtos/find-sticker.dto.ts create mode 100644 src/APIs/stickers/dtos/update-sticker.dto.ts diff --git a/src/APIs/stickers/dtos/find-sticker.dto.ts b/src/APIs/stickers/dtos/find-sticker.dto.ts new file mode 100644 index 0000000..88d87dd --- /dev/null +++ b/src/APIs/stickers/dtos/find-sticker.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; + +export class FindStickerInput { + @ApiProperty({ description: '찾을 스티커의 id', type: Number }) + @IsNumber() + id: number; +} + +export class FindStickerDto extends FindStickerInput { + @IsNumber() + kakaoId: number; +} diff --git a/src/APIs/stickers/dtos/update-sticker.dto.ts b/src/APIs/stickers/dtos/update-sticker.dto.ts new file mode 100644 index 0000000..7d41d73 --- /dev/null +++ b/src/APIs/stickers/dtos/update-sticker.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsUrl } from 'class-validator'; +import { URL } from 'url'; + +export class UpdateStickerInput { + @ApiProperty({ description: '찾을 스티커의 id', type: Number }) + @IsNumber() + id: number; + + @ApiProperty({ description: '변경할 url', type: String }) + @IsUrl() + image_url: string; +} +export class UpdateStickerDto extends UpdateStickerInput { + @IsNumber() + kakaoId: number; +} diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index d5b7dd1..3c69e44 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -1,12 +1,13 @@ import { Body, Controller, + Delete, Get, HttpCode, Param, + Patch, Post, Req, - Res, UploadedFile, UseGuards, UseInterceptors, @@ -29,6 +30,8 @@ import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; import { RemoveBgDto } from './dtos/remove-bg.dto'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { FindStickerInput } from './dtos/find-sticker.dto'; +import { UpdateStickerInput } from './dtos/update-sticker.dto'; @ApiTags('스티커 API') @Controller('stickers') @@ -94,32 +97,34 @@ export class StickersController { file, }); } + + @Get('private') @ApiOperation({ - summary: '스티커 재사용 여부를 토글한다.', + summary: '재사용 가능한 private 스티커를 fetch한다.', description: - '본인이 만든 스티커의 재사용 여부를 토글한다. 보관함 저장 혹은 삭제 용도로 사용할 것', + '본인이 만든 재사용 가능한 스티커들을 fetch한다. toggle이 우선적으로 이루어져야함.', }) - @Post('toggle/:id') + @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) @UseGuards(AuthGuard('jwt')) @ApiCookieAuth() @HttpCode(200) - async toggleReusable(@Req() req: Request, @Param('id') id: number) { + async fetchPrivateStickers(@Req() req: Request): Promise { const userKakaoId = req.user.userId; - return await this.stickersService.toggleReusable({ userKakaoId, id }); + return await this.stickersService.fetchUserStickers({ userKakaoId }); } @ApiOperation({ - summary: 'private 스티커를 fetch한다.', - description: '본인이 만든 재사용 가능한 스티커들을 fetch한다.', + summary: '스티커 재사용 여부를 토글한다.', + description: + '본인이 만든 스티커의 재사용 여부를 토글한다. 보관함 저장 혹은 삭제 용도로 사용할 것', }) - @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) - @Get('private') + @Post('toggle/:id') @UseGuards(AuthGuard('jwt')) @ApiCookieAuth() @HttpCode(200) - async fetchPrivateStickers(@Req() req: Request): Promise { + async toggleReusable(@Req() req: Request, @Param('id') id: number) { const userKakaoId = req.user.userId; - return await this.stickersService.fetchUserStickers({ userKakaoId }); + return await this.stickersService.toggleReusable({ userKakaoId, id }); } @ApiOperation({ @@ -135,12 +140,52 @@ export class StickersController { @ApiOperation({ summary: '스티커 배경을 제거한다.', - description: '스티커 배경을 제거하고, png로 받는다.', + description: `스티커 배경을 제거하고, url을 받는다. 스티커에 적용하려면 update 필요.\n + workflow: post('background') => delete('s3') => patch('image') + `, }) - @Post('remove') - @HttpCode(201) + @UseGuards(AuthGuard('jwt')) + @ApiCookieAuth() + @Post('background') async removeBg(@Body() body: RemoveBgDto): Promise { - const image_url = await this.stickersService.removeBg({ url: body.url }); - return image_url; + return await this.stickersService.removeBg({ url: body.url }); + } + + @Patch('image') + @ApiOperation({ + summary: '스티커 객체 이미지 수정', + description: + '스티커 객체의 이미지 url을 변경한다. 호출 이전에 기존의 이미지 제거를 권장.', + }) + @UseGuards(AuthGuard('jwt')) + @ApiCookieAuth() + async updateSticker( + @Req() req: Request, + @Body() body: UpdateStickerInput, + ): Promise { + const kakaoId = req.user.userId; + return await this.stickersService.updateSticker({ + kakaoId, + ...body, + }); + } + + @ApiOperation({ + summary: 's3에 업로드된 이미지 삭제', + description: `s3에 올라간 파일을 삭제한다. 스티커 객체가 삭제되지는 않는다.
+ sticker 객채에 새로운 이미지를 업데이트 해줄 때, 기존의 이미지를 제거할 때만 사용.
+ 로직 순서: delete('s3') => patch('image')
+ **만약 사용중인 객체의 이미지만 제거 할 경우 이미지가 깨진다.**`, + }) + @UseGuards(AuthGuard('jwt')) + @ApiCookieAuth() + @Delete('s3') + @HttpCode(200) + async removeS3(@Body() body: FindStickerInput, @Req() req: Request) { + const kakaoId = req.user.userId; + return await this.stickersService.removeFromS3({ + id: body.id, + kakaoId, + }); } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 6d4c90a..f25b027 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -12,7 +12,8 @@ import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; import { removeBackground } from '@imgly/background-removal-node'; -import { buffer } from 'stream/consumers'; +import { FindStickerDto } from './dtos/find-sticker.dto'; +import { UpdateStickerDto } from './dtos/update-sticker.dto'; @Injectable() export class StickersService { @@ -66,7 +67,6 @@ export class StickersService { const data = await this.stickersRepository.findOne({ where: { id } }); return data; } - async createPublicSticker({ userKakaoId, file, @@ -112,12 +112,57 @@ export class StickersService { const blobData = await removeBackground(url); const arrayBuffer = await blobData.arrayBuffer(); const bufferData = Buffer.from(arrayBuffer); + const imageName = this.utilsService.getUUID(); + const image_url = await this.awsService.imageUploadToS3Buffer( imageName, bufferData, - 'image/png', + 'png', ); return { image_url }; } + + async updateSticker({ + image_url, + kakaoId, + id, + }: UpdateStickerDto): Promise { + try { + const sticker = await this.stickersRepository.findOne({ + where: { id, user: { kakaoId } }, + }); + if (!sticker) + throw new NotFoundException( + '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', + ); + await this.stickersRepository + .createQueryBuilder() + .update(Sticker) + .set({ image_url }) + .where('id = :id', { id }) + .execute(); + const data = await this.stickersRepository.findOne({ where: { id } }); + return data; + } catch (e) { + throw e; + } + } + + async removeFromS3({ id, kakaoId }: FindStickerDto) { + try { + const sticker = await this.stickersRepository.findOne({ + where: { id, user: { kakaoId } }, + }); + if (!sticker) + throw new NotFoundException( + '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', + ); + return await this.awsService.deleteImageFromS3({ + url: sticker.image_url, + }); + } catch (e) { + throw e; + } + } } diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index 118780f..dffe46a 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -1,7 +1,12 @@ // aws.service.ts -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { + DeleteObjectCommand, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; @Injectable() export class AwsService { @@ -24,13 +29,28 @@ export class AwsService { Key: fileName, // 업로드될 파일의 이름 Body: file, // 업로드할 파일 ACL: 'public-read', // 파일 접근 권한 - ContentType: `image/${ext}`, // 파일 타입 + ContentType: `image/${ext}`, // 파일 타입, }); - const result = await this.s3Client.send(command); - console.log(result); + await this.s3Client.send(command); return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; } + async deleteImageFromS3({ url }) { + try { + const fileNameRegex = /\/([^\/]+)\.[^.]+$/; + const matches = url.match(fileNameRegex); + const objectKey = matches && matches[1]; + const deleteParams = { + Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 + Key: objectKey, + }; + const command = new DeleteObjectCommand(deleteParams); + return await this.s3Client.send(command); + } catch (e) { + throw new BadRequestException('존재하지 않거나 적합하지 않은 url입니다.'); + } + } + async imageUploadToS3( fileName: string, // 업로드될 파일의 이름 file: Express.Multer.File, // 업로드할 파일 From e064525a0af4d71972ed9b7d662f7c9a76a25df0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Apr 2024 13:06:27 +0900 Subject: [PATCH 013/236] feat: add cors restriction --- src/APIs/stickerCategories/stickerCategories.service.ts | 2 +- src/main.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 01e76cb..0ed1437 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -57,7 +57,7 @@ export class StickerCategoriesService { async fetchStickersByCategoryName({ name }) { await this.existCheckByName({ name }); - return this.stickerCategoryMappersRepository.find({ + return await this.stickerCategoryMappersRepository.find({ relations: { sticker: true, stickerCategory: true }, where: { stickerCategory: { name }, sticker: { isDefault: true } }, }); diff --git a/src/main.ts b/src/main.ts index eb7eea7..0b8a52b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,14 @@ async function bootstrap() { }, }), ); - app.enableCors(); + app.enableCors({ + origin: [ + 'http://localhost:3000/', + 'https://blccu.com/', + 'https://www.blccu.com/', + ], + credentials: true, + }); app.useGlobalPipes( new ValidationPipe({ transform: true, From d9f3c80363900a1a031c2f31749c4e8a4de28b1f Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Apr 2024 12:05:24 +0900 Subject: [PATCH 014/236] fix: only get kakaoId on kakao-user.dto --- src/APIs/auth/dtos/kakao-user.dto.ts | 6 ------ src/APIs/auth/interfaces/auth.service.interface.ts | 0 2 files changed, 6 deletions(-) delete mode 100644 src/APIs/auth/interfaces/auth.service.interface.ts diff --git a/src/APIs/auth/dtos/kakao-user.dto.ts b/src/APIs/auth/dtos/kakao-user.dto.ts index a17abfe..ffb6500 100644 --- a/src/APIs/auth/dtos/kakao-user.dto.ts +++ b/src/APIs/auth/dtos/kakao-user.dto.ts @@ -3,10 +3,4 @@ import { ApiProperty } from '@nestjs/swagger'; export class KakaoUserDto { @ApiProperty() kakaoId: number; - - @ApiProperty() - username: string; - - @ApiProperty() - profile_image: string; } diff --git a/src/APIs/auth/interfaces/auth.service.interface.ts b/src/APIs/auth/interfaces/auth.service.interface.ts deleted file mode 100644 index e69de29..0000000 From 89152d491af895bf6f2ac498f821db1c567e2e72 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Apr 2024 12:07:57 +0900 Subject: [PATCH 015/236] fix: delete username & profile_image from user create dto --- src/APIs/auth/strategies/kakao.stategy.ts | 7 ------- src/APIs/users/interfaces/users.service.interface.ts | 2 -- src/APIs/users/users.service.ts | 6 ++---- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/APIs/auth/strategies/kakao.stategy.ts b/src/APIs/auth/strategies/kakao.stategy.ts index 161d80b..25832c7 100644 --- a/src/APIs/auth/strategies/kakao.stategy.ts +++ b/src/APIs/auth/strategies/kakao.stategy.ts @@ -20,16 +20,9 @@ export class KakaoStategy extends PassportStrategy(Strategy, 'kakao') { ) { try { const { _json } = profile; - const userData = _json.properties; const user = { kakaoId: _json.id, - username: userData.nickname, - profile_image: userData.profile_image, }; - // 프로필 이미지 비동의 시 기본값 설정해주기! - if (!userData.profile_image) { - user.profile_image = ''; - } done(null, user); } catch (error) { done(error); diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index 8b28876..78d3065 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -1,7 +1,5 @@ export interface IUsersServiceCreate { kakaoId: number; - profile_image: string; - username: string; } export interface IUsersServiceFindUserByKakaoId { diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index a29ed27..963f5eb 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -32,11 +32,9 @@ export class UsersService { }); if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); } - async create({ kakaoId, username, profile_image }: IUsersServiceCreate) { + async create({ kakaoId }: IUsersServiceCreate) { const result = await this.usersRepository.save({ - kakaoId: kakaoId, - username, - profile_image, + kakaoId, }); return result; } From 86a0bdcfb65dd4bc80bdf4672ee53be6ce1886e9 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Apr 2024 12:36:48 +0900 Subject: [PATCH 016/236] feat: add user tempname create logic --- src/APIs/users/users.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 963f5eb..91c6bea 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -33,8 +33,10 @@ export class UsersService { if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); } async create({ kakaoId }: IUsersServiceCreate) { + const userTempName = "user" + this.utilsService.getUUID().substring(0, 8); const result = await this.usersRepository.save({ kakaoId, + username: userTempName; }); return result; } From 7e68aa0cafc4702d67d9d1a8722c0e6fe77196fa Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Apr 2024 12:42:19 +0900 Subject: [PATCH 017/236] feat: set user profile image default value 0 --- src/APIs/auth/auth.controller.ts | 2 -- src/APIs/users/entities/user.entity.ts | 20 ++++++++++---------- src/APIs/users/users.service.ts | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 76fadaf..942281b 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -39,8 +39,6 @@ export class AuthController { // console.log(req.user); const { accessToken, refreshToken } = await this.authService.getJWT({ kakaoId: req.user.kakaoId, - username: req.user.username, - profile_image: req.user.profile_image, }); res.cookie('accessToken', accessToken, { httpOnly: true }); res.cookie('refreshToken', refreshToken, { httpOnly: true }); diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index b9abf3d..5dee25c 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -11,38 +11,38 @@ import { @Entity() export class User { @Column({ type: 'bigint', primary: true }) - @ApiProperty({ description: '카카오 id' }) + @ApiProperty({ description: '카카오 id', type: Number }) kakaoId: number; @Column({ default: '' }) - @ApiProperty({ description: 'crypted refresh token' }) + @ApiProperty({ description: 'crypted refresh token', type: String }) current_refresh_token: string; @Column({ default: false }) - @ApiProperty({ description: '어드민 유저 여부' }) + @ApiProperty({ description: '어드민 유저 여부', type: Boolean }) isAdmin: boolean; @Column() - @ApiProperty({ description: '유저 이름' }) + @ApiProperty({ description: '유저 이름', type: String }) username: string; @Column({ default: '' }) - @ApiProperty({ description: '유저 설명' }) + @ApiProperty({ description: '유저 설명', type: String }) description: string; - @Column() - @ApiProperty({ description: '프로필 이미지 url' }) + @Column({ default: '' }) + @ApiProperty({ description: '프로필 이미지 url', type: String }) profile_image: string; @Column({ default: '' }) - @ApiProperty({ description: '프로필 배경 이미지 url' }) + @ApiProperty({ description: '프로필 배경 이미지 url', type: String }) background_image: string; @CreateDateColumn() - @ApiProperty({ description: '생성된 날짜' }) + @ApiProperty({ description: '생성된 날짜', type: Date }) date_created: Date; @DeleteDateColumn() - @ApiProperty({ description: '삭제된 날짜' }) + @ApiProperty({ description: '삭제된 날짜', type: Date }) date_deleted: Date; } diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 91c6bea..1922255 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -33,10 +33,10 @@ export class UsersService { if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); } async create({ kakaoId }: IUsersServiceCreate) { - const userTempName = "user" + this.utilsService.getUUID().substring(0, 8); + const userTempName = 'user' + this.utilsService.getUUID().substring(0, 8); const result = await this.usersRepository.save({ kakaoId, - username: userTempName; + username: userTempName, }); return result; } From 51048d96314db2ac684c8b6ed6ad126fb3ab7004 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Apr 2024 12:43:59 +0900 Subject: [PATCH 018/236] fix: set username unique column --- src/APIs/users/entities/user.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 5dee25c..414611e 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -22,7 +22,7 @@ export class User { @ApiProperty({ description: '어드민 유저 여부', type: Boolean }) isAdmin: boolean; - @Column() + @Column({ unique: true }) @ApiProperty({ description: '유저 이름', type: String }) username: string; From 7831eb87995d7f9216f9f2a14c3f1e80c106bb19 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 14 Apr 2024 07:23:58 +0900 Subject: [PATCH 019/236] feat: fetch user's posts with category restriction option --- .../postCategories/PostCategories.service.ts | 2 +- .../dtos/create-post-category-response.dto.ts | 11 +++++-- src/APIs/posts/dtos/fetch-user-posts.dto.ts | 7 +++++ src/APIs/posts/dtos/fetch-user-posts.input.ts | 14 +++++++++ src/APIs/posts/posts.controller.ts | 20 ++++++++++++ src/APIs/posts/posts.repository.ts | 24 ++++++++++++++ src/APIs/posts/posts.service.ts | 31 +++++++++++++++++++ src/app.module.ts | 2 +- src/commons/types/express.d.ts | 2 -- 9 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 src/APIs/posts/dtos/fetch-user-posts.dto.ts create mode 100644 src/APIs/posts/dtos/fetch-user-posts.input.ts diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index d402663..43cb23e 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -13,7 +13,7 @@ export class PostCategoriesService { async create({ kakaoId, name }): Promise { const result = await this.postCategoriesRepository.save({ - user: kakaoId, + user: { kakaoId }, name, }); return result; diff --git a/src/APIs/postCategories/dtos/create-post-category-response.dto.ts b/src/APIs/postCategories/dtos/create-post-category-response.dto.ts index 7103fc0..77694ef 100644 --- a/src/APIs/postCategories/dtos/create-post-category-response.dto.ts +++ b/src/APIs/postCategories/dtos/create-post-category-response.dto.ts @@ -1,4 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { User } from 'src/APIs/users/entities/user.entity'; export class CreatePostCategoryResponseDto { @ApiProperty({ @@ -17,5 +18,11 @@ export class CreatePostCategoryResponseDto { type: Number, description: '유저 kakaoId', }) - user: number; + userKakaoId: number; + + @ApiProperty({ + type: PickType(User, ['kakaoId']), + description: '유저의 picktype', + }) + user: User; } diff --git a/src/APIs/posts/dtos/fetch-user-posts.dto.ts b/src/APIs/posts/dtos/fetch-user-posts.dto.ts new file mode 100644 index 0000000..0312ccb --- /dev/null +++ b/src/APIs/posts/dtos/fetch-user-posts.dto.ts @@ -0,0 +1,7 @@ +import { FetchUserPostsInput } from './fetch-user-posts.input'; + +export class FetchUserPostsDto extends FetchUserPostsInput { + kakaoId: number; + + targetKakaoId: number; +} diff --git a/src/APIs/posts/dtos/fetch-user-posts.input.ts b/src/APIs/posts/dtos/fetch-user-posts.input.ts new file mode 100644 index 0000000..e4265f0 --- /dev/null +++ b/src/APIs/posts/dtos/fetch-user-posts.input.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsOptional } from 'class-validator'; + +export class FetchUserPostsInput { + @ApiProperty({ + description: '필터링할 카테고리 이름', + type: String, + required: false, + }) + @IsOptional() + @Type(() => String) + postCategoryName?: string | null; +} diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index bfbe80d..d76058b 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -32,6 +32,7 @@ import { PublishPostInput } from './dtos/publish-post.input'; import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; @ApiTags('게시글 API') @Controller('posts') @@ -185,4 +186,23 @@ export class PostsController { async fetchPostDetail(@Param('id') id: number) { return await this.postsService.fetchDetail({ id }); } + + @Get('unauth/user/kakaoId') + async fetchUnauthUserPosts(@Param('kakaoId') targetKakaoId: number) {} + + @ApiCookieAuth() + @UseGuards(AuthGuard('jwt')) + @Get('auth/user/:kakaoId') + async fetchAuthUserPosts( + @Param('kakaoId') targetKakaoId: number, + @Req() req: Request, + @Query() query: FetchUserPostsInput, + ) { + const kakaoId = req.user.userId; + return await this.postsService.fetchUserPosts({ + kakaoId, + targetKakaoId, + ...query, + }); + } } diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 0cb5009..a0d4f0e 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -2,6 +2,7 @@ import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -97,4 +98,27 @@ export class PostsRepository extends Repository { .andWhere(`p.isPublished = false`) .getMany(); } + + async fetchUserPosts({ scope, userKakaoId, postCategoryName }) { + console.log(postCategoryName); + const query = this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .innerJoinAndSelect('p.postBackground', 'postBackground') + .innerJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.userKakaoId = :userKakaoId', { userKakaoId }) + .andWhere('p.scope IN (:scope)', { scope }) + .andWhere('p.isPublished = true'); + if (postCategoryName) { + query.andWhere('postCategory.name = :postCategoryName', { + postCategoryName, + }); + } + return await query.getMany(); + } } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 021b281..178a35c 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -21,6 +21,8 @@ import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dt import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; import { CommentsService } from '../comments/comments.service'; +import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; +import { OpenScope } from 'src/commons/enums/open-scope.enum'; @Injectable() export class PostsService { @@ -38,6 +40,19 @@ export class PostsService { return await this.imageUpload(file); } + async getScope({ from_user, to_user }) { + console.log(from_user, to_user); + if (from_user === to_user) + return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; + const neighbor = await this.neighborsRepository.findOne({ + where: { from_user, to_user }, + }); + if (neighbor) { + return [OpenScope.PUBLIC, OpenScope.PROTECTED]; + } + return [OpenScope.PUBLIC]; + } + async imageUpload( file: Express.Multer.File, ): Promise { @@ -188,4 +203,20 @@ export class PostsService { async hardDelete({ kakaoId, id }) { return await this.postsRepository.delete({ user: { kakaoId }, id }); } + + async fetchUserPosts({ + kakaoId, + targetKakaoId, + postCategoryName, + }: FetchUserPostsDto) { + const scope = await this.getScope({ + from_user: targetKakaoId, + to_user: kakaoId, + }); + return await this.postsRepository.fetchUserPosts({ + scope, + userKakaoId: targetKakaoId, + postCategoryName, + }); + } } diff --git a/src/app.module.ts b/src/app.module.ts index 9770195..a8e57d7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,7 +46,7 @@ import { ReportsModule } from './APIs/reports/reports.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: true, + synchronize: false, logging: true, }), ], diff --git a/src/commons/types/express.d.ts b/src/commons/types/express.d.ts index 8967a8c..4677857 100644 --- a/src/commons/types/express.d.ts +++ b/src/commons/types/express.d.ts @@ -5,8 +5,6 @@ declare module 'express' { interface Request extends Req { user: { kakaoId?: number; - username?: string; - profile_image?: string; userId?: Types.ObjectId; }; } From e2fe6831427cdb2ed28c38f0c199b30e97d39beb Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 14 Apr 2024 07:27:06 +0900 Subject: [PATCH 020/236] feat: add category naming constraint --- src/APIs/postCategories/PostCategories.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index 43cb23e..6957802 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PostCategory } from './entities/postCategory.entity'; import { Repository } from 'typeorm'; @@ -10,8 +10,17 @@ export class PostCategoriesService { @InjectRepository(PostCategory) private readonly postCategoriesRepository: Repository, ) {} + async findWithName({ kakaoId, name }) { + return await this.postCategoriesRepository.find({ + where: { user: { kakaoId }, name }, + }); + } async create({ kakaoId, name }): Promise { + const data = await this.findWithName({ kakaoId, name }); + if (data) { + throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); + } const result = await this.postCategoriesRepository.save({ user: { kakaoId }, name, From 1ac1ef8fcdc67778ca296ea62ba78313803d1bec Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 14 Apr 2024 14:29:28 +0900 Subject: [PATCH 021/236] feat: divide functions of AuthGuard for parsing middleware & guard --- src/APIs/auth/auth.controller.ts | 13 +++++++ src/APIs/auth/strategies/jwt-check.stategy.ts | 34 +++++++++++++++++++ src/APIs/auth/strategies/jwt.strategy.ts | 2 +- src/APIs/posts/posts.controller.ts | 9 +++-- src/APIs/posts/posts.module.ts | 3 +- src/APIs/posts/posts.repository.ts | 2 -- src/APIs/posts/posts.service.ts | 4 --- src/app.module.ts | 22 ++++++++++-- src/commons/guards/auth.guard.ts | 17 ++++++++++ .../middlewares/auth-token.middleware.ts | 32 +++++++++++++++++ 10 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 src/APIs/auth/strategies/jwt-check.stategy.ts create mode 100644 src/commons/guards/auth.guard.ts create mode 100644 src/commons/middlewares/auth-token.middleware.ts diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 942281b..9d8c9bc 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -76,6 +76,19 @@ export class AuthController { throw new UnauthorizedException(e.message); } } + + @ApiOperation({ + summary: '로그아웃(clear cookie)', + description: '클라이언트의 로그인 관련 쿠키를 초기화한다.', + }) + @Get('logout') + @HttpCode(204) + async logout(@Res() res: Response) { + res.clearCookie('accessToken'); + res.clearCookie('refreshToken'); + res.clearCookie('isLoggedIn'); + return res.send(); + } } //https://velog.io/@leemhoon00/Nestjs-JWT-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84 diff --git a/src/APIs/auth/strategies/jwt-check.stategy.ts b/src/APIs/auth/strategies/jwt-check.stategy.ts new file mode 100644 index 0000000..d70e202 --- /dev/null +++ b/src/APIs/auth/strategies/jwt-check.stategy.ts @@ -0,0 +1,34 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtCheckStrategy extends PassportStrategy(Strategy, 'jwt-check') { + // controller에 요청이 왔을 때 constructor가 실행 + constructor(private readonly configService: ConfigService) { + super({ + // accessToken 위치 + jwtFromRequest: ExtractJwt.fromExtractors([ + (request) => { + try { + // const accessToken = request.cookies?.accessToken; + // console.log(accessToken); + return null; + } catch (e) { + // throw new UnauthorizedException(e.message); + } + }, + ]), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + failWithError: false, + }); + } + + async validate(payload) { + return null; + return { message: 'Authentication failed' }; + return { userId: payload.userId }; + } +} diff --git a/src/APIs/auth/strategies/jwt.strategy.ts b/src/APIs/auth/strategies/jwt.strategy.ts index 8ffc855..3e923c3 100644 --- a/src/APIs/auth/strategies/jwt.strategy.ts +++ b/src/APIs/auth/strategies/jwt.strategy.ts @@ -4,7 +4,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { // controller에 요청이 왔을 때 constructor가 실행 constructor(private readonly configService: ConfigService) { super({ diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index d76058b..dcd00e2 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -33,6 +33,7 @@ import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('게시글 API') @Controller('posts') @@ -48,7 +49,7 @@ export class PostsController { @Post('temp') @ApiCookieAuth() @ApiCreatedResponse({ description: '임시등록 성공', type: PublishPostDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @HttpCode(201) async updatePost(@Req() req: Request, @Body() body: CreatePostInput) { const kakaoId = req.user.userId; @@ -187,18 +188,16 @@ export class PostsController { return await this.postsService.fetchDetail({ id }); } - @Get('unauth/user/kakaoId') - async fetchUnauthUserPosts(@Param('kakaoId') targetKakaoId: number) {} - @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) @Get('auth/user/:kakaoId') async fetchAuthUserPosts( @Param('kakaoId') targetKakaoId: number, @Req() req: Request, @Query() query: FetchUserPostsInput, ) { + console.log(req.user); const kakaoId = req.user.userId; + console.log(kakaoId); return await this.postsService.fetchUserPosts({ kakaoId, targetKakaoId, diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 1e12ca6..894229e 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -13,6 +13,7 @@ import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; import { CommentsModule } from '../comments/comments.module'; +import { JwtCheckStrategy } from '../auth/strategies/jwt-check.stategy'; @Module({ imports: [ @@ -28,7 +29,7 @@ import { CommentsModule } from '../comments/comments.module'; StickerBlocksModule, CommentsModule, ], - providers: [JwtStrategy, PostsService, PostsRepository], + providers: [JwtStrategy, JwtCheckStrategy, PostsService, PostsRepository], controllers: [PostsController], exports: [PostsService], }) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index a0d4f0e..1e318b4 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -2,7 +2,6 @@ import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; -import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -100,7 +99,6 @@ export class PostsRepository extends Repository { } async fetchUserPosts({ scope, userKakaoId, postCategoryName }) { - console.log(postCategoryName); const query = this.createQueryBuilder('p') .innerJoin('p.user', 'user') .innerJoinAndSelect('p.postBackground', 'postBackground') diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 178a35c..25d695f 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -41,7 +41,6 @@ export class PostsService { } async getScope({ from_user, to_user }) { - console.log(from_user, to_user); if (from_user === to_user) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; const neighbor = await this.neighborsRepository.findOne({ @@ -78,7 +77,6 @@ export class PostsService { } async fkValidCheck({ posts, passNonEssentail }) { - console.log(posts); const pc = await this.dataSource .getRepository(PostCategory) .createQueryBuilder('pc') @@ -129,7 +127,6 @@ export class PostsService { posts: post, passNonEssentail: !createPostDto.isPublished, }); - console.log(post); // queryRunner 안에서는 커스텀 레포 메서드 사용 불가능. 직접 짤 것. const data = await queryRunner.manager .createQueryBuilder() @@ -180,7 +177,6 @@ export class PostsService { page, ); - console.log(postsAndCounts); return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); } diff --git a/src/app.module.ts b/src/app.module.ts index a8e57d7..e5b674f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,11 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsModule } from './APIs/comments/comments.module'; import { PostsModule } from './APIs/posts/posts.module'; import { UsersModule } from './APIs/users/users.module'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; import { NeighborsModule } from './APIs/neighbors/neighbors.module'; import { PostBackgroundsModule } from './APIs/postBackgrounds/postBackgrounds.module'; @@ -17,6 +17,8 @@ import { StickerBlocksModule } from './APIs/stickerBlocks/stickerBlocks.module'; import { NotificationsModule } from './APIs/notifications/notifications.module'; import { AnnouncementsModule } from './APIs/announcements/announcements.module'; import { ReportsModule } from './APIs/reports/reports.module'; +import { AuthTokenMiddleware } from './commons/middlewares/auth-token.middleware'; +import { JwtModule, JwtService } from '@nestjs/jwt'; @Module({ imports: [ @@ -34,6 +36,14 @@ import { ReportsModule } from './APIs/reports/reports.module'; PostBackgroundsModule, PostCategoriesModule, ReportsModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') }, + }), + inject: [ConfigService], + }), ConfigModule.forRoot({ isGlobal: true, }), @@ -53,4 +63,10 @@ import { ReportsModule } from './APIs/reports/reports.module'; controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer + .apply(AuthTokenMiddleware) + .forRoutes({ path: '*', method: RequestMethod.ALL }); + } +} diff --git a/src/commons/guards/auth.guard.ts b/src/commons/guards/auth.guard.ts new file mode 100644 index 0000000..316ec18 --- /dev/null +++ b/src/commons/guards/auth.guard.ts @@ -0,0 +1,17 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class AuthGuardV2 implements CanActivate { + constructor(private readonly reflector: Reflector) {} + public canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + if (!req.user.userId) throw new UnauthorizedException('권한이 없습니다'); + return true; + } +} diff --git a/src/commons/middlewares/auth-token.middleware.ts b/src/commons/middlewares/auth-token.middleware.ts new file mode 100644 index 0000000..2f629f8 --- /dev/null +++ b/src/commons/middlewares/auth-token.middleware.ts @@ -0,0 +1,32 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class AuthTokenMiddleware implements NestMiddleware { + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + // token을 decode후 req.user에 붙여서 넘어갑니다. + // 만약 토큰이 없거나 유효하지 않으면 req.user에는 null 값이 들어갑니다. + public async use(req: Request, res: Response, next: () => void) { + const userId = this.verifyUser(req); + req.user = { userId }; + return next(); + } + + private verifyUser(req: Request): Promise { + let user = null; + try { + const accessToken = req.cookies.accessToken; + const decoded = this.jwtService.verify(accessToken, { + secret: this.configService.get('JWT_SECRET'), + }); + user = decoded.userId; + } catch (e) {} + + return user; + } +} From 69df25bddef452b936cd7de7f2cdf3c2aad10b32 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 15 Apr 2024 10:53:45 +0900 Subject: [PATCH 022/236] docs: correct post&comment response dto --- src/APIs/auth/strategies/jwt-check.stategy.ts | 34 ------------ src/APIs/comments/comments.repository.ts | 3 +- src/APIs/comments/comments.service.ts | 3 +- src/APIs/comments/dtos/fetch-comments.dto.ts | 36 +++++++++++++ src/APIs/comments/entities/comment.entity.ts | 14 +++++ src/APIs/posts/dtos/fetch-post-detail.dto.ts | 11 ++++ src/APIs/posts/dtos/post-response.dto.ts | 38 +++---------- src/APIs/posts/entities/posts.entity.ts | 54 +++++++++---------- src/APIs/posts/posts.controller.ts | 18 +++++-- src/APIs/posts/posts.module.ts | 3 +- src/APIs/posts/posts.repository.ts | 9 +++- src/APIs/posts/posts.service.ts | 3 +- src/APIs/users/dtos/user-response.dto.ts | 9 +++- 13 files changed, 128 insertions(+), 107 deletions(-) delete mode 100644 src/APIs/auth/strategies/jwt-check.stategy.ts create mode 100644 src/APIs/comments/dtos/fetch-comments.dto.ts create mode 100644 src/APIs/posts/dtos/fetch-post-detail.dto.ts diff --git a/src/APIs/auth/strategies/jwt-check.stategy.ts b/src/APIs/auth/strategies/jwt-check.stategy.ts deleted file mode 100644 index d70e202..0000000 --- a/src/APIs/auth/strategies/jwt-check.stategy.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class JwtCheckStrategy extends PassportStrategy(Strategy, 'jwt-check') { - // controller에 요청이 왔을 때 constructor가 실행 - constructor(private readonly configService: ConfigService) { - super({ - // accessToken 위치 - jwtFromRequest: ExtractJwt.fromExtractors([ - (request) => { - try { - // const accessToken = request.cookies?.accessToken; - // console.log(accessToken); - return null; - } catch (e) { - // throw new UnauthorizedException(e.message); - } - }, - ]), - ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), - failWithError: false, - }); - } - - async validate(payload) { - return null; - return { message: 'Authentication failed' }; - return { userId: payload.userId }; - } -} diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 4355628..dae13ef 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -1,6 +1,7 @@ import { DataSource, Repository } from 'typeorm'; import { Comment } from './entities/comment.entity'; import { Injectable } from '@nestjs/common'; +import { FetchCommentsDto } from './dtos/fetch-comments.dto'; @Injectable() export class CommentsRepository extends Repository { @@ -17,7 +18,7 @@ export class CommentsRepository extends Repository { .execute(); } - async fetchComments({ postsId }) { + async fetchComments({ postsId }): Promise { return await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 7ec4999..82bbf48 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -8,6 +8,7 @@ import { UsersService } from '../users/users.service'; import { CommentsRepository } from './comments.repository'; import { DataSource } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; +import { FetchCommentsDto } from './dtos/fetch-comments.dto'; @Injectable() export class CommentsService { @@ -48,7 +49,7 @@ export class CommentsService { return await this.commentsRepository.upsertComment({ createCommentDto }); } - async fetchComments({ postsId }) { + async fetchComments({ postsId }): Promise { return await this.commentsRepository.fetchComments({ postsId }); } diff --git a/src/APIs/comments/dtos/fetch-comments.dto.ts b/src/APIs/comments/dtos/fetch-comments.dto.ts new file mode 100644 index 0000000..e4500cd --- /dev/null +++ b/src/APIs/comments/dtos/fetch-comments.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { Comment } from '../entities/comment.entity'; + +export class ChildrenComment extends PickType(Comment, [ + 'id', + 'userKakaoId', + 'content', + 'date_created', + 'date_updated', + 'date_deleted', + 'blame_count', + 'parentId', + 'postsId', +]) { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; +} + +export class FetchCommentsDto extends PickType(Comment, [ + 'id', + 'userKakaoId', + 'content', + 'date_created', + 'date_updated', + 'date_deleted', + 'blame_count', + 'parentId', + 'postsId', +]) { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; + + @ApiProperty({ description: '자식 댓글 배열', type: [ChildrenComment] }) + children: ChildrenComment[]; +} diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 675c1b1..2f958ea 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { @@ -15,21 +16,26 @@ import { @Entity() export class Comment { + @ApiProperty({ type: Number, description: '댓글 id' }) @PrimaryGeneratedColumn() id: number; + @ApiProperty({ type: Number, description: '작성자 유저 아이디' }) @Column() @RelationId((comment: Comment) => comment.user) userKakaoId: number; + @ApiProperty({ type: User, description: '사용자 정보' }) @ManyToOne(() => User, (users) => users.kakaoId, { nullable: false }) @JoinColumn() user: User; + @ApiProperty({ type: Number, description: '게시글 id' }) @Column() @RelationId((comment: Comment) => comment.posts) postsId: number; + @ApiProperty({ type: Posts, description: '게시글 정보' }) @ManyToOne(() => Posts, (posts) => posts.id, { nullable: false, onUpdate: 'NO ACTION', @@ -38,12 +44,15 @@ export class Comment { @JoinColumn() posts: Posts; + @ApiProperty({ type: String, description: '내용 정보' }) @Column({ length: 1500 }) content: string; + @ApiProperty({ type: Number, description: '신고 당한 횟수' }) @Column({ default: 0 }) blame_count: number; + @ApiProperty({ type: Comment, description: '루트 댓글 정보' }) @ManyToOne(() => Comment, (comment) => comment.children, { nullable: true, onUpdate: 'NO ACTION', @@ -52,19 +61,24 @@ export class Comment { @JoinColumn() parent: Comment; + @ApiProperty({ type: Number, description: '루트 댓글 아이디' }) @Column({ nullable: true }) @RelationId((comment: Comment) => comment.parent) parentId: Comment; + @ApiProperty({ type: [Comment], description: '자식 댓글 정보' }) @OneToMany(() => Comment, (comment) => comment.parent) children: Comment[]; + @ApiProperty({ type: Date, description: '생성 날짜' }) @CreateDateColumn() date_created: Date; + @ApiProperty({ type: Date, description: '수정 날짜' }) @UpdateDateColumn() date_updated: Date; + @ApiProperty({ type: Date, description: '논리 삭제 칼럼' }) @DeleteDateColumn() date_deleted: Date; } diff --git a/src/APIs/posts/dtos/fetch-post-detail.dto.ts b/src/APIs/posts/dtos/fetch-post-detail.dto.ts new file mode 100644 index 0000000..dfa6234 --- /dev/null +++ b/src/APIs/posts/dtos/fetch-post-detail.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PostResponseDto } from './post-response.dto'; +import { FetchCommentsDto } from 'src/APIs/comments/dtos/fetch-comments.dto'; + +export class fetchPostDetailDto { + @ApiProperty({ type: PostResponseDto, description: '게시글 정보' }) + post: PostResponseDto; + + @ApiProperty({ type: [FetchCommentsDto], description: '댓글 정보' }) + comments: FetchCommentsDto[]; +} diff --git a/src/APIs/posts/dtos/post-response.dto.ts b/src/APIs/posts/dtos/post-response.dto.ts index 0a0124e..3266acc 100644 --- a/src/APIs/posts/dtos/post-response.dto.ts +++ b/src/APIs/posts/dtos/post-response.dto.ts @@ -1,35 +1,9 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; -import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; -import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; -export class PostResponseDto { - @ApiProperty({ description: '임시저장 된 포스트의 아이디', type: Number }) - id: number; +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { Posts } from '../entities/posts.entity'; - @ApiProperty({ description: '작성자의 정보', type: UserResponseDto }) - user: UserResponseDto; - - @ApiProperty({ description: '지정할 카테고리의 아이디', type: PostCategory }) - postCategory: PostCategory; - - @ApiProperty({ description: '지정할 내지의 아이디', type: PostBackground }) - postBackground: PostBackground; - - @ApiProperty({ description: '제목 설정(최대 100자)', type: String }) - title: string; - - @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) - allow_comment: boolean; - - @ApiProperty({ - description: - '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', - type: 'enum', - enum: OpenScope, - }) - @IsEnum(Object.values(OpenScope)) - scope: OpenScope; +export class PostResponseDto extends OmitType(Posts, ['user']) { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; } diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index 6886ce6..eaceb2c 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -23,33 +23,6 @@ export class Posts { @PrimaryGeneratedColumn() id: number; - @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) - @ManyToOne(() => PostCategory, { - nullable: true, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - @JoinColumn() - postCategory: PostCategory; - - @ApiProperty({ description: '연결된 내지', type: PostBackground }) - @JoinColumn() - @ManyToOne(() => PostBackground, { - nullable: false, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - postBackground: PostBackground; - - @ApiProperty({ description: '작성자', type: User }) - @JoinColumn() - @ManyToOne(() => User, { - nullable: false, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - user: User; - @IsString() @ApiProperty({ description: '연결된 카테고리 fk', type: String }) @Column({ nullable: true }) @@ -123,4 +96,31 @@ export class Posts { @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) @Column() main_image_url: string; + + @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) + @ManyToOne(() => PostCategory, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + @JoinColumn() + postCategory: PostCategory; + + @ApiProperty({ description: '연결된 내지', type: PostBackground }) + @JoinColumn() + @ManyToOne(() => PostBackground, { + nullable: false, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + postBackground: PostBackground; + + @ApiProperty({ description: '작성자', type: User }) + @JoinColumn() + @ManyToOne(() => User, { + nullable: false, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + user: User; } diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index dcd00e2..742050a 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -18,6 +18,7 @@ import { ApiConsumes, ApiCookieAuth, ApiCreatedResponse, + ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; @@ -34,6 +35,8 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { PostResponseDto } from './dtos/post-response.dto'; +import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; @ApiTags('게시글 API') @Controller('posts') @@ -184,20 +187,25 @@ export class PostsController { description: 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다.', }) @Get('detail/:id') + @ApiOkResponse({ type: fetchPostDetailDto }) async fetchPostDetail(@Param('id') id: number) { return await this.postsService.fetchDetail({ id }); } - @ApiCookieAuth() - @Get('auth/user/:kakaoId') - async fetchAuthUserPosts( + @ApiOperation({ + summary: '특정 유저의 게시글 조회', + description: + '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', + }) + @Get('/user/:kakaoId') + @ApiOkResponse({ type: [PostResponseDto] }) + async fetchUserPosts( @Param('kakaoId') targetKakaoId: number, @Req() req: Request, @Query() query: FetchUserPostsInput, - ) { + ): Promise { console.log(req.user); const kakaoId = req.user.userId; - console.log(kakaoId); return await this.postsService.fetchUserPosts({ kakaoId, targetKakaoId, diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 894229e..1e12ca6 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -13,7 +13,6 @@ import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; import { CommentsModule } from '../comments/comments.module'; -import { JwtCheckStrategy } from '../auth/strategies/jwt-check.stategy'; @Module({ imports: [ @@ -29,7 +28,7 @@ import { JwtCheckStrategy } from '../auth/strategies/jwt-check.stategy'; StickerBlocksModule, CommentsModule, ], - providers: [JwtStrategy, JwtCheckStrategy, PostsService, PostsRepository], + providers: [JwtStrategy, PostsService, PostsRepository], controllers: [PostsController], exports: [PostsService], }) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 1e318b4..11e7f50 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -2,6 +2,7 @@ import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { PostResponseDto } from './dtos/post-response.dto'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -37,7 +38,7 @@ export class PostsRepository extends Repository { .getManyAndCount(); } - async fetchPostDetail(id) { + async fetchPostDetail(id): Promise { await this.update(id, { view_count: () => 'view_count +1', }); @@ -98,7 +99,11 @@ export class PostsRepository extends Repository { .getMany(); } - async fetchUserPosts({ scope, userKakaoId, postCategoryName }) { + async fetchUserPosts({ + scope, + userKakaoId, + postCategoryName, + }): Promise { const query = this.createQueryBuilder('p') .innerJoin('p.user', 'user') .innerJoinAndSelect('p.postBackground', 'postBackground') diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 25d695f..a8c756f 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -23,6 +23,7 @@ import { PostsRepository } from './posts.repository'; import { CommentsService } from '../comments/comments.service'; import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { PostResponseDto } from './dtos/post-response.dto'; @Injectable() export class PostsService { @@ -204,7 +205,7 @@ export class PostsService { kakaoId, targetKakaoId, postCategoryName, - }: FetchUserPostsDto) { + }: FetchUserPostsDto): Promise { const scope = await this.getScope({ from_user: targetKakaoId, to_user: kakaoId, diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index 47a56c2..fde80df 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -1,4 +1,4 @@ -import { OmitType } from '@nestjs/swagger'; +import { OmitType, PickType } from '@nestjs/swagger'; import { User } from '../entities/user.entity'; export const USER_SELECT_OPTION = { @@ -11,7 +11,12 @@ export const USER_SELECT_OPTION = { date_created: true, date_deleted: true, }; - +export class UserPrimaryResponseDto extends PickType(User, [ + 'kakaoId', + 'username', + 'profile_image', + 'description', +]) {} export class UserResponseDto extends OmitType(User, ['current_refresh_token']) { // @ApiProperty({ description: '카카오 id', type: Number }) // kakaoId: number; From 51aed1ce08ced6b41b96915f9c7bc566c70b9030 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 15 Apr 2024 11:08:39 +0900 Subject: [PATCH 023/236] feat: change whole apis guard to AuthGuardV2 --- src/APIs/comments/comments.controller.ts | 6 +++--- src/APIs/likes/likes.controller.ts | 4 ++-- src/APIs/likes/likes.module.ts | 3 +-- src/APIs/neighbors/neighbors.controller.ts | 6 +++--- src/APIs/neighbors/neighbors.module.ts | 3 +-- .../postCategories/PostCategories.controller.ts | 8 ++++---- src/APIs/postCategories/PostCategories.module.ts | 3 +-- src/APIs/posts/posts.controller.ts | 13 ++++++------- src/APIs/posts/posts.module.ts | 2 +- src/APIs/stickerBlocks/stickerBlocks.module.ts | 3 +-- .../stickerCategories.controller.ts | 6 +++--- .../stickerCategories.module.ts | 2 +- src/APIs/stickers/stickers.controller.ts | 16 ++++++++-------- src/APIs/stickers/stickers.module.ts | 3 +-- src/APIs/users/users.controller.ts | 10 +++++----- src/APIs/users/users.module.ts | 3 +-- 16 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 037d8af..6d543b6 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -1,10 +1,10 @@ import { Body, Controller, Delete, Post, Req, UseGuards } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; import { CommentsService } from './comments.service'; import { CreateCommentInput } from './dtos/create-comment.dto'; import { Request } from 'express'; import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { DeleteCommentDto } from './dtos/delete-comment.dto'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('댓글 API') @Controller('comments') @@ -17,7 +17,7 @@ export class CommentsController { }) @ApiCookieAuth() @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) async upsertComment(@Req() req: Request, @Body() body: CreateCommentInput) { const userKakaoId = req.user.userId; return await this.commentsService.upsert({ ...body, userKakaoId }); @@ -29,7 +29,7 @@ export class CommentsController { }) @ApiCookieAuth() @Delete() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) async deleteComment(@Req() req: Request, @Body() body: DeleteCommentDto) { const userKakaoId = req.user.userId; return await this.commentsService.delete({ ...body, userKakaoId }); diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index 152587c..cb51ccd 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -17,11 +17,11 @@ import { ApiTags, } from '@nestjs/swagger'; import { ToggleLikeDto } from './dtos/toggle-like.dto'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('좋아요 API') @Controller('likes') @@ -35,7 +35,7 @@ export class LikesController { @ApiCookieAuth() @ApiOkResponse({ description: '토글 성공', type: ToggleLikeResponseDto }) @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @HttpCode(200) @Post() async toggleLike( diff --git a/src/APIs/likes/likes.module.ts b/src/APIs/likes/likes.module.ts index 01960f6..966089d 100644 --- a/src/APIs/likes/likes.module.ts +++ b/src/APIs/likes/likes.module.ts @@ -1,14 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Posts } from '../posts/entities/posts.entity'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { LikesController } from './likes.controller'; import { LikesService } from './likes.service'; import { Likes } from './entities/like.entity'; @Module({ imports: [TypeOrmModule.forFeature([Posts, Likes])], - providers: [JwtStrategy, LikesService], + providers: [LikesService], controllers: [LikesController], }) export class LikesModule {} diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/neighbors/neighbors.controller.ts index 544753d..4e49643 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/neighbors/neighbors.controller.ts @@ -8,7 +8,6 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { NeighborsService } from './neighbors.service'; import { @@ -24,6 +23,7 @@ import { FollowDto } from './dtos/follow.dto'; import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('이웃 API') @Controller('neighbors') @@ -37,7 +37,7 @@ export class NeighborsController { @ApiCookieAuth() @ApiCreatedResponse({ description: '이웃 추가 성공', type: FollowUserDto }) @ApiConflictResponse({ description: '이미 팔로우한 상태이다.' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Post('follow') @HttpCode(201) async followUser( @@ -59,7 +59,7 @@ export class NeighborsController { @ApiCookieAuth() @ApiOkResponse({ description: '언팔로우 성공' }) @ApiNotFoundResponse({ description: '존재하지 않는 이웃 정보이다.' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Post('unfollow') @HttpCode(200) unfollowUser(@Body() body: FollowDto, @Req() req: Request) { diff --git a/src/APIs/neighbors/neighbors.module.ts b/src/APIs/neighbors/neighbors.module.ts index cc85cff..f47d656 100644 --- a/src/APIs/neighbors/neighbors.module.ts +++ b/src/APIs/neighbors/neighbors.module.ts @@ -3,13 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Neighbor } from './entities/neighbor.entity'; import { NeighborsService } from './neighbors.service'; import { NeighborsController } from './neighbors.controller'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { UsersModule } from '../users/users.module'; import { User } from '../users/entities/user.entity'; @Module({ imports: [UsersModule, TypeOrmModule.forFeature([Neighbor, User])], - providers: [NeighborsService, JwtStrategy], + providers: [NeighborsService], controllers: [NeighborsController], }) export class NeighborsModule {} diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 96dcfb7..eb37e91 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -17,11 +17,11 @@ import { ApiTags, } from '@nestjs/swagger'; import { PostCategoriesService } from './PostCategories.service'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { PostCategory } from './entities/postCategory.entity'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('카테고리 API') @Controller('postcg') @@ -37,7 +37,7 @@ export class PostCategoriesController { description: '카테고리 생성 완료', type: CreatePostCategoryResponseDto, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Post() @HttpCode(201) async createPostCategory( @@ -58,7 +58,7 @@ export class PostCategoriesController { description: '', type: [PostCategory], }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Get() @HttpCode(200) async fetchPostCategories(@Req() req: Request): Promise { @@ -73,7 +73,7 @@ export class PostCategoriesController { }) @ApiCookieAuth() @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) async deletePostCategory(@Req() req: Request, @Param('id') id: string) { const kakaoId = req.user.userId; return await this.postCategoriesService.delete({ kakaoId, id }); diff --git a/src/APIs/postCategories/PostCategories.module.ts b/src/APIs/postCategories/PostCategories.module.ts index c4d1780..6cd50aa 100644 --- a/src/APIs/postCategories/PostCategories.module.ts +++ b/src/APIs/postCategories/PostCategories.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { PostCategory } from './entities/postCategory.entity'; import { PostCategoriesService } from './PostCategories.service'; import { PostCategoriesController } from './PostCategories.controller'; @Module({ imports: [TypeOrmModule.forFeature([PostCategory])], - providers: [JwtStrategy, PostCategoriesService], + providers: [PostCategoriesService], controllers: [PostCategoriesController], }) export class PostCategoriesModule {} diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 742050a..6b6c27a 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -12,7 +12,6 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; import { ApiBody, ApiConsumes, @@ -69,7 +68,7 @@ export class PostsController { @Post() @ApiCookieAuth() @ApiCreatedResponse({ description: '등록 성공', type: PublishPostDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @HttpCode(201) async publishPost(@Req() req: Request, @Body() body: PublishPostInput) { const kakaoId = req.user.userId; @@ -94,7 +93,7 @@ export class PostsController { description: '로그인된 유저의 임시작성 게시글을 조회한다.', }) @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Get('temp') async fetchTempPosts(@Req() req: Request): Promise { const kakaoId = req.user.userId; @@ -115,7 +114,7 @@ export class PostsController { description: '이미지 서버에 파일 업로드 완료', type: ImageUploadResponseDto, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @Post('image') @UseInterceptors(FileInterceptor('file')) @@ -133,7 +132,7 @@ export class PostsController { '친구의 게시글을 조회한다. Query를 통해 페이지네이션 가능. default) pageNo: 1, pageSize: 10', }) @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @HttpCode(200) @ApiCookieAuth() @Get('friends') @@ -151,7 +150,7 @@ export class PostsController { '로그인 된 유저의 {id}에 해당하는 게시글을 논리삭제한다. 발행된 게시글에 사용을 권장', }) @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Delete('soft/:id') async softDelete(@Req() req: Request, @Param('id') id: number) { const kakaoId = req.user.userId; @@ -164,7 +163,7 @@ export class PostsController { '로그인 된 유저의 {id}에 해당하는 게시글을 물리삭제한다. 임시 저장된 게시글에 사용을 권장', }) @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Delete('hard/:id') async hardDelete(@Req() req: Request, @Param('id') id: number) { const kakaoId = req.user.userId; diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 1e12ca6..1620cfa 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -28,7 +28,7 @@ import { CommentsModule } from '../comments/comments.module'; StickerBlocksModule, CommentsModule, ], - providers: [JwtStrategy, PostsService, PostsRepository], + providers: [PostsService, PostsRepository], controllers: [PostsController], exports: [PostsService], }) diff --git a/src/APIs/stickerBlocks/stickerBlocks.module.ts b/src/APIs/stickerBlocks/stickerBlocks.module.ts index 809c403..3a48902 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.module.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { StickerBlock } from './entities/stickerblock.entity'; import { StickerBlocksController } from './stickerBlocks.controller'; @@ -9,7 +8,7 @@ import { StickerBlocksService } from './stickerBlocks.service'; @Module({ imports: [TypeOrmModule.forFeature([StickerBlock]), StickersModule], - providers: [JwtStrategy, StickerBlocksService], + providers: [StickerBlocksService], controllers: [StickerBlocksController], exports: [StickerBlocksService], }) diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 970966d..c75b266 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -14,10 +14,10 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { MapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('스티커 카테고리 API') @Controller('stickercg') @@ -32,7 +32,7 @@ export class StickerCategoriesController { }) @ApiOkResponse({ description: '생성 완료', type: StickerCategory }) @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Post('create/:name') async createCategory(@Req() req: Request, @Param('name') name: string) { const kakaoId = req.user.userId; @@ -47,7 +47,7 @@ export class StickerCategoriesController { description: '[어드민 전용] 스티커에 카테고리를 매핑한다.', }) @ApiCookieAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @Post('map') async mapCategory( @Req() req: Request, diff --git a/src/APIs/stickerCategories/stickerCategories.module.ts b/src/APIs/stickerCategories/stickerCategories.module.ts index 7a8c687..95ce614 100644 --- a/src/APIs/stickerCategories/stickerCategories.module.ts +++ b/src/APIs/stickerCategories/stickerCategories.module.ts @@ -14,7 +14,7 @@ import { StickersModule } from '../stickers/stickers.module'; UsersModule, StickersModule, ], - providers: [JwtStrategy, StickerCategoriesService], + providers: [StickerCategoriesService], controllers: [StickerCategoriesController], }) export class StickerCategoriesModule {} diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 3c69e44..82cb238 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -25,13 +25,13 @@ import { } from '@nestjs/swagger'; import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; import { RemoveBgDto } from './dtos/remove-bg.dto'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { FindStickerInput } from './dtos/find-sticker.dto'; import { UpdateStickerInput } from './dtos/update-sticker.dto'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('스티커 API') @Controller('stickers') @@ -51,7 +51,7 @@ export class StickersController { description: '이미지 서버에 파일 업로드 완료', type: Sticker, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @Post('private') @UseInterceptors(FileInterceptor('file')) @@ -82,7 +82,7 @@ export class StickersController { type: Sticker, }) @ApiUnauthorizedResponse({ description: '어드민이 아님' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @Post('public') @UseInterceptors(FileInterceptor('file')) @@ -105,7 +105,7 @@ export class StickersController { '본인이 만든 재사용 가능한 스티커들을 fetch한다. toggle이 우선적으로 이루어져야함.', }) @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @HttpCode(200) async fetchPrivateStickers(@Req() req: Request): Promise { @@ -119,7 +119,7 @@ export class StickersController { '본인이 만든 스티커의 재사용 여부를 토글한다. 보관함 저장 혹은 삭제 용도로 사용할 것', }) @Post('toggle/:id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @HttpCode(200) async toggleReusable(@Req() req: Request, @Param('id') id: number) { @@ -144,7 +144,7 @@ export class StickersController { workflow: post('background') => delete('s3') => patch('image') `, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @Post('background') async removeBg(@Body() body: RemoveBgDto): Promise { @@ -157,7 +157,7 @@ export class StickersController { description: '스티커 객체의 이미지 url을 변경한다. 호출 이전에 기존의 이미지 제거를 권장.', }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() async updateSticker( @Req() req: Request, @@ -177,7 +177,7 @@ export class StickersController { 로직 순서: delete('s3') => patch('image')
**만약 사용중인 객체의 이미지만 제거 할 경우 이미지가 깨진다.**`, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @Delete('s3') @HttpCode(200) diff --git a/src/APIs/stickers/stickers.module.ts b/src/APIs/stickers/stickers.module.ts index 5f7c845..1e71155 100644 --- a/src/APIs/stickers/stickers.module.ts +++ b/src/APIs/stickers/stickers.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { Sticker } from './entities/sticker.entity'; import { StickersController } from './stickers.controller'; import { StickersService } from './stickers.service'; @@ -10,7 +9,7 @@ import { UsersModule } from '../users/users.module'; @Module({ imports: [TypeOrmModule.forFeature([Sticker]), UsersModule], - providers: [JwtStrategy, StickersService, AwsService, UtilsService], + providers: [StickersService, AwsService, UtilsService], controllers: [StickersController], exports: [StickersService], }) diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 61c2a25..fe5d07d 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -21,13 +21,13 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { UserResponseDto } from './dtos/user-response.dto'; import { PatchUserInput } from './dtos/patch-user.input'; import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; @ApiTags('유저 API') @Controller('users') @@ -52,7 +52,7 @@ export class UsersController { @ApiCookieAuth() @ApiOkResponse({ description: '불러오기 완료', type: UserResponseDto }) @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @HttpCode(200) async fetchUser(@Req() req: Request): Promise { const kakaoId = req.user.userId; @@ -67,7 +67,7 @@ export class UsersController { @ApiCookieAuth() @Patch() @HttpCode(200) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) async patchUser( @Req() req: Request, @Body() body: PatchUserInput, @@ -95,7 +95,7 @@ export class UsersController { description: '업로드 성공', type: ImageUploadResponseDto, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @UseInterceptors(FileInterceptor('file')) @HttpCode(201) @@ -124,7 +124,7 @@ export class UsersController { description: '업로드 성공', type: ImageUploadResponseDto, }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuardV2) @ApiCookieAuth() @UseInterceptors(FileInterceptor('file')) @HttpCode(201) diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index b2eea54..102800f 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -3,13 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; @Module({ imports: [TypeOrmModule.forFeature([User])], - providers: [UsersService, JwtStrategy, AwsService, UtilsService], + providers: [UsersService, AwsService, UtilsService], controllers: [UsersController], exports: [UsersService], }) From be45214530eb3421a48fba2e36690f8d522bcf79 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 15 Apr 2024 11:45:56 +0900 Subject: [PATCH 024/236] docs: correct posts api dtos --- .../posts/dtos/fetch-post-for-update.dto.ts | 17 ++++++ src/APIs/posts/dtos/fetch-posts.dto.ts | 11 +--- src/APIs/posts/dtos/publish-post.dto.ts | 56 ------------------- src/APIs/posts/posts.controller.ts | 31 ++++++++-- src/APIs/posts/posts.repository.ts | 25 +++++++-- src/APIs/posts/posts.service.ts | 28 +++++++--- 6 files changed, 81 insertions(+), 87 deletions(-) create mode 100644 src/APIs/posts/dtos/fetch-post-for-update.dto.ts diff --git a/src/APIs/posts/dtos/fetch-post-for-update.dto.ts b/src/APIs/posts/dtos/fetch-post-for-update.dto.ts new file mode 100644 index 0000000..59b4323 --- /dev/null +++ b/src/APIs/posts/dtos/fetch-post-for-update.dto.ts @@ -0,0 +1,17 @@ +import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; +import { PostResponseDto } from './post-response.dto'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; + +export class PostResponseDtoExceptCategory extends OmitType(PostResponseDto, [ + 'postCategory', +]) {} +export class FetchPostForUpdateDto { + @ApiProperty({ + description: '게시글 정보', + type: PostResponseDtoExceptCategory, + }) + post: PostResponseDtoExceptCategory; + + @ApiProperty({ description: '스티커 블록 배열', type: [StickerBlock] }) + stickerBlocks: StickerBlock[]; +} diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index 72fe19c..4dabd4d 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -1,15 +1,6 @@ import { PageRequest } from '../../../utils/pages/page-request'; -export class FetchPostsDto extends PageRequest { - // @ApiProperty({ - // description: - // '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개 // 상위 공개 범위는 함께 fetch 됨', - // type: 'enum', - // enum: OpenScope, - // }) - // @IsEnum(OpenScope) - // scope: OpenScope; -} +export class FetchPostsDto extends PageRequest {} export const FETCH_POST_OPTION = { id: true, diff --git a/src/APIs/posts/dtos/publish-post.dto.ts b/src/APIs/posts/dtos/publish-post.dto.ts index 3daa8b3..203b755 100644 --- a/src/APIs/posts/dtos/publish-post.dto.ts +++ b/src/APIs/posts/dtos/publish-post.dto.ts @@ -6,59 +6,3 @@ export class PublishPostDto extends OmitType(Posts, [ 'user', 'postCategory', ]) {} -// @ApiProperty({ -// description: '배경 id', -// type: PickType(PostBackground, ['id'] as const), -// }) -// postBackground: Partial; -// // postBackground: Pick; - -// @ApiProperty({ -// description: '카테고리 id', -// type: PickType(PostCategory, ['id'] as const), -// }) -// postCategory: Partial; -// // postCategoty: Pick; - -// @ApiProperty({ -// description: '작성자 id', -// type: PickType(User, ['kakaoId'] as const), -// }) -// user: Partial; -// // user: Pick; - -// @ApiProperty({ description: '포스트의 고유 아이디', type: Number }) -// id: number; - -// @ApiProperty({ description: '제목(최대 100자)', type: String }) -// title: string; - -// @ApiProperty({ description: '임시저장(false), 발행(true)', type: Boolean }) -// isPublished: boolean; - -// @ApiProperty({ description: '좋아요 카운트', type: Number }) -// like_count: number; - -// @ApiProperty({ description: '조회수 카운트', type: Number }) -// view_count: number; - -// @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) -// allow_comment: boolean; - -// @ApiProperty({ -// description: -// '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', -// type: 'enum', -// enum: OpenScope, -// }) -// scope: OpenScope; - -// @ApiProperty({ description: '생성된 날짜', type: Date }) -// date_created: Date; - -// @ApiProperty({ description: '수정된 날짜', type: Date }) -// date_updated: Date; - -// @ApiProperty({ description: 'soft delete column', type: Date }) -// date_deleted: Date; -// } diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 6b6c27a..e0a1818 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -36,6 +36,10 @@ import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { PostResponseDto } from './dtos/post-response.dto'; import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; +import { + FetchPostForUpdateDto, + PostResponseDtoExceptCategory, +} from './dtos/fetch-post-for-update.dto'; @ApiTags('게시글 API') @Controller('posts') @@ -93,9 +97,12 @@ export class PostsController { description: '로그인된 유저의 임시작성 게시글을 조회한다.', }) @ApiCookieAuth() + @ApiOkResponse({ type: [PostResponseDtoExceptCategory] }) @UseGuards(AuthGuardV2) @Get('temp') - async fetchTempPosts(@Req() req: Request): Promise { + async fetchTempPosts( + @Req() req: Request, + ): Promise { const kakaoId = req.user.userId; console.log(kakaoId); return await this.postsService.fetchTempPosts({ kakaoId }); @@ -175,20 +182,32 @@ export class PostsController { description: '본인 게시글 수정용으로 id에 해당하는 게시글에 조인된 스티커 블록들의 값과 게시글 세부 데이터를 모두 가져온다.', }) + @ApiCookieAuth() + @ApiOkResponse({ type: FetchPostForUpdateDto }) + @UseGuards(AuthGuardV2) @HttpCode(200) @Get('update/:id') - async fetchPost(@Param('id') id: number) { - return await this.postsService.fetchPostForUpdate({ id }); + async fetchPost( + @Req() req: Request, + @Param('id') id: number, + ): Promise { + const kakaoId = req.user.userId; + return await this.postsService.fetchPostForUpdate({ id, kakaoId }); } @ApiOperation({ summary: '게시글 디테일 뷰 fetch', - description: 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다.', + description: + 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', }) @Get('detail/:id') @ApiOkResponse({ type: fetchPostDetailDto }) - async fetchPostDetail(@Param('id') id: number) { - return await this.postsService.fetchDetail({ id }); + async fetchPostDetail( + @Param('id') id: number, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.postsService.fetchDetail({ kakaoId, id }); } @ApiOperation({ diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 11e7f50..f7bb60b 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -3,6 +3,7 @@ import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; +import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -38,7 +39,7 @@ export class PostsRepository extends Repository { .getManyAndCount(); } - async fetchPostDetail(id): Promise { + async fetchPostDetail({ id, scope }): Promise { await this.update(id, { view_count: () => 'view_count +1', }); @@ -53,6 +54,7 @@ export class PostsRepository extends Repository { 'user.username', ]) .where('p.id = :id', { id }) + .andWhere('p.scope IN (:scope)', { scope }) .getOne(); } async fetchPostForUpdate(id) { @@ -92,11 +94,22 @@ export class PostsRepository extends Repository { .getManyAndCount(); } - async fetchTempPosts(kakaoId) { - return this.createQueryBuilder('p') - .where('p.userKakaoId = :kakaoId', { kakaoId }) - .andWhere(`p.isPublished = false`) - .getMany(); + async fetchTempPosts(kakaoId): Promise { + return ( + this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .innerJoinAndSelect('p.postBackground', 'postBackground') + // .innerJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.userKakaoId = :kakaoId', { kakaoId }) + .andWhere(`p.isPublished = false`) + .getMany() + ); } async fetchUserPosts({ diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index a8c756f..b822875 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable, NotFoundException, + UnauthorizedException, } from '@nestjs/common'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; @@ -24,6 +25,11 @@ import { CommentsService } from '../comments/comments.service'; import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; +import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; +import { + FetchPostForUpdateDto, + PostResponseDtoExceptCategory, +} from './dtos/fetch-post-for-update.dto'; @Injectable() export class PostsService { @@ -107,10 +113,10 @@ export class PostsService { let post = {}; try { if (createPostDto.id) { - // 이때 락 걸어야 되나? post = await queryRunner.manager.findOne(Posts, { where: { id: createPostDto.id, + user: { kakaoId: createPostDto.userKakaoId }, }, }); if (!post) { @@ -128,7 +134,6 @@ export class PostsService { posts: post, passNonEssentail: !createPostDto.isPublished, }); - // queryRunner 안에서는 커스텀 레포 메서드 사용 불가능. 직접 짤 것. const data = await queryRunner.manager .createQueryBuilder() .insert() @@ -147,23 +152,24 @@ export class PostsService { await queryRunner.release(); } } + async fetchPosts(page: FetchPostsDto): Promise { const postsAndCounts = await this.postsRepository.fetchPosts(page); - return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); } - async fetchPostForUpdate({ id }) { - // 카카오 아이디로 valid check? 공개설정 안된 게시글 fetch 못하게 하자!! + async fetchPostForUpdate({ id, kakaoId }): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: true }); + if (data.userKakaoId !== kakaoId) + throw new UnauthorizedException('본인이 아닙니다.'); const post = await this.postsRepository.fetchPostForUpdate(id); - const stickerBlocks = await this.stickerBlocksService.fetchBlocks({ postsId: id, }); return { post, stickerBlocks }; } + async fetchFriendsPosts({ kakaoId, page, @@ -181,15 +187,19 @@ export class PostsService { return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); } - async fetchTempPosts({ kakaoId }): Promise { + async fetchTempPosts({ kakaoId }): Promise { return await this.postsRepository.fetchTempPosts(kakaoId); } - async fetchDetail({ id }) { + async fetchDetail({ kakaoId, id }): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: false }); + const scope = await this.getScope({ + from_user: data.userKakaoId, + to_user: kakaoId, + }); const comments = await this.commentsService.fetchComments({ postsId: id }); - const post = await this.postsRepository.fetchPostDetail(id); + const post = await this.postsRepository.fetchPostDetail({ id, scope }); return { comments, post }; } From 0f692c64ff7231d8c865df72c5a402221ac4aded Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 10:51:04 +0900 Subject: [PATCH 025/236] feat: check if comments parentId is root --- src/APIs/comments/comments.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 82bbf48..b2533d7 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -22,8 +22,10 @@ export class CommentsService { const parent = await this.existCheck({ id: parentId }); if (parent.postsId != postsId) throw new BadRequestException( - '루트 댓글이 작성된 게시글 아이디와 일치하지 않습니다.', + '게시글 아이디가 루트 댓글이 작성된 게시글 아이디와 일치하지 않습니다.', ); + if (parent.parentId) + throw new BadRequestException('부모 댓글이 루트 댓글이 아닙니다.'); } async existCheck({ id }) { const comment = await this.commentsRepository.findOne({ where: { id } }); From f8d5365c1eb67a9ec740207984c7d3ce39917b7f Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 11:20:46 +0900 Subject: [PATCH 026/236] docs: add response info --- src/APIs/comments/comments.controller.ts | 33 +++++++++++++-- src/APIs/comments/comments.service.ts | 52 ++++++++++++++++-------- src/APIs/users/dtos/user-response.dto.ts | 6 +++ 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 6d543b6..dfb4695 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -1,10 +1,25 @@ -import { Body, Controller, Delete, Post, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + HttpCode, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { CommentsService } from './comments.service'; import { CreateCommentInput } from './dtos/create-comment.dto'; import { Request } from 'express'; -import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiCookieAuth, + ApiNoContentResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; import { DeleteCommentDto } from './dtos/delete-comment.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; @ApiTags('댓글 API') @Controller('comments') @@ -15,10 +30,15 @@ export class CommentsController { summary: '댓글을 작성하거나 수정한다.', description: '댓글을 작성하거나 (optional)id에 해당하는 댓글을 수정한다.', }) + @ApiOkResponse({ type: ChildrenComment }) @ApiCookieAuth() @Post() @UseGuards(AuthGuardV2) - async upsertComment(@Req() req: Request, @Body() body: CreateCommentInput) { + @HttpCode(201) + async upsertComment( + @Req() req: Request, + @Body() body: CreateCommentInput, + ): Promise { const userKakaoId = req.user.userId; return await this.commentsService.upsert({ ...body, userKakaoId }); } @@ -28,9 +48,14 @@ export class CommentsController { description: '댓글을 논리삭제한다. date_deleted 칼럼에 값이 생긴다.', }) @ApiCookieAuth() + @ApiNoContentResponse({ description: '삭제 성공' }) @Delete() @UseGuards(AuthGuardV2) - async deleteComment(@Req() req: Request, @Body() body: DeleteCommentDto) { + @HttpCode(204) + async deleteComment( + @Req() req: Request, + @Body() body: DeleteCommentDto, + ): Promise { const userKakaoId = req.user.userId; return await this.commentsService.delete({ ...body, userKakaoId }); } diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index b2533d7..3d17a47 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -8,7 +8,8 @@ import { UsersService } from '../users/users.service'; import { CommentsRepository } from './comments.repository'; import { DataSource } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; -import { FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { USER_PRIMARY_SELECT_OPTION } from '../users/dtos/user-response.dto'; @Injectable() export class CommentsService { @@ -36,33 +37,50 @@ export class CommentsService { } return comment; } - async upsert(createCommentDto: CreateCommentDto) { - if (createCommentDto.id) { - await this.existCheck({ id: createCommentDto.id }); - } + async upsert(createCommentDto: CreateCommentDto): Promise { if (createCommentDto.parentId) await this.postsIdValidCheck({ parentId: createCommentDto.parentId, postsId: createCommentDto.postsId, }); - await this.dataSource.manager.update(Posts, createCommentDto.postsId, { - comment_count: () => 'comment_count +1', + if (createCommentDto.id) { + await this.existCheck({ id: createCommentDto.id }); + } else { + // id를 입력하지 않았을 경우(생성의 경우)에만 count 증가 + await this.dataSource.manager.update(Posts, createCommentDto.postsId, { + comment_count: () => 'comment_count +1', + }); + } + const upsertData = await this.commentsRepository.upsertComment({ + createCommentDto, + }); + const id = upsertData.identifiers[0]; + console.log(id); + return await this.commentsRepository.findOne({ + select: { + user: USER_PRIMARY_SELECT_OPTION, + }, + relations: { user: true }, + where: { ...id }, }); - return await this.commentsRepository.upsertComment({ createCommentDto }); } async fetchComments({ postsId }): Promise { return await this.commentsRepository.fetchComments({ postsId }); } - async delete({ id, userKakaoId }) { - const data = await this.existCheck({ id }); - await this.dataSource.manager.update(Posts, data.postsId, { - comment_count: () => 'comment_count -1', - }); - await this.commentsRepository.softDelete({ - user: { kakaoId: userKakaoId }, - id, - }); + async delete({ id, userKakaoId }): Promise { + try { + const data = await this.existCheck({ id }); + await this.commentsRepository.softDelete({ + user: { kakaoId: userKakaoId }, + id, + }); + await this.dataSource.manager.update(Posts, data.postsId, { + comment_count: () => 'comment_count -1', + }); + } catch (e) { + throw e; + } } } diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index fde80df..1593ce1 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -11,6 +11,12 @@ export const USER_SELECT_OPTION = { date_created: true, date_deleted: true, }; +export const USER_PRIMARY_SELECT_OPTION = { + kakaoId: true, + username: true, + description: true, + profile_image: true, +}; export class UserPrimaryResponseDto extends PickType(User, [ 'kakaoId', 'username', From 6888b03a577bffa7c2d9a6325e9ae1e17ef7e1fc Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 11:25:15 +0900 Subject: [PATCH 027/236] fix: delete unused dto --- src/APIs/comments/comments.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index dfb4695..69d5230 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -19,7 +19,7 @@ import { } from '@nestjs/swagger'; import { DeleteCommentDto } from './dtos/delete-comment.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { ChildrenComment } from './dtos/fetch-comments.dto'; @ApiTags('댓글 API') @Controller('comments') From ddf11aadd90a10a23173319a1444b46182e42fa0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 11:26:58 +0900 Subject: [PATCH 028/236] fix: patch user response dto --- src/APIs/users/users.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index fe5d07d..26e137b 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -63,7 +63,7 @@ export class UsersController { summary: '로그인된 유저의 이름이나 설명을 변경', description: '로그인된 유저의 이름이나 설명, 혹은 둘 다를 변경한다.', }) - @ApiOkResponse({ description: '변경 성공', type: PatchUserInput }) + @ApiOkResponse({ description: '변경 성공', type: UserResponseDto }) @ApiCookieAuth() @Patch() @HttpCode(200) From 62a64a2b0965be7e7e51cca0c7bc4274fe125a5d Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 11:27:56 +0900 Subject: [PATCH 029/236] fix: patch user response dto --- src/APIs/comments/comments.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 69d5230..f215c75 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -34,7 +34,7 @@ export class CommentsController { @ApiCookieAuth() @Post() @UseGuards(AuthGuardV2) - @HttpCode(201) + @HttpCode(200) async upsertComment( @Req() req: Request, @Body() body: CreateCommentInput, From c6707e2ea9e4869f53015048c0d5fdddae6f578d Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Apr 2024 11:28:42 +0900 Subject: [PATCH 030/236] fix: delete unused dto --- src/APIs/posts/posts.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index e0a1818..2057a85 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -25,7 +25,6 @@ import { Request } from 'express'; import { PostsService } from './posts.service'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PublishPostDto } from './dtos/publish-post.dto'; -import { Posts } from './entities/posts.entity'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { CreatePostInput } from './dtos/create-post.input'; import { PublishPostInput } from './dtos/publish-post.input'; From 65b910dfa60f825d81790f9b950766e0361ffb4e Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 17 Apr 2024 11:17:54 +0900 Subject: [PATCH 031/236] feat: add index to date_created column on posts&comment entity --- src/APIs/comments/entities/comment.entity.ts | 4 +- src/APIs/posts/dtos/fetch-posts.dto.ts | 35 +++++++++++++- src/APIs/posts/entities/posts.entity.ts | 4 +- src/APIs/posts/posts.controller.ts | 1 - src/APIs/posts/posts.repository.ts | 48 +++++++++++++------- src/APIs/users/users.controller.ts | 2 +- src/commons/enums/posts-filter-option.ts | 11 +++++ src/commons/enums/posts-order-option.ts | 14 ++++++ 8 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 src/commons/enums/posts-filter-option.ts create mode 100644 src/commons/enums/posts-order-option.ts diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 2f958ea..3af367e 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -6,6 +6,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + Index, JoinColumn, ManyToOne, OneToMany, @@ -70,8 +71,9 @@ export class Comment { @OneToMany(() => Comment, (comment) => comment.parent) children: Comment[]; + @Index() @ApiProperty({ type: Date, description: '생성 날짜' }) - @CreateDateColumn() + @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) date_created: Date; @ApiProperty({ type: Date, description: '수정 날짜' }) diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index 4dabd4d..2adee51 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -1,6 +1,39 @@ +import { IsEnum, IsOptional, IsString } from 'class-validator'; import { PageRequest } from '../../../utils/pages/page-request'; +import { ApiProperty } from '@nestjs/swagger'; +import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; +import { PostsFilterOptionWrap } from 'src/commons/enums/posts-filter-option'; -export class FetchPostsDto extends PageRequest {} +export class FetchPostsDto extends PageRequest { + @ApiProperty({ + description: '페이지 정렬 옵션(default = TIME)', + type: 'enum', + enum: PostsOrderOptionWrap, + required: false, + }) + @IsOptional() + @IsEnum(PostsOrderOptionWrap) + order: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; + + @ApiProperty({ + description: '페이지 검색 옵션(default = TITLE)', + type: 'enum', + enum: PostsFilterOptionWrap, + required: false, + }) + @IsOptional() + @IsEnum(PostsFilterOptionWrap) + filter: PostsFilterOptionWrap = PostsFilterOptionWrap.TITLE; + + @ApiProperty({ + description: '검색할 내용', + type: String, + required: false, + }) + @IsOptional() + @IsString() + search: string = '%'; +} export const FETCH_POST_OPTION = { id: true, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index eaceb2c..f8af54b 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -9,6 +9,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, @@ -73,8 +74,9 @@ export class Posts { @Column({ default: 'PUBLIC' }) scope: OpenScope; + @Index() @ApiProperty({ description: '생성된 날짜', type: Date }) - @CreateDateColumn() + @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) date_created: Date; @ApiProperty({ description: '수정된 날짜', type: Date }) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index e0a1818..2057a85 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -25,7 +25,6 @@ import { Request } from 'express'; import { PostsService } from './posts.service'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PublishPostDto } from './dtos/publish-post.dto'; -import { Posts } from './entities/posts.entity'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { CreatePostInput } from './dtos/create-post.input'; import { PublishPostInput } from './dtos/publish-post.input'; diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index f7bb60b..56de50c 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -4,6 +4,8 @@ import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; +import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; +import { PostsFilterOption } from 'src/commons/enums/posts-filter-option'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -21,22 +23,28 @@ export class PostsRepository extends Repository { } async fetchPosts(page) { - return this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') - .addSelect([ - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.isPublished = true') - .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) - .orderBy('p.id', 'DESC') - .take(page.getLimit()) - .skip(page.getOffset()) - .getManyAndCount(); + return ( + this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .innerJoinAndSelect('p.postBackground', 'postBackground') + .innerJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.isPublished = true') + .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) + //sql injection 방지를 위해 반드시 enum 거칠 것 + .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { + search: `%${page.search}%`, + }) + .orderBy(`p.${PostsOrderOption[page.order]}`, 'DESC') + .take(page.getLimit()) + .skip(page.getOffset()) + .getManyAndCount() + ); } async fetchPostDetail({ id, scope }): Promise { @@ -86,7 +94,11 @@ export class PostsRepository extends Repository { .where(`p.userKakaoId = any(${subQuery})`) .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], + }) //sql injection 방지를 위해 만드시 enum 거칠 것 + .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { + search: `%${page.search}%`, }) + .orderBy(`p.${PostsOrderOption[page.order]}`, 'DESC') .andWhere('p.isPublished = true') .orderBy('p.id', 'DESC') .take(page.getLimit()) @@ -108,6 +120,7 @@ export class PostsRepository extends Repository { ]) .where('p.userKakaoId = :kakaoId', { kakaoId }) .andWhere(`p.isPublished = false`) + .orderBy('p.id', 'DESC') .getMany() ); } @@ -130,11 +143,12 @@ export class PostsRepository extends Repository { .where('p.userKakaoId = :userKakaoId', { userKakaoId }) .andWhere('p.scope IN (:scope)', { scope }) .andWhere('p.isPublished = true'); + if (postCategoryName) { query.andWhere('postCategory.name = :postCategoryName', { postCategoryName, }); } - return await query.getMany(); + return await query.orderBy('p.id', 'DESC').getMany(); } } diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index fe5d07d..26e137b 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -63,7 +63,7 @@ export class UsersController { summary: '로그인된 유저의 이름이나 설명을 변경', description: '로그인된 유저의 이름이나 설명, 혹은 둘 다를 변경한다.', }) - @ApiOkResponse({ description: '변경 성공', type: PatchUserInput }) + @ApiOkResponse({ description: '변경 성공', type: UserResponseDto }) @ApiCookieAuth() @Patch() @HttpCode(200) diff --git a/src/commons/enums/posts-filter-option.ts b/src/commons/enums/posts-filter-option.ts new file mode 100644 index 0000000..6e72845 --- /dev/null +++ b/src/commons/enums/posts-filter-option.ts @@ -0,0 +1,11 @@ +export enum PostsFilterOption { + TITLE = 'p.title', + CONTENT = 'p.content', + USER = 'user.username', +} +// client에게 key값을 받기 위한 wrapping enum +export enum PostsFilterOptionWrap { + TITLE = 'TITLE', + CONTENT = 'CONTENT', + USER = 'USER', +} diff --git a/src/commons/enums/posts-order-option.ts b/src/commons/enums/posts-order-option.ts new file mode 100644 index 0000000..fb4c0de --- /dev/null +++ b/src/commons/enums/posts-order-option.ts @@ -0,0 +1,14 @@ +export enum PostsOrderOption { + LIKE = 'like_count', + VIEW = 'view_count', + COMMENT = 'comment_count', + DATE = 'id', +} + +// client에게 key값을 받기 위한 wrapping enum +export enum PostsOrderOptionWrap { + LIKE = 'LIKE', + VIEW = 'VIEW', + COMMENT = 'COMMENT', + DATE = 'DATE', +} From 06c429669ac85da3bf8295ad422a4dc870e82320 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 18 Apr 2024 13:50:33 +0900 Subject: [PATCH 032/236] feat: add cursor-based pagination --- src/APIs/posts/posts.controller.ts | 25 +++++++ src/APIs/posts/posts.repository.ts | 31 ++++++++ src/APIs/posts/posts.service.ts | 73 +++++++++++++++++++ src/commons/enums/sort-option.ts | 4 + .../cursor-pages/dtos/cursor-page-meta.dto.ts | 20 +++++ .../dtos/cursor-page-option.dto.ts | 44 +++++++++++ .../cursor-pages/dtos/cursor-page.dto.ts | 13 ++++ .../interfaces/cursor-page-meta-dto-params.ts | 8 ++ 8 files changed, 218 insertions(+) create mode 100644 src/commons/enums/sort-option.ts create mode 100644 src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts create mode 100644 src/utils/cursor-pages/dtos/cursor-page-option.dto.ts create mode 100644 src/utils/cursor-pages/dtos/cursor-page.dto.ts create mode 100644 src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 2057a85..7104005 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -39,6 +39,10 @@ import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, } from './dtos/fetch-post-for-update.dto'; +import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; +import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; +import { Posts } from './entities/posts.entity'; +import { SortOption } from 'src/commons/enums/sort-option'; @ApiTags('게시글 API') @Controller('posts') @@ -229,4 +233,25 @@ export class PostsController { ...query, }); } + + @ApiOperation({ summary: '커서기반 페이지네이션' }) + @Get('customCursorPaginate') + async paginateByCustomCursor( + @Query() customCursorPageOptionsDto: CustomCursorPageOptionsDto, + ): Promise> { + if ( + !customCursorPageOptionsDto.customCursor && + customCursorPageOptionsDto.sort === SortOption.ASC + ) { + customCursorPageOptionsDto.customCursor = + this.postsService.createDefaultCustomCursorValue(7, 7, '0'); + } else if ( + !customCursorPageOptionsDto.customCursor && + customCursorPageOptionsDto.sort === SortOption.DESC + ) { + customCursorPageOptionsDto.customCursor = + this.postsService.createDefaultCustomCursorValue(7, 7, '9'); + } + return this.postsService.paginateByCustomCursor(customCursorPageOptionsDto); + } } diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 56de50c..f925bfe 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -6,6 +6,9 @@ import { PostResponseDto } from './dtos/post-response.dto'; import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; import { PostsFilterOption } from 'src/commons/enums/posts-filter-option'; +import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; +import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; +import { SortOption } from 'src/commons/enums/sort-option'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -151,4 +154,32 @@ export class PostsRepository extends Repository { } return await query.orderBy('p.id', 'DESC').getMany(); } + + // cursor + async paginateByCustomCursor( + customCursorPageOptionsDto: CustomCursorPageOptionsDto, + ) { + const queryBuilder = this.createQueryBuilder('p'); + console.log(customCursorPageOptionsDto.order); + const ORDER = PostsOrderOption[customCursorPageOptionsDto.order]; + console.log(ORDER); + const queryByPriceSort = + customCursorPageOptionsDto.sort === SortOption.ASC + ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + + queryBuilder + .take(customCursorPageOptionsDto.take) + .where(queryByPriceSort, { + customCursor: customCursorPageOptionsDto.customCursor, + }) + .orderBy(`p.${ORDER}`, customCursorPageOptionsDto.sort as any) + .addOrderBy('p.id', customCursorPageOptionsDto.sort as any); + + const allPosts: Posts[] = await this.find(); + const posts: Posts[] = await queryBuilder.getMany(); + const total: number = await queryBuilder.getCount(); + + return { allPosts, posts, total }; + } } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index b822875..f97f732 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -30,6 +30,11 @@ import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, } from './dtos/fetch-post-for-update.dto'; +import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; +import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; +import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; +import { all } from 'axios'; @Injectable() export class PostsService { @@ -226,4 +231,72 @@ export class PostsService { postCategoryName, }); } + //cursor + + async paginateByCustomCursor( + customCursorPageOptionsDto: CustomCursorPageOptionsDto, + ): Promise> { + const { allPosts, posts, total } = + await this.postsRepository.paginateByCustomCursor( + customCursorPageOptionsDto, + ); + + const order = PostsOrderOption[customCursorPageOptionsDto.order]; + let hasNextData: boolean = true; + let idByLastDataPerPage: number; + let customCursor: string; + + const takePerPage = customCursorPageOptionsDto.take; + const isLastPage = total <= takePerPage; + const lastDataPerPage = posts[posts.length - 1]; + + if (isLastPage) { + hasNextData = false; + idByLastDataPerPage = null; + customCursor = null; + } else { + idByLastDataPerPage = lastDataPerPage.id; + const lastDataPerPageIndexOf = allPosts.findIndex( + (data) => data.id === idByLastDataPerPage, + ); + customCursor = await this.createCustomCursor({ + cursorIndex: lastDataPerPageIndexOf, + order, + }); + } + + const customCursorPageMetaDto = new CustomCursorPageMetaDto({ + customCursorPageOptionsDto, + total, + hasNextData, + customCursor, + }); + + return new CustomCursorPageDto(posts, customCursorPageMetaDto); + } + + async createCustomCursor({ cursorIndex, order }): Promise { + const posts = await this.postsRepository.find(); + + const customCursor = posts.map((posts) => { + const id = posts.id; + const _order = posts[order]; + const customCursor: string = + String(_order).padStart(7, '0') + String(id).padStart(7, '0'); + return customCursor; + }); + + return customCursor[cursorIndex]; + } + + createDefaultCustomCursorValue( + digitById: number, + digitByTargetColumn: number, + initialValue: string, + ) { + const defaultCustomCursor: string = + String().padStart(digitByTargetColumn, `${initialValue}`) + + String().padStart(digitById, `${initialValue}`); + return defaultCustomCursor; + } } diff --git a/src/commons/enums/sort-option.ts b/src/commons/enums/sort-option.ts new file mode 100644 index 0000000..73b736a --- /dev/null +++ b/src/commons/enums/sort-option.ts @@ -0,0 +1,4 @@ +export enum SortOption { + ASC = 'ASC', + DESC = 'DESC', +} diff --git a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts new file mode 100644 index 0000000..baff7ff --- /dev/null +++ b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts @@ -0,0 +1,20 @@ +import { CustomCursorPageMetaDtoParameters } from '../interfaces/cursor-page-meta-dto-params'; + +export class CustomCursorPageMetaDto { + readonly total: number; + readonly take: number; + readonly hasNextData: boolean; + readonly customCursor: string; + + constructor({ + customCursorPageOptionsDto, + total, + hasNextData, + customCursor, + }: CustomCursorPageMetaDtoParameters) { + this.take = customCursorPageOptionsDto.take; + this.total = total; + this.hasNextData = hasNextData; + this.customCursor = customCursor; + } +} diff --git a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts new file mode 100644 index 0000000..78ef7eb --- /dev/null +++ b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts @@ -0,0 +1,44 @@ +// page-option.dto.ts + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, IsOptional } from 'class-validator'; +import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; +import { SortOption } from 'src/commons/enums/sort-option'; + +export class CustomCursorPageOptionsDto { + @ApiProperty({ + description: '정렬 옵션', + type: 'enum', + enum: SortOption, + required: false, + default: SortOption.ASC, + }) + @Type(() => String) + @IsEnum(SortOption) + @IsOptional() + readonly sort?: SortOption = SortOption.ASC; + + @ApiProperty({ + description: '페이지네이션 단위', + type: Number, + required: false, + }) + @Type(() => Number) + @IsOptional() + readonly take?: number = 5; + + @ApiProperty({ + description: '정렬 옵션', + type: 'enum', + enum: PostsOrderOptionWrap, + required: false, + default: PostsOrderOptionWrap.DATE, + }) + order?: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; + + @ApiProperty({ description: '커서', type: String, required: false }) + @Type(() => String) + @IsOptional() + customCursor?: string; +} diff --git a/src/utils/cursor-pages/dtos/cursor-page.dto.ts b/src/utils/cursor-pages/dtos/cursor-page.dto.ts new file mode 100644 index 0000000..d65bd86 --- /dev/null +++ b/src/utils/cursor-pages/dtos/cursor-page.dto.ts @@ -0,0 +1,13 @@ +import { IsArray } from 'class-validator'; +import { CustomCursorPageMetaDto } from './cursor-page-meta.dto'; + +export class CustomCursorPageDto { + @IsArray() + readonly data: T[]; + readonly meta: CustomCursorPageMetaDto; + + constructor(data: T[], meta: CustomCursorPageMetaDto) { + this.data = data; + this.meta = meta; + } +} diff --git a/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts b/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts new file mode 100644 index 0000000..85c561a --- /dev/null +++ b/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts @@ -0,0 +1,8 @@ +import { CustomCursorPageOptionsDto } from '../dtos/cursor-page-option.dto'; + +export interface CustomCursorPageMetaDtoParameters { + customCursorPageOptionsDto: CustomCursorPageOptionsDto; + total: number; + hasNextData: boolean; + customCursor: string; +} From f0bfb2f873cf9a7d5fc89e6df36ab46eef5cc6fd Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 20 Apr 2024 17:39:25 +0900 Subject: [PATCH 033/236] docs: add swagger on cursor pagination api & dtos --- src/APIs/posts/dtos/cursor-fetch-posts.dto.ts | 14 ++++ .../dtos/cursor-page-post-response.dto.ts | 14 ++++ src/APIs/posts/posts.controller.ts | 77 ++++++++++--------- src/APIs/posts/posts.module.ts | 1 - src/APIs/posts/posts.repository.ts | 35 +++++---- src/APIs/posts/posts.service.ts | 22 +++--- .../cursor-pages/dtos/cursor-page-meta.dto.ts | 8 ++ .../dtos/cursor-page-option.dto.ts | 10 --- .../cursor-pages/dtos/cursor-page.dto.ts | 7 ++ 9 files changed, 116 insertions(+), 72 deletions(-) create mode 100644 src/APIs/posts/dtos/cursor-fetch-posts.dto.ts create mode 100644 src/APIs/posts/dtos/cursor-page-post-response.dto.ts diff --git a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts new file mode 100644 index 0000000..d4fb640 --- /dev/null +++ b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; +import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; + +export class CursorFetchPosts extends CustomCursorPageOptionsDto { + @ApiProperty({ + description: '정렬 옵션', + type: 'enum', + enum: PostsOrderOptionWrap, + required: false, + default: PostsOrderOptionWrap.DATE, + }) + order?: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; +} diff --git a/src/APIs/posts/dtos/cursor-page-post-response.dto.ts b/src/APIs/posts/dtos/cursor-page-post-response.dto.ts new file mode 100644 index 0000000..913b515 --- /dev/null +++ b/src/APIs/posts/dtos/cursor-page-post-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { PostResponseDto } from './post-response.dto'; + +export class CursorPagePostResponseDto { + @ApiProperty({ description: '조회된 데이터', type: [PostResponseDto] }) + readonly data: PostResponseDto[]; + + @ApiProperty({ + description: '페이지네이션 메타 데이터', + type: CustomCursorPageMetaDto, + }) + readonly meta: CustomCursorPageMetaDto; +} diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 7104005..c8b606c 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -39,10 +39,10 @@ import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, } from './dtos/fetch-post-for-update.dto'; -import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { Posts } from './entities/posts.entity'; import { SortOption } from 'src/commons/enums/sort-option'; +import { CursorFetchPosts } from './dtos/cursor-fetch-posts.dto'; +import { CursorPagePostResponseDto } from './dtos/cursor-page-post-response.dto'; @ApiTags('게시글 API') @Controller('posts') @@ -85,16 +85,35 @@ export class PostsController { } @ApiOperation({ - summary: '전체 게시글 조회 API', + summary: '[offset]전체 게시글 조회 API', description: - 'Query를 통해 페이지네이션 가능. default) pageNo: 1, pageSize: 10', + 'Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', }) @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) @HttpCode(200) - @Get() + @Get('offset') async fetchPosts(@Query() post: FetchPostsDto): Promise { return await this.postsService.fetchPosts(post); } + + @ApiOperation({ + summary: '[offset]친구 게시글 조회', + description: + '친구의 게시글을 조회한다. Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', + }) + @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) + @UseGuards(AuthGuardV2) + @HttpCode(200) + @ApiCookieAuth() + @Get('friends') + async fetchFriendsPosts( + @Query() page: FetchPostsDto, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.postsService.fetchFriendsPosts({ kakaoId, page }); + } + @ApiOperation({ summary: '임시작성 게시글 조회', description: '로그인된 유저의 임시작성 게시글을 조회한다.', @@ -136,24 +155,6 @@ export class PostsController { return await this.postsService.saveImage(file); } - @ApiOperation({ - summary: '친구 게시글 조회', - description: - '친구의 게시글을 조회한다. Query를 통해 페이지네이션 가능. default) pageNo: 1, pageSize: 10', - }) - @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @UseGuards(AuthGuardV2) - @HttpCode(200) - @ApiCookieAuth() - @Get('friends') - async fetchFriendsPosts( - @Query() page: FetchPostsDto, - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchFriendsPosts({ kakaoId, page }); - } - @ApiOperation({ summary: '게시글 soft delete', description: @@ -234,24 +235,28 @@ export class PostsController { }); } - @ApiOperation({ summary: '커서기반 페이지네이션' }) - @Get('customCursorPaginate') + @ApiOperation({ + summary: '[cursor]전체 게시글 조회 API', + description: + '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', + }) + @Get('cursor') + @ApiOkResponse({ type: CursorPagePostResponseDto }) async paginateByCustomCursor( - @Query() customCursorPageOptionsDto: CustomCursorPageOptionsDto, - ): Promise> { - if ( - !customCursorPageOptionsDto.customCursor && - customCursorPageOptionsDto.sort === SortOption.ASC - ) { - customCursorPageOptionsDto.customCursor = + @Query() cursorOption: CursorFetchPosts, + @Req() req: Request, + ): Promise> { + const kakaoId = req.user.userId; + if (!cursorOption.customCursor && cursorOption.sort === SortOption.ASC) { + cursorOption.customCursor = this.postsService.createDefaultCustomCursorValue(7, 7, '0'); } else if ( - !customCursorPageOptionsDto.customCursor && - customCursorPageOptionsDto.sort === SortOption.DESC + !cursorOption.customCursor && + cursorOption.sort === SortOption.DESC ) { - customCursorPageOptionsDto.customCursor = + cursorOption.customCursor = this.postsService.createDefaultCustomCursorValue(7, 7, '9'); } - return this.postsService.paginateByCustomCursor(customCursorPageOptionsDto); + return this.postsService.paginateByCustomCursor({ cursorOption, kakaoId }); } } diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 1620cfa..b467137 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Posts } from './entities/posts.entity'; import { User } from '../users/entities/user.entity'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { PostsController } from './posts.controller'; import { UtilsModule } from 'src/utils/utils.module'; import { AwsModule } from 'src/utils/aws/aws.module'; diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index f925bfe..b7ba001 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -6,8 +6,6 @@ import { PostResponseDto } from './dtos/post-response.dto'; import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; import { PostsFilterOption } from 'src/commons/enums/posts-filter-option'; -import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; -import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { SortOption } from 'src/commons/enums/sort-option'; @Injectable() export class PostsRepository extends Repository { @@ -156,27 +154,36 @@ export class PostsRepository extends Repository { } // cursor - async paginateByCustomCursor( - customCursorPageOptionsDto: CustomCursorPageOptionsDto, - ) { + async paginateByCustomCursor({ cursorOption }) { const queryBuilder = this.createQueryBuilder('p'); - console.log(customCursorPageOptionsDto.order); - const ORDER = PostsOrderOption[customCursorPageOptionsDto.order]; - console.log(ORDER); + const ORDER = PostsOrderOption[cursorOption.order]; const queryByPriceSort = - customCursorPageOptionsDto.sort === SortOption.ASC + cursorOption.sort === SortOption.ASC ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; queryBuilder - .take(customCursorPageOptionsDto.take) + .take(cursorOption.take) .where(queryByPriceSort, { - customCursor: customCursorPageOptionsDto.customCursor, + customCursor: cursorOption.customCursor, }) - .orderBy(`p.${ORDER}`, customCursorPageOptionsDto.sort as any) - .addOrderBy('p.id', customCursorPageOptionsDto.sort as any); + .innerJoin('p.user', 'user') + .innerJoinAndSelect('p.postBackground', 'postBackground') + .innerJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.isPublished = true') + .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) + .orderBy(`p.${ORDER}`, cursorOption.sort as any) + .addOrderBy('p.id', cursorOption.sort as any); - const allPosts: Posts[] = await this.find(); + const allPosts: Posts[] = await this.find({ + where: { scope: OpenScope.PUBLIC }, + }); const posts: Posts[] = await queryBuilder.getMany(); const total: number = await queryBuilder.getCount(); diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index f97f732..b27f9b3 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -32,9 +32,7 @@ import { } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; -import { all } from 'axios'; @Injectable() export class PostsService { @@ -233,20 +231,22 @@ export class PostsService { } //cursor - async paginateByCustomCursor( - customCursorPageOptionsDto: CustomCursorPageOptionsDto, - ): Promise> { + async paginateByCustomCursor({ + cursorOption, + kakaoId, + }): Promise> { + console.log(kakaoId); const { allPosts, posts, total } = - await this.postsRepository.paginateByCustomCursor( - customCursorPageOptionsDto, - ); + await this.postsRepository.paginateByCustomCursor({ + cursorOption, + }); - const order = PostsOrderOption[customCursorPageOptionsDto.order]; + const order = PostsOrderOption[cursorOption.order]; let hasNextData: boolean = true; let idByLastDataPerPage: number; let customCursor: string; - const takePerPage = customCursorPageOptionsDto.take; + const takePerPage = cursorOption.take; const isLastPage = total <= takePerPage; const lastDataPerPage = posts[posts.length - 1]; @@ -266,7 +266,7 @@ export class PostsService { } const customCursorPageMetaDto = new CustomCursorPageMetaDto({ - customCursorPageOptionsDto, + customCursorPageOptionsDto: cursorOption, total, hasNextData, customCursor, diff --git a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts index baff7ff..3c17b04 100644 --- a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts @@ -1,9 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; import { CustomCursorPageMetaDtoParameters } from '../interfaces/cursor-page-meta-dto-params'; export class CustomCursorPageMetaDto { + @ApiProperty({ description: '전체 아이템 수', type: Number }) readonly total: number; + + @ApiProperty({ description: '한번에 가져올 아이템 수', type: Number }) readonly take: number; + + @ApiProperty({ description: '다음 페이지 존재 여부', type: Boolean }) readonly hasNextData: boolean; + + @ApiProperty({ description: '커서 값', type: String }) readonly customCursor: string; constructor({ diff --git a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts index 78ef7eb..fd3e12e 100644 --- a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts @@ -3,7 +3,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsOptional } from 'class-validator'; -import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; import { SortOption } from 'src/commons/enums/sort-option'; export class CustomCursorPageOptionsDto { @@ -28,15 +27,6 @@ export class CustomCursorPageOptionsDto { @IsOptional() readonly take?: number = 5; - @ApiProperty({ - description: '정렬 옵션', - type: 'enum', - enum: PostsOrderOptionWrap, - required: false, - default: PostsOrderOptionWrap.DATE, - }) - order?: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; - @ApiProperty({ description: '커서', type: String, required: false }) @Type(() => String) @IsOptional() diff --git a/src/utils/cursor-pages/dtos/cursor-page.dto.ts b/src/utils/cursor-pages/dtos/cursor-page.dto.ts index d65bd86..cabecff 100644 --- a/src/utils/cursor-pages/dtos/cursor-page.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page.dto.ts @@ -1,9 +1,16 @@ import { IsArray } from 'class-validator'; import { CustomCursorPageMetaDto } from './cursor-page-meta.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class CustomCursorPageDto { + @ApiProperty({ description: '조회된 데이터', type: [] }) @IsArray() readonly data: T[]; + + @ApiProperty({ + description: '페이지네이션 메타 데이터', + type: CustomCursorPageMetaDto, + }) readonly meta: CustomCursorPageMetaDto; constructor(data: T[], meta: CustomCursorPageMetaDto) { From 7e445c9b85bb14f6bb0498df74936f44e178bc42 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 20 Apr 2024 21:25:49 +0900 Subject: [PATCH 034/236] feat: add friends post fetching api --- .../postCategories/PostCategories.service.ts | 3 +- src/APIs/posts/posts.controller.ts | 53 +++++++++++++++---- src/APIs/posts/posts.repository.ts | 43 ++++++++++++++- src/APIs/posts/posts.service.ts | 50 +++++++++++++++++ .../dtos/cursor-page-option.dto.ts | 2 +- 5 files changed, 137 insertions(+), 14 deletions(-) diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index 6957802..f5b2514 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -18,7 +18,8 @@ export class PostCategoriesService { async create({ kakaoId, name }): Promise { const data = await this.findWithName({ kakaoId, name }); - if (data) { + console.log(data); + if (data.length > 0) { throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); } const result = await this.postCategoriesRepository.save({ diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index c8b606c..50bb304 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -105,7 +105,7 @@ export class PostsController { @UseGuards(AuthGuardV2) @HttpCode(200) @ApiCookieAuth() - @Get('friends') + @Get('offset/friends') async fetchFriendsPosts( @Query() page: FetchPostsDto, @Req() req: Request, @@ -242,21 +242,52 @@ export class PostsController { }) @Get('cursor') @ApiOkResponse({ type: CursorPagePostResponseDto }) - async paginateByCustomCursor( + async fetchCursor( @Query() cursorOption: CursorFetchPosts, @Req() req: Request, ): Promise> { const kakaoId = req.user.userId; - if (!cursorOption.customCursor && cursorOption.sort === SortOption.ASC) { - cursorOption.customCursor = - this.postsService.createDefaultCustomCursorValue(7, 7, '0'); - } else if ( - !cursorOption.customCursor && - cursorOption.sort === SortOption.DESC - ) { - cursorOption.customCursor = - this.postsService.createDefaultCustomCursorValue(7, 7, '9'); + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( + 7, + 7, + '0', + ); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( + 7, + 7, + '9', + ); } return this.postsService.paginateByCustomCursor({ cursorOption, kakaoId }); } + + @ApiOperation({ + summary: '[cursor]친구 게시글 조회 API', + description: + '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', + }) + @Get('cursor/friends') + @ApiOkResponse({ type: CursorPagePostResponseDto }) + async fetchFriendsCursor( + @Query() cursorOption: CursorFetchPosts, + @Req() req: Request, + ): Promise> { + const kakaoId = req.user.userId; + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( + 7, + 7, + '0', + ); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( + 7, + 7, + '9', + ); + } + return this.postsService.fetchFriendsCursor({ cursorOption, kakaoId }); + } } diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index b7ba001..b689df0 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -1,4 +1,4 @@ -import { DataSource, Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; @@ -154,6 +154,47 @@ export class PostsRepository extends Repository { } // cursor + + async paginateByCustomCursorFriends({ cursorOption, subQuery }) { + const queryBuilder = this.createQueryBuilder('p'); + const ORDER = PostsOrderOption[cursorOption.order]; + const queryByPriceSort = + cursorOption.sort === SortOption.ASC + ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + + queryBuilder + .take(cursorOption.take) + .where(queryByPriceSort, { + customCursor: cursorOption.customCursor, + }) + .innerJoin('p.user', 'user') + .innerJoinAndSelect('p.postBackground', 'postBackground') + .innerJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.isPublished = true') + .andWhere(`p.userKakaoId = any(${subQuery})`) + .andWhere('p.scope IN (:...scopes)', { + scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], + }) //sql injection 방지를 위해 만드시 enum 거칠 것 + .orderBy(`p.${ORDER}`, cursorOption.sort as any) + .addOrderBy('p.id', cursorOption.sort as any); + + console.log('하이'); + const allPosts: Posts[] = await this.find({ + where: { scope: In([OpenScope.PUBLIC, OpenScope.PROTECTED]) }, + }); + const posts: Posts[] = await queryBuilder.getMany(); + const total: number = await queryBuilder.getCount(); + + return { allPosts, posts, total }; + } + async paginateByCustomCursor({ cursorOption }) { const queryBuilder = this.createQueryBuilder('p'); const ORDER = PostsOrderOption[cursorOption.order]; diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index b27f9b3..057fae7 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -299,4 +299,54 @@ export class PostsService { String().padStart(digitById, `${initialValue}`); return defaultCustomCursor; } + + async fetchFriendsCursor({ + cursorOption, + kakaoId, + }): Promise> { + console.log(kakaoId); + const subQuery = await this.neighborsRepository + .createQueryBuilder('n') + .select('n.toUserKakaoId') + .where(`n.fromUserKakaoId = ${kakaoId}`) + .getQuery(); + const { allPosts, posts, total } = + await this.postsRepository.paginateByCustomCursorFriends({ + cursorOption, + subQuery, + }); + + const order = PostsOrderOption[cursorOption.order]; + let hasNextData: boolean = true; + let idByLastDataPerPage: number; + let customCursor: string; + + const takePerPage = cursorOption.take; + const isLastPage = total <= takePerPage; + const lastDataPerPage = posts[posts.length - 1]; + + if (isLastPage) { + hasNextData = false; + idByLastDataPerPage = null; + customCursor = null; + } else { + idByLastDataPerPage = lastDataPerPage.id; + const lastDataPerPageIndexOf = allPosts.findIndex( + (data) => data.id === idByLastDataPerPage, + ); + customCursor = await this.createCustomCursor({ + cursorIndex: lastDataPerPageIndexOf, + order, + }); + } + + const customCursorPageMetaDto = new CustomCursorPageMetaDto({ + customCursorPageOptionsDto: cursorOption, + total, + hasNextData, + customCursor, + }); + + return new CustomCursorPageDto(posts, customCursorPageMetaDto); + } } diff --git a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts index fd3e12e..a80a60e 100644 --- a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts @@ -30,5 +30,5 @@ export class CustomCursorPageOptionsDto { @ApiProperty({ description: '커서', type: String, required: false }) @Type(() => String) @IsOptional() - customCursor?: string; + cursor?: string; } From f3f4ac92a14310cb090d5c0b6cb1ccef7c8e3209 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 21 Apr 2024 14:26:47 +0900 Subject: [PATCH 035/236] feat: change fetching api to cursor-based-pagination --- src/APIs/neighbors/neighbors.module.ts | 1 + src/APIs/neighbors/neighbors.service.ts | 18 +++ .../PostCategories.controller.ts | 22 ++-- .../postCategories/PostCategories.module.ts | 6 +- .../PostCategories.repository.ts | 30 +++++ .../postCategories/PostCategories.service.ts | 23 ++-- .../dtos/fetch-post-category.dto.ts | 12 ++ .../entities/postCategory.entity.ts | 5 + src/APIs/posts/dtos/fetch-user-posts.input.ts | 3 +- src/APIs/posts/posts.controller.ts | 116 ++++++++---------- src/APIs/posts/posts.module.ts | 10 +- src/APIs/posts/posts.repository.ts | 45 ++++--- src/APIs/posts/posts.service.ts | 83 ++++++++----- 13 files changed, 235 insertions(+), 139 deletions(-) create mode 100644 src/APIs/postCategories/PostCategories.repository.ts create mode 100644 src/APIs/postCategories/dtos/fetch-post-category.dto.ts diff --git a/src/APIs/neighbors/neighbors.module.ts b/src/APIs/neighbors/neighbors.module.ts index f47d656..277e407 100644 --- a/src/APIs/neighbors/neighbors.module.ts +++ b/src/APIs/neighbors/neighbors.module.ts @@ -10,5 +10,6 @@ import { User } from '../users/entities/user.entity'; imports: [UsersModule, TypeOrmModule.forFeature([Neighbor, User])], providers: [NeighborsService], controllers: [NeighborsController], + exports: [NeighborsService], }) export class NeighborsModule {} diff --git a/src/APIs/neighbors/neighbors.service.ts b/src/APIs/neighbors/neighbors.service.ts index a82426d..1d07db4 100644 --- a/src/APIs/neighbors/neighbors.service.ts +++ b/src/APIs/neighbors/neighbors.service.ts @@ -6,6 +6,8 @@ import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; +import e from 'express'; +import { OpenScope } from 'src/commons/enums/open-scope.enum'; @Injectable() export class NeighborsService { @@ -21,6 +23,22 @@ export class NeighborsService { } return false; } + + async getScope({ from_user, to_user }) { + if (from_user === to_user) + return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; + if (from_user !== null && to_user !== null) { + const neighbor = await this.neighborsRepository.findOne({ + where: { from_user, to_user }, + }); + if (neighbor) { + return [OpenScope.PUBLIC, OpenScope.PROTECTED]; + } + } + + return [OpenScope.PUBLIC]; + } + async isExist({ from_user, to_user }): Promise { console.log(from_user, to_user); const neighbor = await this.neighborsRepository.findOne({ diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index eb37e91..c171961 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -20,8 +20,8 @@ import { PostCategoriesService } from './PostCategories.service'; import { Request } from 'express'; import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; -import { PostCategory } from './entities/postCategory.entity'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { FetchPostCategoryDto } from './dtos/fetch-post-category.dto'; @ApiTags('카테고리 API') @Controller('postcg') @@ -50,20 +50,26 @@ export class PostCategoriesController { } @ApiOperation({ - summary: '유저의 모든 카테고리 불러오기', - description: '로그인된 유저가 생성한 카테고리를 모두 불러온다', + summary: '특정 유저의 카테고리 정보 조회', + description: + '특정 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', }) @ApiCookieAuth() @ApiOkResponse({ description: '', - type: [PostCategory], + type: [FetchPostCategoryDto], }) - @UseGuards(AuthGuardV2) - @Get() + @Get(':kakaoId') @HttpCode(200) - async fetchPostCategories(@Req() req: Request): Promise { + async fetchPostCategories( + @Req() req: Request, + @Param('kakaoId') targetKakaoId: number, + ): Promise { const kakaoId = req.user.userId; - return await this.postCategoriesService.fetchAll({ kakaoId }); + return await this.postCategoriesService.fetchAll({ + kakaoId, + targetKakaoId, + }); } @ApiOperation({ diff --git a/src/APIs/postCategories/PostCategories.module.ts b/src/APIs/postCategories/PostCategories.module.ts index 6cd50aa..8e6e8c2 100644 --- a/src/APIs/postCategories/PostCategories.module.ts +++ b/src/APIs/postCategories/PostCategories.module.ts @@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { PostCategory } from './entities/postCategory.entity'; import { PostCategoriesService } from './PostCategories.service'; import { PostCategoriesController } from './PostCategories.controller'; +import { PostCategoriesRepository } from './PostCategories.repository'; +import { NeighborsModule } from '../neighbors/neighbors.module'; @Module({ - imports: [TypeOrmModule.forFeature([PostCategory])], - providers: [PostCategoriesService], + imports: [TypeOrmModule.forFeature([PostCategory]), NeighborsModule], + providers: [PostCategoriesService, PostCategoriesRepository], controllers: [PostCategoriesController], }) export class PostCategoriesModule {} diff --git a/src/APIs/postCategories/PostCategories.repository.ts b/src/APIs/postCategories/PostCategories.repository.ts new file mode 100644 index 0000000..d0ad4bb --- /dev/null +++ b/src/APIs/postCategories/PostCategories.repository.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { PostCategory } from './entities/postCategory.entity'; +import { DataSource, Repository } from 'typeorm'; + +@Injectable() +export class PostCategoriesRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PostCategory, dataSource.createEntityManager()); + } + + async fetchUserCategory({ scope, userKakaoId }) { + const query = this.createQueryBuilder('pc') + .select([ + 'COALESCE(COUNT(p.id), 0) as postCount', // postCategory당 posts의 개수를 집계 + 'pc.id as categoryId', // postCategory에 대한 그룹화를 위해 id 열을 추가 + 'pc.name as categoryName', + ]) + .leftJoin( + 'pc.posts', + 'p', + 'p.scope IN (:scope) AND p.isPublished = true', + { scope }, + ) // LEFT JOIN으로 연결된 엔티티의 조건을 추가 + .where('pc.userKakaoId = :userKakaoId', { userKakaoId }) + .groupBy('pc.id'); // postCategory.id를 기준으로 그룹화 + console.log(userKakaoId); + + return await query.getRawMany(); + } +} diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index f5b2514..9bc6e12 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -1,14 +1,15 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { PostCategory } from './entities/postCategory.entity'; -import { Repository } from 'typeorm'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; +import { PostCategoriesRepository } from './PostCategories.repository'; + +import { FetchPostCategoryDto } from './dtos/fetch-post-category.dto'; +import { NeighborsService } from '../neighbors/neighbors.service'; @Injectable() export class PostCategoriesService { constructor( - @InjectRepository(PostCategory) - private readonly postCategoriesRepository: Repository, + private readonly neighborsService: NeighborsService, + private readonly postCategoriesRepository: PostCategoriesRepository, ) {} async findWithName({ kakaoId, name }) { return await this.postCategoriesRepository.find({ @@ -18,7 +19,6 @@ export class PostCategoriesService { async create({ kakaoId, name }): Promise { const data = await this.findWithName({ kakaoId, name }); - console.log(data); if (data.length > 0) { throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); } @@ -29,9 +29,14 @@ export class PostCategoriesService { return result; } - async fetchAll({ kakaoId }): Promise { - return await this.postCategoriesRepository.find({ - where: { user: { kakaoId } }, + async fetchAll({ kakaoId, targetKakaoId }): Promise { + const scope = await this.neighborsService.getScope({ + from_user: targetKakaoId, + to_user: kakaoId, + }); + return await this.postCategoriesRepository.fetchUserCategory({ + userKakaoId: targetKakaoId, + scope, }); } diff --git a/src/APIs/postCategories/dtos/fetch-post-category.dto.ts b/src/APIs/postCategories/dtos/fetch-post-category.dto.ts new file mode 100644 index 0000000..e3e1cd5 --- /dev/null +++ b/src/APIs/postCategories/dtos/fetch-post-category.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FetchPostCategoryDto { + @ApiProperty({ type: Number }) + postCount: number; + + @ApiProperty({ type: String }) + categoryId: string; + + @ApiProperty({ type: String }) + categoryName: string; +} diff --git a/src/APIs/postCategories/entities/postCategory.entity.ts b/src/APIs/postCategories/entities/postCategory.entity.ts index 45a4f1f..69b6cd7 100644 --- a/src/APIs/postCategories/entities/postCategory.entity.ts +++ b/src/APIs/postCategories/entities/postCategory.entity.ts @@ -1,10 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { Column, Entity, JoinColumn, ManyToOne, + OneToMany, PrimaryGeneratedColumn, RelationId, } from 'typeorm'; @@ -23,6 +25,9 @@ export class PostCategory { @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; + @OneToMany(() => Posts, (posts) => posts.postCategory) + posts: Posts; + @Column() @RelationId((postCategory: PostCategory) => postCategory.user) userKakaoId: number; diff --git a/src/APIs/posts/dtos/fetch-user-posts.input.ts b/src/APIs/posts/dtos/fetch-user-posts.input.ts index e4265f0..9dbc534 100644 --- a/src/APIs/posts/dtos/fetch-user-posts.input.ts +++ b/src/APIs/posts/dtos/fetch-user-posts.input.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsOptional } from 'class-validator'; +import { CursorFetchPosts } from './cursor-fetch-posts.dto'; -export class FetchUserPostsInput { +export class FetchUserPostsInput extends CursorFetchPosts { @ApiProperty({ description: '필터링할 카테고리 이름', type: String, diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 50bb304..ad09919 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -84,36 +84,6 @@ export class PostsController { return await this.postsService.save(dto); } - @ApiOperation({ - summary: '[offset]전체 게시글 조회 API', - description: - 'Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', - }) - @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @HttpCode(200) - @Get('offset') - async fetchPosts(@Query() post: FetchPostsDto): Promise { - return await this.postsService.fetchPosts(post); - } - - @ApiOperation({ - summary: '[offset]친구 게시글 조회', - description: - '친구의 게시글을 조회한다. Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', - }) - @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @UseGuards(AuthGuardV2) - @HttpCode(200) - @ApiCookieAuth() - @Get('offset/friends') - async fetchFriendsPosts( - @Query() page: FetchPostsDto, - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchFriendsPosts({ kakaoId, page }); - } - @ApiOperation({ summary: '임시작성 게시글 조회', description: '로그인된 유저의 임시작성 게시글을 조회한다.', @@ -215,24 +185,33 @@ export class PostsController { } @ApiOperation({ - summary: '특정 유저의 게시글 조회', + summary: '[offset]전체 게시글 조회 API', description: - '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', + 'Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', }) - @Get('/user/:kakaoId') - @ApiOkResponse({ type: [PostResponseDto] }) - async fetchUserPosts( - @Param('kakaoId') targetKakaoId: number, + @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) + @HttpCode(200) + @Get('offset') + async fetchPosts(@Query() post: FetchPostsDto): Promise { + return await this.postsService.fetchPosts(post); + } + + @ApiOperation({ + summary: '[offset]친구 게시글 조회', + description: + '친구의 게시글을 조회한다. Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', + }) + @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) + @UseGuards(AuthGuardV2) + @HttpCode(200) + @ApiCookieAuth() + @Get('offset/friends') + async fetchFriendsPosts( + @Query() page: FetchPostsDto, @Req() req: Request, - @Query() query: FetchUserPostsInput, - ): Promise { - console.log(req.user); + ): Promise { const kakaoId = req.user.userId; - return await this.postsService.fetchUserPosts({ - kakaoId, - targetKakaoId, - ...query, - }); + return await this.postsService.fetchFriendsPosts({ kakaoId, page }); } @ApiOperation({ @@ -248,17 +227,9 @@ export class PostsController { ): Promise> { const kakaoId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { - cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( - 7, - 7, - '0', - ); + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { - cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( - 7, - 7, - '9', - ); + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } return this.postsService.paginateByCustomCursor({ cursorOption, kakaoId }); } @@ -276,18 +247,35 @@ export class PostsController { ): Promise> { const kakaoId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { - cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( - 7, - 7, - '0', - ); + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { - cursorOption.cursor = this.postsService.createDefaultCustomCursorValue( - 7, - 7, - '9', - ); + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } return this.postsService.fetchFriendsCursor({ cursorOption, kakaoId }); } + + @ApiOperation({ + summary: '[cursor]특정 유저의 게시글 조회', + description: + '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', + }) + @Get('/cursor/user/:kakaoId') + @ApiOkResponse({ type: CursorPagePostResponseDto }) + async fetchUserPosts( + @Param('kakaoId') targetKakaoId: number, + @Req() req: Request, + @Query() cursorOption: FetchUserPostsInput, + ): Promise { + const kakaoId = req.user.userId; + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); + } + return await this.postsService.fetchUserPosts({ + kakaoId, + targetKakaoId, + cursorOption, + }); + } } diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index b467137..14ddd00 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -12,18 +12,14 @@ import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; import { CommentsModule } from '../comments/comments.module'; +import { NeighborsModule } from '../neighbors/neighbors.module'; @Module({ imports: [ - TypeOrmModule.forFeature([ - Posts, - User, - Neighbor, - PostBackground, - PostCategory, - ]), + TypeOrmModule.forFeature([Posts, User, PostBackground, PostCategory]), UtilsModule, AwsModule, + NeighborsModule, StickerBlocksModule, CommentsModule, ], diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index b689df0..2ac0be8 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -125,13 +125,19 @@ export class PostsRepository extends Repository { .getMany() ); } + async fetchUserPosts({ cursorOption, scope, userKakaoId }) { + const queryBuilder = this.createQueryBuilder('p'); + const ORDER = PostsOrderOption[cursorOption.order]; + const queryByPriceSort = + cursorOption.sort === SortOption.ASC + ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; - async fetchUserPosts({ - scope, - userKakaoId, - postCategoryName, - }): Promise { - const query = this.createQueryBuilder('p') + queryBuilder + .take(cursorOption.take) + .where(queryByPriceSort, { + customCursor: cursorOption.customCursor, + }) .innerJoin('p.user', 'user') .innerJoinAndSelect('p.postBackground', 'postBackground') .innerJoinAndSelect('p.postCategory', 'postCategory') @@ -141,19 +147,27 @@ export class PostsRepository extends Repository { 'user.profile_image', 'user.username', ]) - .where('p.userKakaoId = :userKakaoId', { userKakaoId }) + .where('p.userKakaoId = :userKakaoId', { + userKakaoId, + }) .andWhere('p.scope IN (:scope)', { scope }) - .andWhere('p.isPublished = true'); - - if (postCategoryName) { - query.andWhere('postCategory.name = :postCategoryName', { - postCategoryName, + .andWhere('p.isPublished = true') + .orderBy(`p.${ORDER}`, cursorOption.sort as any) + .addOrderBy('p.id', cursorOption.sort as any); + if (cursorOption.postCategoryName) { + queryBuilder.andWhere('postCategory.name = :postCategoryName', { + postCategoryName: cursorOption.postCategoryName, }); } - return await query.orderBy('p.id', 'DESC').getMany(); - } - // cursor + const allPosts: Posts[] = await this.find({ + where: { scope: In([OpenScope.PUBLIC, OpenScope.PROTECTED]) }, + }); + const posts: Posts[] = await queryBuilder.getMany(); + const total: number = await queryBuilder.getCount(); + + return { allPosts, posts, total }; + } async paginateByCustomCursorFriends({ cursorOption, subQuery }) { const queryBuilder = this.createQueryBuilder('p'); @@ -185,7 +199,6 @@ export class PostsRepository extends Repository { .orderBy(`p.${ORDER}`, cursorOption.sort as any) .addOrderBy('p.id', cursorOption.sort as any); - console.log('하이'); const allPosts: Posts[] = await this.find({ where: { scope: In([OpenScope.PUBLIC, OpenScope.PROTECTED]) }, }); diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 057fae7..7a468e5 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -6,9 +6,8 @@ import { } from '@nestjs/common'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource } from 'typeorm'; import { Posts } from './entities/posts.entity'; -import { InjectRepository } from '@nestjs/typeorm'; import { Page } from '../../utils/pages/page'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; @@ -22,8 +21,6 @@ import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dt import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; import { CommentsService } from '../comments/comments.service'; -import { FetchUserPostsDto } from './dtos/fetch-user-posts.dto'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; import { @@ -33,6 +30,7 @@ import { import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; +import { NeighborsService } from '../neighbors/neighbors.service'; @Injectable() export class PostsService { @@ -43,25 +41,12 @@ export class PostsService { private readonly stickerBlocksService: StickerBlocksService, private readonly commentsService: CommentsService, private readonly postsRepository: PostsRepository, - @InjectRepository(Neighbor) - private readonly neighborsRepository: Repository, + private readonly neighborsService: NeighborsService, ) {} async saveImage(file: Express.Multer.File) { return await this.imageUpload(file); } - async getScope({ from_user, to_user }) { - if (from_user === to_user) - return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; - const neighbor = await this.neighborsRepository.findOne({ - where: { from_user, to_user }, - }); - if (neighbor) { - return [OpenScope.PUBLIC, OpenScope.PROTECTED]; - } - return [OpenScope.PUBLIC]; - } - async imageUpload( file: Express.Multer.File, ): Promise { @@ -177,8 +162,8 @@ export class PostsService { kakaoId, page, }: FetchFriendsPostsDto): Promise { - const subQuery = await this.neighborsRepository - .createQueryBuilder('n') + const subQuery = await this.dataSource + .createQueryBuilder(Neighbor, 'n') .select('n.toUserKakaoId') .where(`n.fromUserKakaoId = ${kakaoId}`) .getQuery(); @@ -197,7 +182,7 @@ export class PostsService { async fetchDetail({ kakaoId, id }): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: false }); - const scope = await this.getScope({ + const scope = await this.neighborsService.getScope({ from_user: data.userKakaoId, to_user: kakaoId, }); @@ -214,22 +199,56 @@ export class PostsService { return await this.postsRepository.delete({ user: { kakaoId }, id }); } + //cursor + async fetchUserPosts({ kakaoId, targetKakaoId, - postCategoryName, - }: FetchUserPostsDto): Promise { - const scope = await this.getScope({ + cursorOption, + }): Promise> { + const scope = await this.neighborsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, }); - return await this.postsRepository.fetchUserPosts({ - scope, - userKakaoId: targetKakaoId, - postCategoryName, + const { allPosts, posts, total } = + await this.postsRepository.fetchUserPosts({ + cursorOption, + scope, + userKakaoId: targetKakaoId, + }); + const order = PostsOrderOption[cursorOption.order]; + let hasNextData: boolean = true; + let idByLastDataPerPage: number; + let customCursor: string; + + const takePerPage = cursorOption.take; + const isLastPage = total <= takePerPage; + const lastDataPerPage = posts[posts.length - 1]; + + if (isLastPage) { + hasNextData = false; + idByLastDataPerPage = null; + customCursor = null; + } else { + idByLastDataPerPage = lastDataPerPage.id; + const lastDataPerPageIndexOf = allPosts.findIndex( + (data) => data.id === idByLastDataPerPage, + ); + customCursor = await this.createCustomCursor({ + cursorIndex: lastDataPerPageIndexOf, + order, + }); + } + + const customCursorPageMetaDto = new CustomCursorPageMetaDto({ + customCursorPageOptionsDto: cursorOption, + total, + hasNextData, + customCursor, }); + + return new CustomCursorPageDto(posts, customCursorPageMetaDto); } - //cursor async paginateByCustomCursor({ cursorOption, @@ -289,7 +308,7 @@ export class PostsService { return customCursor[cursorIndex]; } - createDefaultCustomCursorValue( + createDefaultCursor( digitById: number, digitByTargetColumn: number, initialValue: string, @@ -305,8 +324,8 @@ export class PostsService { kakaoId, }): Promise> { console.log(kakaoId); - const subQuery = await this.neighborsRepository - .createQueryBuilder('n') + const subQuery = await this.dataSource + .createQueryBuilder(Neighbor, 'n') .select('n.toUserKakaoId') .where(`n.fromUserKakaoId = ${kakaoId}`) .getQuery(); From 51a8e24dc20ae97a30af0382a0d5dc7d8ac390d8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 21 Apr 2024 18:29:54 +0900 Subject: [PATCH 036/236] refactor: optimize & extract cursorPagination --- src/APIs/posts/posts.repository.ts | 116 +++++--------- src/APIs/posts/posts.service.ts | 147 +++++------------- .../cursor-pages/dtos/cursor-page-meta.dto.ts | 5 - .../interfaces/cursor-page-meta-dto-params.ts | 1 - 4 files changed, 76 insertions(+), 193 deletions(-) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 2ac0be8..d785e60 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -125,19 +125,18 @@ export class PostsRepository extends Repository { .getMany() ); } - async fetchUserPosts({ cursorOption, scope, userKakaoId }) { + + getCursorQuery({ order, sort, take, cursor }) { + order = PostsOrderOption[order]; + const queryBuilder = this.createQueryBuilder('p'); - const ORDER = PostsOrderOption[cursorOption.order]; - const queryByPriceSort = - cursorOption.sort === SortOption.ASC - ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` - : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + const queryByOrderSort = + sort === SortOption.ASC + ? `CONCAT(LPAD(p.${order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; queryBuilder - .take(cursorOption.take) - .where(queryByPriceSort, { - customCursor: cursorOption.customCursor, - }) + .take(take + 1) .innerJoin('p.user', 'user') .innerJoinAndSelect('p.postBackground', 'postBackground') .innerJoinAndSelect('p.postCategory', 'postCategory') @@ -147,100 +146,61 @@ export class PostsRepository extends Repository { 'user.profile_image', 'user.username', ]) - .where('p.userKakaoId = :userKakaoId', { - userKakaoId, + .where('p.isPublished = true') + .andWhere(queryByOrderSort, { + customCursor: cursor, }) - .andWhere('p.scope IN (:scope)', { scope }) - .andWhere('p.isPublished = true') - .orderBy(`p.${ORDER}`, cursorOption.sort as any) - .addOrderBy('p.id', cursorOption.sort as any); + .orderBy(`p.${order}`, sort as any) + .addOrderBy('p.id', sort as any); + + return queryBuilder; + } + + async fetchUserPosts({ cursorOption, scope, userKakaoId }) { + const { order, cursor, take, sort, ...rest } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); + if (cursorOption.postCategoryName) { queryBuilder.andWhere('postCategory.name = :postCategoryName', { postCategoryName: cursorOption.postCategoryName, }); } + queryBuilder + .andWhere('p.userKakaoId = :userKakaoId', { + userKakaoId, + }) + .andWhere('p.scope IN (:scope)', { scope }); - const allPosts: Posts[] = await this.find({ - where: { scope: In([OpenScope.PUBLIC, OpenScope.PROTECTED]) }, - }); const posts: Posts[] = await queryBuilder.getMany(); - const total: number = await queryBuilder.getCount(); - return { allPosts, posts, total }; + return { posts }; } async paginateByCustomCursorFriends({ cursorOption, subQuery }) { - const queryBuilder = this.createQueryBuilder('p'); - const ORDER = PostsOrderOption[cursorOption.order]; - const queryByPriceSort = - cursorOption.sort === SortOption.ASC - ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` - : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + const { order, cursor, take, sort, ...rest } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); queryBuilder - .take(cursorOption.take) - .where(queryByPriceSort, { - customCursor: cursorOption.customCursor, - }) - .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') - .addSelect([ - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.isPublished = true') .andWhere(`p.userKakaoId = any(${subQuery})`) .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], - }) //sql injection 방지를 위해 만드시 enum 거칠 것 - .orderBy(`p.${ORDER}`, cursorOption.sort as any) - .addOrderBy('p.id', cursorOption.sort as any); + }); //sql injection 방지를 위해 만드시 enum 거칠 것 - const allPosts: Posts[] = await this.find({ - where: { scope: In([OpenScope.PUBLIC, OpenScope.PROTECTED]) }, - }); const posts: Posts[] = await queryBuilder.getMany(); - const total: number = await queryBuilder.getCount(); - return { allPosts, posts, total }; + return { posts }; } async paginateByCustomCursor({ cursorOption }) { - const queryBuilder = this.createQueryBuilder('p'); - const ORDER = PostsOrderOption[cursorOption.order]; - const queryByPriceSort = - cursorOption.sort === SortOption.ASC - ? `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` - : `CONCAT(LPAD(p.${ORDER}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + const { order, cursor, take, sort, ...rest } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - queryBuilder - .take(cursorOption.take) - .where(queryByPriceSort, { - customCursor: cursorOption.customCursor, - }) - .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') - .addSelect([ - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.isPublished = true') - .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) - .orderBy(`p.${ORDER}`, cursorOption.sort as any) - .addOrderBy('p.id', cursorOption.sort as any); - - const allPosts: Posts[] = await this.find({ - where: { scope: OpenScope.PUBLIC }, + queryBuilder.andWhere('p.scope IN (:...scopes)', { + scopes: [OpenScope.PUBLIC], }); + const posts: Posts[] = await queryBuilder.getMany(); - const total: number = await queryBuilder.getCount(); - return { allPosts, posts, total }; + return { posts }; } } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 7a468e5..4ca1aa1 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -200,54 +200,54 @@ export class PostsService { } //cursor - - async fetchUserPosts({ - kakaoId, - targetKakaoId, + async createCursorResponse({ cursorOption, + posts, }): Promise> { - const scope = await this.neighborsService.getScope({ - from_user: targetKakaoId, - to_user: kakaoId, - }); - const { allPosts, posts, total } = - await this.postsRepository.fetchUserPosts({ - cursorOption, - scope, - userKakaoId: targetKakaoId, - }); const order = PostsOrderOption[cursorOption.order]; let hasNextData: boolean = true; - let idByLastDataPerPage: number; let customCursor: string; const takePerPage = cursorOption.take; - const isLastPage = total <= takePerPage; - const lastDataPerPage = posts[posts.length - 1]; + console.log(posts.length); + console.log(cursorOption); + const isLastPage = posts.length <= takePerPage; + const responseData = posts.slice(0, takePerPage); + const lastDataPerPage = responseData[responseData.length - 1]; if (isLastPage) { hasNextData = false; - idByLastDataPerPage = null; customCursor = null; } else { - idByLastDataPerPage = lastDataPerPage.id; - const lastDataPerPageIndexOf = allPosts.findIndex( - (data) => data.id === idByLastDataPerPage, - ); customCursor = await this.createCustomCursor({ - cursorIndex: lastDataPerPageIndexOf, + post: lastDataPerPage, order, }); } const customCursorPageMetaDto = new CustomCursorPageMetaDto({ customCursorPageOptionsDto: cursorOption, - total, hasNextData, customCursor, }); - return new CustomCursorPageDto(posts, customCursorPageMetaDto); + return new CustomCursorPageDto(responseData, customCursorPageMetaDto); + } + async fetchUserPosts({ + kakaoId, + targetKakaoId, + cursorOption, + }): Promise> { + const scope = await this.neighborsService.getScope({ + from_user: targetKakaoId, + to_user: kakaoId, + }); + const { posts } = await this.postsRepository.fetchUserPosts({ + cursorOption, + scope, + userKakaoId: targetKakaoId, + }); + return await this.createCursorResponse({ posts, cursorOption }); } async paginateByCustomCursor({ @@ -255,57 +255,19 @@ export class PostsService { kakaoId, }): Promise> { console.log(kakaoId); - const { allPosts, posts, total } = - await this.postsRepository.paginateByCustomCursor({ - cursorOption, - }); - - const order = PostsOrderOption[cursorOption.order]; - let hasNextData: boolean = true; - let idByLastDataPerPage: number; - let customCursor: string; - - const takePerPage = cursorOption.take; - const isLastPage = total <= takePerPage; - const lastDataPerPage = posts[posts.length - 1]; - - if (isLastPage) { - hasNextData = false; - idByLastDataPerPage = null; - customCursor = null; - } else { - idByLastDataPerPage = lastDataPerPage.id; - const lastDataPerPageIndexOf = allPosts.findIndex( - (data) => data.id === idByLastDataPerPage, - ); - customCursor = await this.createCustomCursor({ - cursorIndex: lastDataPerPageIndexOf, - order, - }); - } - - const customCursorPageMetaDto = new CustomCursorPageMetaDto({ - customCursorPageOptionsDto: cursorOption, - total, - hasNextData, - customCursor, + const { posts } = await this.postsRepository.paginateByCustomCursor({ + cursorOption, }); - - return new CustomCursorPageDto(posts, customCursorPageMetaDto); + return await this.createCursorResponse({ posts, cursorOption }); } - async createCustomCursor({ cursorIndex, order }): Promise { - const posts = await this.postsRepository.find(); + async createCustomCursor({ post, order }): Promise { + const id = post.id; + const _order = post[order]; + const customCursor: string = + String(_order).padStart(7, '0') + String(id).padStart(7, '0'); - const customCursor = posts.map((posts) => { - const id = posts.id; - const _order = posts[order]; - const customCursor: string = - String(_order).padStart(7, '0') + String(id).padStart(7, '0'); - return customCursor; - }); - - return customCursor[cursorIndex]; + return customCursor; } createDefaultCursor( @@ -329,43 +291,10 @@ export class PostsService { .select('n.toUserKakaoId') .where(`n.fromUserKakaoId = ${kakaoId}`) .getQuery(); - const { allPosts, posts, total } = - await this.postsRepository.paginateByCustomCursorFriends({ - cursorOption, - subQuery, - }); - - const order = PostsOrderOption[cursorOption.order]; - let hasNextData: boolean = true; - let idByLastDataPerPage: number; - let customCursor: string; - - const takePerPage = cursorOption.take; - const isLastPage = total <= takePerPage; - const lastDataPerPage = posts[posts.length - 1]; - - if (isLastPage) { - hasNextData = false; - idByLastDataPerPage = null; - customCursor = null; - } else { - idByLastDataPerPage = lastDataPerPage.id; - const lastDataPerPageIndexOf = allPosts.findIndex( - (data) => data.id === idByLastDataPerPage, - ); - customCursor = await this.createCustomCursor({ - cursorIndex: lastDataPerPageIndexOf, - order, - }); - } - - const customCursorPageMetaDto = new CustomCursorPageMetaDto({ - customCursorPageOptionsDto: cursorOption, - total, - hasNextData, - customCursor, + const { posts } = await this.postsRepository.paginateByCustomCursorFriends({ + cursorOption, + subQuery, }); - - return new CustomCursorPageDto(posts, customCursorPageMetaDto); + return await this.createCursorResponse({ posts, cursorOption }); } } diff --git a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts index 3c17b04..9b29e4c 100644 --- a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts @@ -2,9 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { CustomCursorPageMetaDtoParameters } from '../interfaces/cursor-page-meta-dto-params'; export class CustomCursorPageMetaDto { - @ApiProperty({ description: '전체 아이템 수', type: Number }) - readonly total: number; - @ApiProperty({ description: '한번에 가져올 아이템 수', type: Number }) readonly take: number; @@ -16,12 +13,10 @@ export class CustomCursorPageMetaDto { constructor({ customCursorPageOptionsDto, - total, hasNextData, customCursor, }: CustomCursorPageMetaDtoParameters) { this.take = customCursorPageOptionsDto.take; - this.total = total; this.hasNextData = hasNextData; this.customCursor = customCursor; } diff --git a/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts b/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts index 85c561a..6091304 100644 --- a/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts +++ b/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts @@ -2,7 +2,6 @@ import { CustomCursorPageOptionsDto } from '../dtos/cursor-page-option.dto'; export interface CustomCursorPageMetaDtoParameters { customCursorPageOptionsDto: CustomCursorPageOptionsDto; - total: number; hasNextData: boolean; customCursor: string; } From 321d30716a5e53474cfc1981c142f45d68c92df2 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 23 Apr 2024 11:02:05 +0900 Subject: [PATCH 037/236] refactor: delete unused params --- src/APIs/posts/posts.repository.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index d785e60..2f56ad5 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -1,4 +1,4 @@ -import { DataSource, In, Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/commons/enums/open-scope.enum'; @@ -157,7 +157,7 @@ export class PostsRepository extends Repository { } async fetchUserPosts({ cursorOption, scope, userKakaoId }) { - const { order, cursor, take, sort, ...rest } = cursorOption; + const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); if (cursorOption.postCategoryName) { @@ -177,7 +177,7 @@ export class PostsRepository extends Repository { } async paginateByCustomCursorFriends({ cursorOption, subQuery }) { - const { order, cursor, take, sort, ...rest } = cursorOption; + const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); queryBuilder @@ -192,7 +192,7 @@ export class PostsRepository extends Repository { } async paginateByCustomCursor({ cursorOption }) { - const { order, cursor, take, sort, ...rest } = cursorOption; + const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); queryBuilder.andWhere('p.scope IN (:...scopes)', { From 27a5e08402569c238881ab788327e8ec3be102f8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 28 Apr 2024 16:40:59 +0900 Subject: [PATCH 038/236] feat: sse connection & notification api ; --- src/APIs/notifications/dtos/emit-not.dto.ts | 11 ++ src/APIs/notifications/dtos/fetch-noti.dto.ts | 33 +++++ .../entities/notification.entity.ts | 6 +- .../notifications/notifications.controller.ts | 95 +++++++++++-- .../notifications/notifications.repository.ts | 32 +++++ .../notifications/notifications.service.ts | 128 +++++++++--------- 6 files changed, 227 insertions(+), 78 deletions(-) create mode 100644 src/APIs/notifications/dtos/emit-not.dto.ts create mode 100644 src/APIs/notifications/dtos/fetch-noti.dto.ts diff --git a/src/APIs/notifications/dtos/emit-not.dto.ts b/src/APIs/notifications/dtos/emit-not.dto.ts new file mode 100644 index 0000000..7bfb58c --- /dev/null +++ b/src/APIs/notifications/dtos/emit-not.dto.ts @@ -0,0 +1,11 @@ +import { OmitType } from '@nestjs/swagger'; +import { Notification } from '../entities/notification.entity'; +export class EmitNotDto extends OmitType(Notification, [ + 'user', + 'id', + 'targetUser', + 'date_created', + 'date_deleted', +]) {} + +export class EmitNotInput extends OmitType(EmitNotDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts new file mode 100644 index 0000000..a5062cb --- /dev/null +++ b/src/APIs/notifications/dtos/fetch-noti.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { DateOption } from 'src/commons/enums/date-option'; +import { Notification } from '../entities/notification.entity'; + +export class FetchNotiInput { + @ApiProperty({ + type: Boolean, + description: '확인 된 알림 조회 여부(true: 조회, false: 스킵)', + default: true, + }) + is_checked: boolean; + + @ApiProperty({ + type: 'enun', + enum: DateOption, + description: '특정 기간 이후 알림 조회, null 일 경우 전체 조회', + required: false, + default: null, + }) + @IsEnum(DateOption) + @IsOptional() + date_created?: DateOption; +} + +export class FetchNotiDto extends FetchNotiInput { + kakaoId: number; +} + +export class FetchNotiResponse extends OmitType(Notification, [ + 'targetUser', + 'user', +]) {} diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 00a568d..d750ac6 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -42,8 +42,8 @@ export class Notification { @Column() type: NotType; - @ApiProperty({ description: '알림 체크 여부', type: Boolean }) - @Column() + @ApiProperty({ description: '알림 체크 여부', type: Boolean, default: false }) + @Column({ default: false }) is_checked: boolean; @ApiProperty({ description: '리다이렉션 url', type: String }) @@ -54,9 +54,11 @@ export class Notification { @Column() message: string; + @ApiProperty({ description: '생성된 날짜', type: Date }) @CreateDateColumn() date_created: Date; + @ApiProperty({ description: '삭제된 날짜', type: Date }) @DeleteDateColumn() date_deleted: Date; } diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index 8fa4f66..c9c4ad4 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -1,7 +1,28 @@ -import { Controller, Get, Param, Req, Sse } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Query, + Req, + Sse, + UseGuards, +} from '@nestjs/common'; import { NotificationsService } from './notifications.service'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiProduces, + ApiTags, +} from '@nestjs/swagger'; import { Request } from 'express'; +import { EmitNotInput } from './dtos/emit-not.dto'; + +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @ApiTags('알림 API') @Controller('nots') @@ -17,19 +38,67 @@ export class NotificationsController { */ @ApiOperation({ summary: '[SSE] kakaoId로 오는 알림을 구독한다.', - description: '[swagger 불가능, postman 권장]', + description: + '[swagger 불가능, postman 권장] sse를 연결한다. 로그인된 유저를 타겟으로 하는 알림이 보내졌을경우 sse를 통해 전달받는다.', + }) + @ApiCookieAuth() + @ApiProduces('text/event-stream') + @UseGuards(AuthGuardV2) + @Sse('sub') + sendClientAlarm( + @Req() req: Request, + // @Param('kakaoId') userKakaoId, + ) { + const userKakaoId = req.user.userId; + const sseStream = this.notificationsService.connectUser(userKakaoId); + return sseStream; + } + + @ApiOperation({ + summary: '알림 조회', + description: + '로그인된 유저들에게 보내진 알림들을 조회한다. query를 통해 알림 조회 옵션 설정. sse 연결 이전 이니셜 데이터 fetch 시 사용', }) - @Sse('sub/:kakaoId') - sendClientAlarm(@Param('kakaoId') kakaoId: number, @Req() req: Request) { - // this.notificationsService.addStream(this.users$, this.observer, userId); - // req.on('close', () => - // this.notificationsService.removeStream(req['user'].id.toString()), - // ); - // return this.notificationsService.sendClientAlarm(+kakaoId); + @ApiOkResponse({ type: [FetchNotiResponse] }) + @Get('init') + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + async fetchNoti( + @Req() req: Request, + @Query() fetchNotiInput: FetchNotiInput, + ): Promise { + const kakaoId = req.user.userId; + return await this.notificationsService.fetch({ + kakaoId, + ...fetchNotiInput, + }); } - @Get('send/:kakaoId') - test(@Param('kakaoId') kakaoId: number) { - // this.notificationsService.emitAlarm(kakaoId); + @ApiOperation({ + summary: '알림 토글', + description: '알림을 읽음 처리한다.', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @ApiOkResponse({ type: FetchNotiResponse }) + @HttpCode(200) + @Post('toggle/:id') + async toggleNoti( + @Req() req: Request, + @Param('id') id: number, + ): Promise { + const targetUserKakaoId = req.user.userId; + return await this.notificationsService.toggle({ id, targetUserKakaoId }); + } + + @ApiOperation({ + summary: 'kakaoId에게 알림 생성', + description: + 'kakaoId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', + }) + @Post('send/:kakaoId') + sendNoti(@Req() req: Request, @Body() body: EmitNotInput) { + const userKakaoId = req.user.userId; + this.notificationsService.emitAlarm({ userKakaoId, ...body }); } } diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index a787402..ad2275b 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -1,10 +1,42 @@ import { Injectable } from '@nestjs/common'; import { Notification } from './entities/notification.entity'; import { DataSource, Repository } from 'typeorm'; +import { EmitNotDto } from './dtos/emit-not.dto'; +import { FetchNotiResponse } from './dtos/fetch-noti.dto'; @Injectable() export class NotificationsRepository extends Repository { constructor(private dataSource: DataSource) { super(Notification, dataSource.createEntityManager()); } + + async createOne(emitNotDto: EmitNotDto) { + return await this.createQueryBuilder() + .insert() + .into(Notification, Object.keys(emitNotDto)) + .values(emitNotDto) + .execute(); + } + + async fetchAll({ + kakaoId, + date_created, + is_checked, + }): Promise { + const query = this.createQueryBuilder('').where('userKakaoId = :kakaoId', { + kakaoId, + }); + if (!is_checked) { + query.andWhere('is_checked = true'); + } + if (date_created) { + query.andWhere('date_created > :date_created', { date_created }); + } + // 열 이름을 별칭으로 지정하여 원래 이름 그대로 출력 + const columnNames = (await this.metadata.columns).map( + (column) => `${column.databaseName} AS ${column.propertyName}`, + ); + query.select(columnNames); + return await query.execute(); + } } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index d49af9f..1a55eae 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -1,76 +1,78 @@ -import { Injectable, MessageEvent } from '@nestjs/common'; +import { BadRequestException, Injectable, MessageEvent } from '@nestjs/common'; import { NotificationsRepository } from './notifications.repository'; -import { Observable, ReplaySubject, Subject, filter, map } from 'rxjs'; +import { Subject, filter, map } from 'rxjs'; +import { Notification } from './entities/notification.entity'; +import { EmitNotDto } from './dtos/emit-not.dto'; +import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; +import { DateOption } from 'src/commons/enums/date-option'; @Injectable() export class NotificationsService { constructor( private readonly notificationsRepository: NotificationsRepository, ) {} + private notis$: Subject = new Subject(); + private observer = this.notis$.asObservable(); - // // [RxJS] Subject 선언 타입은 Users이다 - // private users$: Subject = new Subject(); + async connectUser(targetUserKakaoId) { + console.log('connected: ' + targetUserKakaoId); + const pipe = this.observer.pipe( + filter((noti) => noti.targetUserKakaoId == targetUserKakaoId), + map((noti) => { + return { + data: noti, + } as MessageEvent; + }), + ); + // const data = { id: 1, targetUserKakaoId: 3388766789, }; + // this.users$.next(data); + return pipe; + } - // // 앞서 선언한 Subjcet를 Observable한 객체로 선언 - // private observer = this.users$.asObservable(); + async emitAlarm(emitNotDto: EmitNotDto) { + const executeResult = + await this.notificationsRepository.createOne(emitNotDto); + const id = executeResult.identifiers[0].id; + const data = await this.notificationsRepository.findOne({ where: { id } }); + // next를 통해 이벤트를 생성 + this.notis$.next(data); + } - // //접속한 브라우저의 커넥션을 담을 객체 - // private stream: { - // id: string; - // subject: ReplaySubject; - // observer: Observable; - // }[] = []; + async fetch({ is_checked, kakaoId, date_created }: FetchNotiDto) { + let currentDate = new Date(); - // // 예시 데이터 ( DB 데이터라고 생각하자 ) - // users = [ - // { - // id: 1, - // nickname: 'jewon', - // level: 1, - // }, - // { id: 2, nickname: 'je', level: 2 }, - // { - // id: 3, - // nickname: 'won', - // level: 3, - // }, - // ]; + switch (date_created) { + case DateOption.WEEK: + currentDate.setDate(currentDate.getDate() - 7); + break; + case DateOption.MONTH: + currentDate.setMonth(currentDate.getMonth() - 1); + break; + case DateOption.YEAR: + currentDate.setFullYear(currentDate.getFullYear() - 1); + break; + default: + currentDate = null; + } + return await this.notificationsRepository.fetchAll({ + is_checked, + kakaoId, + date_created: currentDate, + }); + } - // // [RxJS] User의 레벨 변화를 감시할 함수, 레벨업이 진행되면 Observable한 Subject에 next로 push - // // onUserLevelChange(userId: number, nickname: string, level: number) { - // // // this.users$.next({ id: userId, nickname, level }); - // // } - - // //브라우저가 접속할 때 해당 스트림을 담아 둡니다. - // addStream( - // subject: ReplaySubject, - // observer: Observable, - // id: string, - // ): void { - // this.stream.push({ - // id, - // subject, - // observer, - // }); - // } - // // emitAlarm(kakaoId: number) { - // // next를 통해 이벤트를 생성 - // this.users$.next({ kakaoId }); - // } - - // sendClientAlarm(kakaoId: number): Observable { - // // 이벤트 발생시 처리 로직 - // return this.observer.pipe( - // // 유저 필터링 - // filter((user) => user.kakaoId === kakaoId), - // // 데이터 전송 - // map((user) => { - // return { - // data: { - // message: '알람이 발생했습니다.', - // }, - // } as MessageEvent; - // }), - // ); - // } + async toggle({ id, targetUserKakaoId }): Promise { + const updateResult = await this.notificationsRepository.update( + { id, targetUserKakaoId }, + { + is_checked: () => '!is_checked', + }, + ); + if (updateResult.affected < 1) { + throw new BadRequestException('알림을 찾을 수 없거나 권한이 없습니다.'); + } + return await this.notificationsRepository.findOne({ + where: { id, targetUserKakaoId }, + }); + } } From af4ab1ec6cf2031ba12dd520279fb5998cc18b98 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 28 Apr 2024 16:44:22 +0900 Subject: [PATCH 039/236] feat: sse connection & notification api --- src/commons/enums/date-option.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/commons/enums/date-option.ts diff --git a/src/commons/enums/date-option.ts b/src/commons/enums/date-option.ts new file mode 100644 index 0000000..85c7549 --- /dev/null +++ b/src/commons/enums/date-option.ts @@ -0,0 +1,5 @@ +export enum DateOption { + WEEK = 'WEEK', + MONTH = 'MONTH', + YEAR = 'YEAR', +} From a6dac31ac65834e38c6c78d6ff0f593b10dc5fca Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 28 Apr 2024 16:52:08 +0900 Subject: [PATCH 040/236] fix: emit-noti.dto name --- .../{emit-not.dto.ts => emit-noti.dto.ts} | 4 ++-- .../notifications/notifications.controller.ts | 6 ++--- .../notifications/notifications.repository.ts | 8 +++---- .../notifications/notifications.service.ts | 23 ++++++++++++------- 4 files changed, 24 insertions(+), 17 deletions(-) rename src/APIs/notifications/dtos/{emit-not.dto.ts => emit-noti.dto.ts} (57%) diff --git a/src/APIs/notifications/dtos/emit-not.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts similarity index 57% rename from src/APIs/notifications/dtos/emit-not.dto.ts rename to src/APIs/notifications/dtos/emit-noti.dto.ts index 7bfb58c..88b53cd 100644 --- a/src/APIs/notifications/dtos/emit-not.dto.ts +++ b/src/APIs/notifications/dtos/emit-noti.dto.ts @@ -1,6 +1,6 @@ import { OmitType } from '@nestjs/swagger'; import { Notification } from '../entities/notification.entity'; -export class EmitNotDto extends OmitType(Notification, [ +export class EmitNotiDto extends OmitType(Notification, [ 'user', 'id', 'targetUser', @@ -8,4 +8,4 @@ export class EmitNotDto extends OmitType(Notification, [ 'date_deleted', ]) {} -export class EmitNotInput extends OmitType(EmitNotDto, ['userKakaoId']) {} +export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index c9c4ad4..b878901 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -19,7 +19,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { EmitNotInput } from './dtos/emit-not.dto'; +import { EmitNotiInput } from './dtos/emit-noti.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @@ -97,8 +97,8 @@ export class NotificationsController { 'kakaoId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', }) @Post('send/:kakaoId') - sendNoti(@Req() req: Request, @Body() body: EmitNotInput) { + async sendNoti(@Req() req: Request, @Body() body: EmitNotiInput) { const userKakaoId = req.user.userId; - this.notificationsService.emitAlarm({ userKakaoId, ...body }); + return await this.notificationsService.emitAlarm({ userKakaoId, ...body }); } } diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index ad2275b..0aef99e 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Notification } from './entities/notification.entity'; import { DataSource, Repository } from 'typeorm'; -import { EmitNotDto } from './dtos/emit-not.dto'; +import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiResponse } from './dtos/fetch-noti.dto'; @Injectable() @@ -10,11 +10,11 @@ export class NotificationsRepository extends Repository { super(Notification, dataSource.createEntityManager()); } - async createOne(emitNotDto: EmitNotDto) { + async createOne(emitNotiDto: EmitNotiDto) { return await this.createQueryBuilder() .insert() - .into(Notification, Object.keys(emitNotDto)) - .values(emitNotDto) + .into(Notification, Object.keys(emitNotiDto)) + .values(emitNotiDto) .execute(); } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 1a55eae..dbb456c 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable, MessageEvent } from '@nestjs/common'; import { NotificationsRepository } from './notifications.repository'; import { Subject, filter, map } from 'rxjs'; import { Notification } from './entities/notification.entity'; -import { EmitNotDto } from './dtos/emit-not.dto'; +import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; import { DateOption } from 'src/commons/enums/date-option'; @@ -29,13 +29,20 @@ export class NotificationsService { return pipe; } - async emitAlarm(emitNotDto: EmitNotDto) { - const executeResult = - await this.notificationsRepository.createOne(emitNotDto); - const id = executeResult.identifiers[0].id; - const data = await this.notificationsRepository.findOne({ where: { id } }); - // next를 통해 이벤트를 생성 - this.notis$.next(data); + async emitAlarm(emitNotiDto: EmitNotiDto) { + try { + const executeResult = + await this.notificationsRepository.createOne(emitNotiDto); + const id = executeResult.identifiers[0].id; + const data = await this.notificationsRepository.findOne({ + where: { id }, + }); + // next를 통해 이벤트를 생성 + this.notis$.next(data); + return data; + } catch (e) { + throw new BadRequestException('대상을 찾을 수 없습니다.'); + } } async fetch({ is_checked, kakaoId, date_created }: FetchNotiDto) { From e9a943a7425953f3a666a546c5ee996e9f74ed42 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 30 Apr 2024 13:28:12 +0900 Subject: [PATCH 041/236] feat: image resizing --- package-lock.json | 48 ++++++++++++++++++++++-------------- package.json | 1 + src/main.ts | 5 ++-- src/utils/aws/aws.service.ts | 16 +++++++++--- tsconfig.json | 3 ++- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 319a43e..be1663f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "sharp": "^0.32.6", "typeorm": "^0.3.20", "uuid": "^9.0.1" }, @@ -4810,31 +4811,40 @@ "optional": true }, "node_modules/bare-fs": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.3.tgz", - "integrity": "sha512-amG72llr9pstfXOBOHve1WjiuKKAMnebcmMbPWDZ7BCevAoJLpugjuAPRsDINEyjT0a6tbaVx3DctkXIRbLuJw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", "optional": true, "dependencies": { "bare-events": "^2.0.0", "bare-path": "^2.0.0", - "streamx": "^2.13.0" + "bare-stream": "^1.0.0" } }, "node_modules/bare-os": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.1.tgz", - "integrity": "sha512-OwPyHgBBMkhC29Hl3O4/YfxW9n7mdTr2+SsO29XBWKKJsbgj3mnorDB80r5TiCQgQstgE5ga1qNYrpes6NvX2w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", "optional": true }, "node_modules/bare-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", - "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.2.tgz", + "integrity": "sha512-o7KSt4prEphWUHa3QUwCxUI00R86VdjiuxmJK0iNVDHYPGo+HsDaVCnqCmPbf/MiW1ok8F4p3m8RTHlWk8K2ig==", "optional": true, "dependencies": { "bare-os": "^2.1.0" } }, + "node_modules/bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "optional": true, + "dependencies": { + "streamx": "^2.16.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5817,9 +5827,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -8965,9 +8975,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.57.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", - "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", + "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", "dependencies": { "semver": "^7.3.5" }, @@ -10838,9 +10848,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" diff --git a/package.json b/package.json index af0722e..c47c337 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "sharp": "^0.32.6", "typeorm": "^0.3.20", "uuid": "^9.0.1" }, diff --git a/src/main.ts b/src/main.ts index 0b8a52b..84239e2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import * as cookieParser from 'cookie-parser'; +import cookieParser from 'cookie-parser'; import { HttpExceptionFilter } from './commons/filter/http-exception.filter'; import { ValidationPipe } from '@nestjs/common'; -import * as expressBasicAuth from 'express-basic-auth'; +import expressBasicAuth from 'express-basic-auth'; +// import * as expressBasicAuth from 'express-basic-auth'; async function bootstrap() { const app = await NestFactory.create(AppModule); diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index dffe46a..326a000 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -6,7 +6,7 @@ import { PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; -import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; +import sharp from 'sharp'; @Injectable() export class AwsService { @@ -24,10 +24,12 @@ export class AwsService { } async imageUploadToS3Buffer(fileName: string, file: Buffer, ext: string) { + const resizedImageBuffer = await this.resizeImage(file, 800); + const command = new PutObjectCommand({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 Key: fileName, // 업로드될 파일의 이름 - Body: file, // 업로드할 파일 + Body: resizedImageBuffer, // 업로드할 파일 ACL: 'public-read', // 파일 접근 권한 ContentType: `image/${ext}`, // 파일 타입, }); @@ -56,11 +58,12 @@ export class AwsService { file: Express.Multer.File, // 업로드할 파일 ext: string, // 파일 확장자 ) { + const resizedImageBuffer = await this.resizeImage(file.buffer, 800); // AWS S3에 이미지 업로드 명령을 생성합니다. 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정합니다. const command = new PutObjectCommand({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 Key: fileName, // 업로드될 파일의 이름 - Body: file.buffer, // 업로드할 파일 + Body: resizedImageBuffer, // 업로드할 파일 ACL: 'public-read', // 파일 접근 권한 ContentType: `image/${ext}`, // 파일 타입 }); @@ -71,4 +74,11 @@ export class AwsService { // 업로드된 이미지의 URL을 반환합니다. return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; } + + async resizeImage(buffer: Buffer, width: number) { + const resizedImageBuffer = await sharp(buffer, { failOnError: false }) + .resize({ width }) + .toBuffer(); + return resizedImageBuffer; + } } diff --git a/tsconfig.json b/tsconfig.json index 95f5641..5485d9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true } } From fd5ef7f71c1a9be538bb7c8a736ca16a20c33772 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 6 May 2024 16:43:07 +0900 Subject: [PATCH 042/236] feat: create report api --- id_rsa_git | 38 +++++++++++++++++++ id_rsa_git.pub | 1 + src/APIs/reports/dtos/create-report.dto.ts | 18 +++++++++ src/APIs/reports/entities/report.entity.ts | 25 +++++++++--- src/APIs/reports/reports.controller.ts | 36 +++++++++++++++++- src/APIs/reports/reports.module.ts | 3 +- src/APIs/reports/reports.service.ts | 12 ++++++ .../stickerCategories.module.ts | 1 - src/APIs/users/users.service.ts | 14 ++++++- 9 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 id_rsa_git create mode 100644 id_rsa_git.pub create mode 100644 src/APIs/reports/dtos/create-report.dto.ts diff --git a/id_rsa_git b/id_rsa_git new file mode 100644 index 0000000..858064f --- /dev/null +++ b/id_rsa_git @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEAo869mpz5FuLD29LPcLVPhbGi7Nzp5FvGo8SAqGzFX7zLJ7vbHequ +xFKvqTW/4pPi4ucLbUD5eDB7e7YbT9/FXFsj9hFvNrLUjaoSgmHGjAxPDPrnDtEgzJWxcA +3zD70At1FEb2AVKwlMXFdYpluibFhuN8NgakiqT+oufmdXTNPWhnWpUtqVAk2bLYfTHfDH +0nd47OMp7tppPiW93RpXSVOdlYk/NPgAF03IZYcf0LG8YOaKyMgHPgEjqR09rJClvFdJkm +Bf9K0GaKbD6Ug69PhPYQDOo8I2j8dNLyLDmCJbra4qeGA4YYMNdFkTQP9YaRBXtF3wG5IZ +ApWHlDA03wXsZG8XzBeK0HCiwZZiH3ea4Eq2srFhpFXeV6rCvMZwfJKKzO5AdvYL+MO8T6 +gdQk4xBuuRHbb0VS++QEj65KD2aQZLcGx1PpV3lapqt7ItBQi2yxLtyu/MAZXCFPf+n+8I +j8AvtEggWDTrluYe1CgVQFBlpsYrjkqkTyITc4EPAAAFiF9bPctfWz3LAAAAB3NzaC1yc2 +EAAAGBAKPOvZqc+Rbiw9vSz3C1T4Wxouzc6eRbxqPEgKhsxV+8yye72x3qrsRSr6k1v+KT +4uLnC21A+Xgwe3u2G0/fxVxbI/YRbzay1I2qEoJhxowMTwz65w7RIMyVsXAN8w+9ALdRRG +9gFSsJTFxXWKZbomxYbjfDYGpIqk/qLn5nV0zT1oZ1qVLalQJNmy2H0x3wx9J3eOzjKe7a +aT4lvd0aV0lTnZWJPzT4ABdNyGWHH9CxvGDmisjIBz4BI6kdPayQpbxXSZJgX/StBmimw+ +lIOvT4T2EAzqPCNo/HTS8iw5giW62uKnhgOGGDDXRZE0D/WGkQV7Rd8BuSGQKVh5QwNN8F +7GRvF8wXitBwosGWYh93muBKtrKxYaRV3leqwrzGcHySiszuQHb2C/jDvE+oHUJOMQbrkR +229FUvvkBI+uSg9mkGS3BsdT6Vd5WqareyLQUItssS7crvzAGVwhT3/p/vCI/AL7RIIFg0 +65bmHtQoFUBQZabGK45KpE8iE3OBDwAAAAMBAAEAAAGATbQUXPN5dVG8dtpZbK2VO2Y4Uw +O4L4sZfzYHkd2HAxMbi42hM1/P53ERwsKsc16Tke7njLv1mv3klZqc+ha8GENjm6ZJizjp +ewniHdcjx+tO1GlwkabCWEnqEa2MTzrozAzQ2cRKRk/y2RrWApQVSC/qmKklY0V1BNOhmn +SLBPa4HLBT0em+JYmKwt5bVyiQoVXrFvPrQFJ6+fANUITeQvpXFkg0o3vBD0zmcsLWZLjr +E0xJKVU5mkAQGni0eNdkBM+45dkobRGeYxNIDZ4K6/R/k6NR1ZfLYLdm/pg0JnV0Dd2pXf +du5bqUe0DCQ/Xp7/WaMa0tKiKKP/duuFfeg2bUBsJXCdnCpjN4/ynFWEcs5ZT2mb/rSt1b +pGfRJczayb++iQWN0I7MPKOJmxS1MgDJM4wG33ei9I8xY53SZtu1DAUAy5NgaDMOSzTpCW +p0Sp3W0Lw15o7epAv8z7M5vhp8lxX8Hmn3BgU7DQHG/c2v3ac/di7foAOeRepze6+BAAAA +wAxCggGXINkTXdBMiHkZRjlwHway+NP0o7xMuXckHDW3bFOX1/eDRFrYJjjGy408bhjn69 +K+0uRLz/a+g9D/VRwRz2Jnhmd7lhfihBgoXjfewe+qE/WZs/ZlwLV1Hb6QtZoDD5z1CF58 +T8yGkqfN1tt/+DaoQ47n04F6MTATRJgkUONhge3tkNOzkHS7UIHqkUZs/oFqDjnX8SJQSS +3cbuOecAKR2FCNRz+I7A3pHUN8tjz6Gue6yGegGzMieH+7AwAAAMEA0VW05JjyyyDcw+9N +aYQc3ZWDhpEbpmQ5gJA87/W2O5Ac9+kkuz4Na9nRF5Zzl+BhdTB23W7qMGUhWqBXZk6Hu2 +tV1NgcYXrFX3EGUPya6KysfZgKJUp2wnAc2siypeCB4Y6dF/GaWe/hUT74R3JBBy2oAQDj +tR7QSsDsYMmGO2NO+pVnmio3qFIfOjbmFjF1tdx7uYsHEtm58TpZ+nbx8h41czPZmQ7I6K +l+KwwB4dpzBReVWj0Bja5AYO7JDcalAAAAwQDIUuR5+3veGdY0LyL1gT9X0aOZ2j5x5Bts +vP6sigxzSSqGMSI74A14SLUAfRPUZqgdYZLH2ljdNkkxGdOwZYGy6MoY03nNBtzk5Hnl+L +yGZsE4Y6cHfDhltoiv2AD1Dbeb+49MVvn9sbzwpP6K7N2gAI7qmbGpbhdKti6a/eQWWXzx +UpzQ9EZRuNEw3jRn5ajGQwM3UdO5O5PzSI8Bu93DY4L90/wU2YLIgpdNDCWlUd57U/qdDU +Q7FVsGPVu9DqMAAAASb3JhbmdubHBAZ21haWwuY29tAQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/id_rsa_git.pub b/id_rsa_git.pub new file mode 100644 index 0000000..79acfe4 --- /dev/null +++ b/id_rsa_git.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCjzr2anPkW4sPb0s9wtU+FsaLs3OnkW8ajxICobMVfvMsnu9sd6q7EUq+pNb/ik+Li5wttQPl4MHt7thtP38VcWyP2EW82stSNqhKCYcaMDE8M+ucO0SDMlbFwDfMPvQC3UURvYBUrCUxcV1imW6JsWG43w2BqSKpP6i5+Z1dM09aGdalS2pUCTZsth9Md8MfSd3js4ynu2mk+Jb3dGldJU52ViT80+AAXTchlhx/Qsbxg5orIyAc+ASOpHT2skKW8V0mSYF/0rQZopsPpSDr0+E9hAM6jwjaPx00vIsOYIlutrip4YDhhgw10WRNA/1hpEFe0XfAbkhkClYeUMDTfBexkbxfMF4rQcKLBlmIfd5rgSraysWGkVd5XqsK8xnB8korM7kB29gv4w7xPqB1CTjEG65EdtvRVL75ASPrkoPZpBktwbHU+lXeVqmq3si0FCLbLEu3K78wBlcIU9/6f7wiPwC+0SCBYNOuW5h7UKBVAUGWmxiuOSqRPIhNzgQ8= orangnlp@gmail.com diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts new file mode 100644 index 0000000..8d27992 --- /dev/null +++ b/src/APIs/reports/dtos/create-report.dto.ts @@ -0,0 +1,18 @@ +import { OmitType } from '@nestjs/swagger'; +import { Report } from '../entities/report.entity'; + +export class CreateReportDto extends OmitType(Report, [ + 'id', + 'user', + 'targetUser', + 'date_created', +]) {} + +export class CreateReportInput extends OmitType(CreateReportDto, [ + 'userKakaoId', +]) {} + +export class CreateReportResponse extends OmitType(Report, [ + 'user', + 'targetUser', +]) {} diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 51ed381..ebbc215 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { User } from 'src/APIs/users/entities/user.entity'; import { ReportType } from 'src/commons/enums/report-type.enum'; import { @@ -12,18 +13,19 @@ import { @Entity() export class Report { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - title: string; + @ApiProperty({ type: Number, description: 'PK: A_I_' }) + @PrimaryGeneratedColumn() + id: number; + @ApiProperty({ type: String, description: '신고 내용' }) @Column() content: string; + @ApiProperty({ type: Date, description: '생성된 날짜' }) @CreateDateColumn() - date_created: string; + date_created: Date; + @ApiProperty({ type: Number, description: '신고한 유저 id' }) @Column() @RelationId((report: Report) => report.user) userKakaoId: number; @@ -32,9 +34,20 @@ export class Report { @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; + @ApiProperty({ type: Number, description: '신고당한 유저 id' }) + @Column() + @RelationId((report: Report) => report.targetUser) + targetUserKakaoId; + + @JoinColumn() + @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + targetUser: User; + + @ApiProperty({ type: 'enum', enum: ReportType, description: '신고 유형' }) @Column() type: ReportType; + @ApiProperty({ type: String, description: '신고가 발생한 게시물의 url' }) @Column() url: string; } diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index bfac629..90beaf3 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -1,7 +1,41 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + HttpCode, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { ReportsService } from './reports.service'; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOperation, +} from '@nestjs/swagger'; +import { + CreateReportInput, + CreateReportResponse, +} from './dtos/create-report.dto'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { Request } from 'express'; @Controller('reports') export class ReportsController { constructor(private readonly reportsService: ReportsService) {} + + @ApiOperation({ + summary: '게시물 || 댓글 신고', + }) + @ApiCookieAuth() + @ApiCreatedResponse({ type: CreateReportResponse }) + @UseGuards(AuthGuardV2) + @Post() + @HttpCode(201) + async report( + @Req() req: Request, + @Body() body: CreateReportInput, + ): Promise { + const userKakaoId = req.user.userId; + return await this.reportsService.create({ userKakaoId, ...body }); + } } diff --git a/src/APIs/reports/reports.module.ts b/src/APIs/reports/reports.module.ts index aac5582..236010b 100644 --- a/src/APIs/reports/reports.module.ts +++ b/src/APIs/reports/reports.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; import { ReportsController } from './reports.controller'; import { ReportsService } from './reports.service'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Report])], + imports: [TypeOrmModule.forFeature([Report]), UsersModule], controllers: [ReportsController], providers: [ReportsService], }) diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 17b81e9..b7d37a9 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -2,11 +2,23 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; import { Repository } from 'typeorm'; +import { + CreateReportDto, + CreateReportResponse, +} from './dtos/create-report.dto'; +import { UsersService } from '../users/users.service'; @Injectable() export class ReportsService { constructor( @InjectRepository(Report) private readonly reportsRepository: Repository, + private readonly usersService: UsersService, ) {} + + async create(dto: CreateReportDto): Promise { + this.usersService.existCheck({ kakaoId: dto.targetUserKakaoId }); + const result = await this.reportsRepository.save(dto); + return result; + } } diff --git a/src/APIs/stickerCategories/stickerCategories.module.ts b/src/APIs/stickerCategories/stickerCategories.module.ts index 95ce614..05d4a6e 100644 --- a/src/APIs/stickerCategories/stickerCategories.module.ts +++ b/src/APIs/stickerCategories/stickerCategories.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { StickerCategory } from './entities/stickerCategory.entity'; import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; import { StickerCategoriesService } from './stickerCategories.service'; diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 1922255..867b142 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -1,4 +1,8 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ILike, Repository } from 'typeorm'; import { User } from './entities/user.entity'; @@ -32,6 +36,14 @@ export class UsersService { }); if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); } + + async existCheck({ kakaoId }) { + const user = await this.findUserByKakaoId({ + kakaoId, + }); + if (!user) throw new BadRequestException('존재하지 않는 유저 입니다.'); + } + async create({ kakaoId }: IUsersServiceCreate) { const userTempName = 'user' + this.utilsService.getUUID().substring(0, 8); const result = await this.usersRepository.save({ From d8fb714a723d0dbc89b4698d9e5f15fef00f2318 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 6 May 2024 16:53:31 +0900 Subject: [PATCH 043/236] feat: fetch reports API --- src/APIs/reports/dtos/create-report.dto.ts | 5 ----- src/APIs/reports/dtos/fetch-report.dto.ts | 7 ++++++ src/APIs/reports/reports.controller.ts | 26 +++++++++++++++++----- src/APIs/reports/reports.service.ts | 16 ++++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 src/APIs/reports/dtos/fetch-report.dto.ts diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts index 8d27992..b8ff697 100644 --- a/src/APIs/reports/dtos/create-report.dto.ts +++ b/src/APIs/reports/dtos/create-report.dto.ts @@ -11,8 +11,3 @@ export class CreateReportDto extends OmitType(Report, [ export class CreateReportInput extends OmitType(CreateReportDto, [ 'userKakaoId', ]) {} - -export class CreateReportResponse extends OmitType(Report, [ - 'user', - 'targetUser', -]) {} diff --git a/src/APIs/reports/dtos/fetch-report.dto.ts b/src/APIs/reports/dtos/fetch-report.dto.ts new file mode 100644 index 0000000..9616d5c --- /dev/null +++ b/src/APIs/reports/dtos/fetch-report.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { Report } from '../entities/report.entity'; + +export class FetchReportResponse extends OmitType(Report, [ + 'user', + 'targetUser', +]) {} diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 90beaf3..495fe07 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Get, HttpCode, Post, Req, @@ -10,15 +11,16 @@ import { ReportsService } from './reports.service'; import { ApiCookieAuth, ApiCreatedResponse, + ApiOkResponse, ApiOperation, + ApiTags, } from '@nestjs/swagger'; -import { - CreateReportInput, - CreateReportResponse, -} from './dtos/create-report.dto'; +import { CreateReportInput } from './dtos/create-report.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { Request } from 'express'; +import { FetchReportResponse } from './dtos/fetch-report.dto'; +@ApiTags('신고 API') @Controller('reports') export class ReportsController { constructor(private readonly reportsService: ReportsService) {} @@ -27,15 +29,27 @@ export class ReportsController { summary: '게시물 || 댓글 신고', }) @ApiCookieAuth() - @ApiCreatedResponse({ type: CreateReportResponse }) + @ApiCreatedResponse({ type: FetchReportResponse }) @UseGuards(AuthGuardV2) @Post() @HttpCode(201) async report( @Req() req: Request, @Body() body: CreateReportInput, - ): Promise { + ): Promise { const userKakaoId = req.user.userId; return await this.reportsService.create({ userKakaoId, ...body }); } + + @ApiOperation({ + summary: '[어드민용] 신고 내역 조회', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: [FetchReportResponse] }) + @UseGuards(AuthGuardV2) + @Get() + async fetchAll(@Req() req: Request): Promise { + const kakaoId = req.user.userId; + return await this.reportsService.fetchAll({ kakaoId }); + } } diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index b7d37a9..34cd0ba 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -2,11 +2,9 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; import { Repository } from 'typeorm'; -import { - CreateReportDto, - CreateReportResponse, -} from './dtos/create-report.dto'; +import { CreateReportDto } from './dtos/create-report.dto'; import { UsersService } from '../users/users.service'; +import { FetchReportResponse } from './dtos/fetch-report.dto'; @Injectable() export class ReportsService { @@ -16,9 +14,15 @@ export class ReportsService { private readonly usersService: UsersService, ) {} - async create(dto: CreateReportDto): Promise { - this.usersService.existCheck({ kakaoId: dto.targetUserKakaoId }); + async create(dto: CreateReportDto): Promise { + await this.usersService.existCheck({ kakaoId: dto.targetUserKakaoId }); const result = await this.reportsRepository.save(dto); return result; } + + async fetchAll({ kakaoId }): Promise { + await this.usersService.adminCheck({ kakaoId }); + const result = await this.reportsRepository.find(); + return result; + } } From 710d2479ac2e31fdd76713c7075abd6a57e9332d Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 6 May 2024 18:37:32 +0900 Subject: [PATCH 044/236] feat: add FK & transaction on report API --- src/APIs/likes/likes.service.ts | 8 --- src/APIs/posts/dtos/fetch-posts.dto.ts | 1 + src/APIs/posts/entities/posts.entity.ts | 4 ++ src/APIs/reports/dtos/create-report.dto.ts | 11 +++- src/APIs/reports/dtos/fetch-report.dto.ts | 2 + src/APIs/reports/entities/report.entity.ts | 38 ++++++++++++++ src/APIs/reports/reports.service.ts | 58 ++++++++++++++++++++-- src/APIs/users/users.service.ts | 8 ++- src/app.module.ts | 2 +- src/commons/enums/report-target.enum.ts | 4 ++ 10 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 src/commons/enums/report-target.enum.ts diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index ea41554..fb9cec9 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -34,14 +34,6 @@ export class LikesService { like_count: () => 'like_count -1', }); postData.like_count -= 1; - // // 좋아요 카운트 락걸고 쿼리!!! xx - // const result = await queryRunner.manager.save(Posts, { - // lock: { mode: 'pessimistic_write' }, - // ...postData, - // like_count: postData.like_count - 1, - // }); - // await queryRunner.commitTransaction(); - // return result; } else { await queryRunner.manager.save(Likes, { user: kakaoId, diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index 2adee51..5fa6baf 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -53,6 +53,7 @@ export const FETCH_POST_OPTION = { main_image_url: true, isPublished: true, like_count: true, + blame_count: true, allow_comment: true, scope: true, date_created: true, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index f8af54b..4f4a6e7 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -61,6 +61,10 @@ export class Posts { @Column({ default: 0 }) comment_count: number; + @ApiProperty({ description: '신고수 카운트', type: Number }) + @Column({ default: 0 }) + blame_count: number; + @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) @Column({ default: true }) allow_comment: boolean; diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts index b8ff697..32f12ef 100644 --- a/src/APIs/reports/dtos/create-report.dto.ts +++ b/src/APIs/reports/dtos/create-report.dto.ts @@ -1,12 +1,19 @@ -import { OmitType } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Report } from '../entities/report.entity'; export class CreateReportDto extends OmitType(Report, [ 'id', 'user', 'targetUser', + 'post', + 'postId', + 'comment', + 'commentId', 'date_created', -]) {} +]) { + @ApiProperty({ type: Number, description: '신고할 게시글/댓글의 아이디' }) + targetId: number; +} export class CreateReportInput extends OmitType(CreateReportDto, [ 'userKakaoId', diff --git a/src/APIs/reports/dtos/fetch-report.dto.ts b/src/APIs/reports/dtos/fetch-report.dto.ts index 9616d5c..b6b28e1 100644 --- a/src/APIs/reports/dtos/fetch-report.dto.ts +++ b/src/APIs/reports/dtos/fetch-report.dto.ts @@ -4,4 +4,6 @@ import { Report } from '../entities/report.entity'; export class FetchReportResponse extends OmitType(Report, [ 'user', 'targetUser', + 'post', + 'comment', ]) {} diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index ebbc215..5ad6d4b 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -1,5 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; +import { Comment } from 'src/APIs/comments/entities/comment.entity'; +import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { ReportTarget } from 'src/commons/enums/report-target.enum'; import { ReportType } from 'src/commons/enums/report-type.enum'; import { Column, @@ -43,11 +47,45 @@ export class Report { @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; + @IsEnum(ReportType) @ApiProperty({ type: 'enum', enum: ReportType, description: '신고 유형' }) @Column() type: ReportType; + @IsEnum(ReportTarget) + @ApiProperty({ type: 'enum', enum: ReportTarget, description: '신고 대상' }) + @Column() + target: ReportTarget; + @ApiProperty({ type: String, description: '신고가 발생한 게시물의 url' }) @Column() url: string; + + @ApiProperty({ type: Number, description: '신고당한 게시글 id' }) + @Column({ nullable: true }) + @RelationId((report: Report) => report.post) + postId: number; + + @ApiProperty({ type: Posts, description: '신고된 게시물', nullable: true }) + @JoinColumn() + @ManyToOne(() => Posts, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }) // 게시물을 참조하는 경우 + post: Posts; + + @ApiProperty({ type: Number, description: '신고당한 댓글 id' }) + @Column({ nullable: true }) + @RelationId((report: Report) => report.comment) + commentId: number; + + @ApiProperty({ type: Comment, description: '신고된 댓글', nullable: true }) + @JoinColumn() + @ManyToOne(() => Comment, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }) // 댓글을 참조하는 경우 + comment: Comment; } diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 34cd0ba..75654b6 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -1,10 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; -import { Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { CreateReportDto } from './dtos/create-report.dto'; import { UsersService } from '../users/users.service'; import { FetchReportResponse } from './dtos/fetch-report.dto'; +import { ReportTarget } from 'src/commons/enums/report-target.enum'; +import { Posts } from '../posts/entities/posts.entity'; +import { Comment } from '../comments/entities/comment.entity'; @Injectable() export class ReportsService { @@ -12,12 +15,59 @@ export class ReportsService { @InjectRepository(Report) private readonly reportsRepository: Repository, private readonly usersService: UsersService, + private readonly dataSource: DataSource, ) {} async create(dto: CreateReportDto): Promise { await this.usersService.existCheck({ kakaoId: dto.targetUserKakaoId }); - const result = await this.reportsRepository.save(dto); - return result; + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + const { targetId, ...rest } = dto; + try { + let data; + switch (dto.target) { + case ReportTarget.POSTS: + const postData = await queryRunner.manager.findOne(Posts, { + where: { id: targetId }, + }); + if (!postData) + throw new BadRequestException('게시글이 존재하지 않습니다.'); + await queryRunner.manager.update(Posts, postData.id, { + blame_count: () => 'blame_count +1', + }); + data = await queryRunner.manager.save(Report, { + ...rest, + postId: targetId, + }); + break; + + case ReportTarget.COMMENTS: + const commentData = await queryRunner.manager.findOne(Comment, { + where: { id: targetId }, + }); + if (!commentData) + throw new BadRequestException('댓글이 존재하지 않습니다.'); + await queryRunner.manager.update(Comment, commentData.id, { + blame_count: () => 'blame_count +1', + }); + data = await queryRunner.manager.save(Report, { + ...rest, + commentId: targetId, + }); + break; + + default: + throw new BadRequestException('잘못된 target입니다.'); + } // end of switch + await queryRunner.commitTransaction(); + return data; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } } async fetchAll({ kakaoId }): Promise { diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 867b142..86a3cf2 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ConflictException, Injectable, UnauthorizedException, } from '@nestjs/common'; @@ -91,7 +92,12 @@ export class UsersService { if (username) { user.username = username; } - return await this.usersRepository.save(user); + try { + const data = await this.usersRepository.save(user); + return data; + } catch (e) { + throw new ConflictException('UK: username이 중복됩니다.'); + } } async findUsersByName({ username }): Promise { diff --git a/src/app.module.ts b/src/app.module.ts index e5b674f..9bb8cdd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,7 +18,7 @@ import { NotificationsModule } from './APIs/notifications/notifications.module'; import { AnnouncementsModule } from './APIs/announcements/announcements.module'; import { ReportsModule } from './APIs/reports/reports.module'; import { AuthTokenMiddleware } from './commons/middlewares/auth-token.middleware'; -import { JwtModule, JwtService } from '@nestjs/jwt'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ diff --git a/src/commons/enums/report-target.enum.ts b/src/commons/enums/report-target.enum.ts new file mode 100644 index 0000000..0ebc712 --- /dev/null +++ b/src/commons/enums/report-target.enum.ts @@ -0,0 +1,4 @@ +export enum ReportTarget { + POSTS = 'POSTS', + COMMENTS = 'COMMENTS', +} From dd886a6d5c0bd4146e8e6d6ff82466c2004b78a3 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 6 May 2024 18:44:25 +0900 Subject: [PATCH 045/236] feat: block duplicated report request from single user --- id_rsa_git | 38 ----------------------------- id_rsa_git.pub | 1 - src/APIs/reports/reports.service.ts | 20 ++++++++++++++- 3 files changed, 19 insertions(+), 40 deletions(-) delete mode 100644 id_rsa_git delete mode 100644 id_rsa_git.pub diff --git a/id_rsa_git b/id_rsa_git deleted file mode 100644 index 858064f..0000000 --- a/id_rsa_git +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn -NhAAAAAwEAAQAAAYEAo869mpz5FuLD29LPcLVPhbGi7Nzp5FvGo8SAqGzFX7zLJ7vbHequ -xFKvqTW/4pPi4ucLbUD5eDB7e7YbT9/FXFsj9hFvNrLUjaoSgmHGjAxPDPrnDtEgzJWxcA -3zD70At1FEb2AVKwlMXFdYpluibFhuN8NgakiqT+oufmdXTNPWhnWpUtqVAk2bLYfTHfDH -0nd47OMp7tppPiW93RpXSVOdlYk/NPgAF03IZYcf0LG8YOaKyMgHPgEjqR09rJClvFdJkm -Bf9K0GaKbD6Ug69PhPYQDOo8I2j8dNLyLDmCJbra4qeGA4YYMNdFkTQP9YaRBXtF3wG5IZ -ApWHlDA03wXsZG8XzBeK0HCiwZZiH3ea4Eq2srFhpFXeV6rCvMZwfJKKzO5AdvYL+MO8T6 -gdQk4xBuuRHbb0VS++QEj65KD2aQZLcGx1PpV3lapqt7ItBQi2yxLtyu/MAZXCFPf+n+8I -j8AvtEggWDTrluYe1CgVQFBlpsYrjkqkTyITc4EPAAAFiF9bPctfWz3LAAAAB3NzaC1yc2 -EAAAGBAKPOvZqc+Rbiw9vSz3C1T4Wxouzc6eRbxqPEgKhsxV+8yye72x3qrsRSr6k1v+KT -4uLnC21A+Xgwe3u2G0/fxVxbI/YRbzay1I2qEoJhxowMTwz65w7RIMyVsXAN8w+9ALdRRG -9gFSsJTFxXWKZbomxYbjfDYGpIqk/qLn5nV0zT1oZ1qVLalQJNmy2H0x3wx9J3eOzjKe7a -aT4lvd0aV0lTnZWJPzT4ABdNyGWHH9CxvGDmisjIBz4BI6kdPayQpbxXSZJgX/StBmimw+ -lIOvT4T2EAzqPCNo/HTS8iw5giW62uKnhgOGGDDXRZE0D/WGkQV7Rd8BuSGQKVh5QwNN8F -7GRvF8wXitBwosGWYh93muBKtrKxYaRV3leqwrzGcHySiszuQHb2C/jDvE+oHUJOMQbrkR -229FUvvkBI+uSg9mkGS3BsdT6Vd5WqareyLQUItssS7crvzAGVwhT3/p/vCI/AL7RIIFg0 -65bmHtQoFUBQZabGK45KpE8iE3OBDwAAAAMBAAEAAAGATbQUXPN5dVG8dtpZbK2VO2Y4Uw -O4L4sZfzYHkd2HAxMbi42hM1/P53ERwsKsc16Tke7njLv1mv3klZqc+ha8GENjm6ZJizjp -ewniHdcjx+tO1GlwkabCWEnqEa2MTzrozAzQ2cRKRk/y2RrWApQVSC/qmKklY0V1BNOhmn -SLBPa4HLBT0em+JYmKwt5bVyiQoVXrFvPrQFJ6+fANUITeQvpXFkg0o3vBD0zmcsLWZLjr -E0xJKVU5mkAQGni0eNdkBM+45dkobRGeYxNIDZ4K6/R/k6NR1ZfLYLdm/pg0JnV0Dd2pXf -du5bqUe0DCQ/Xp7/WaMa0tKiKKP/duuFfeg2bUBsJXCdnCpjN4/ynFWEcs5ZT2mb/rSt1b -pGfRJczayb++iQWN0I7MPKOJmxS1MgDJM4wG33ei9I8xY53SZtu1DAUAy5NgaDMOSzTpCW -p0Sp3W0Lw15o7epAv8z7M5vhp8lxX8Hmn3BgU7DQHG/c2v3ac/di7foAOeRepze6+BAAAA -wAxCggGXINkTXdBMiHkZRjlwHway+NP0o7xMuXckHDW3bFOX1/eDRFrYJjjGy408bhjn69 -K+0uRLz/a+g9D/VRwRz2Jnhmd7lhfihBgoXjfewe+qE/WZs/ZlwLV1Hb6QtZoDD5z1CF58 -T8yGkqfN1tt/+DaoQ47n04F6MTATRJgkUONhge3tkNOzkHS7UIHqkUZs/oFqDjnX8SJQSS -3cbuOecAKR2FCNRz+I7A3pHUN8tjz6Gue6yGegGzMieH+7AwAAAMEA0VW05JjyyyDcw+9N -aYQc3ZWDhpEbpmQ5gJA87/W2O5Ac9+kkuz4Na9nRF5Zzl+BhdTB23W7qMGUhWqBXZk6Hu2 -tV1NgcYXrFX3EGUPya6KysfZgKJUp2wnAc2siypeCB4Y6dF/GaWe/hUT74R3JBBy2oAQDj -tR7QSsDsYMmGO2NO+pVnmio3qFIfOjbmFjF1tdx7uYsHEtm58TpZ+nbx8h41czPZmQ7I6K -l+KwwB4dpzBReVWj0Bja5AYO7JDcalAAAAwQDIUuR5+3veGdY0LyL1gT9X0aOZ2j5x5Bts -vP6sigxzSSqGMSI74A14SLUAfRPUZqgdYZLH2ljdNkkxGdOwZYGy6MoY03nNBtzk5Hnl+L -yGZsE4Y6cHfDhltoiv2AD1Dbeb+49MVvn9sbzwpP6K7N2gAI7qmbGpbhdKti6a/eQWWXzx -UpzQ9EZRuNEw3jRn5ajGQwM3UdO5O5PzSI8Bu93DY4L90/wU2YLIgpdNDCWlUd57U/qdDU -Q7FVsGPVu9DqMAAAASb3JhbmdubHBAZ21haWwuY29tAQ== ------END OPENSSH PRIVATE KEY----- diff --git a/id_rsa_git.pub b/id_rsa_git.pub deleted file mode 100644 index 79acfe4..0000000 --- a/id_rsa_git.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCjzr2anPkW4sPb0s9wtU+FsaLs3OnkW8ajxICobMVfvMsnu9sd6q7EUq+pNb/ik+Li5wttQPl4MHt7thtP38VcWyP2EW82stSNqhKCYcaMDE8M+ucO0SDMlbFwDfMPvQC3UURvYBUrCUxcV1imW6JsWG43w2BqSKpP6i5+Z1dM09aGdalS2pUCTZsth9Md8MfSd3js4ynu2mk+Jb3dGldJU52ViT80+AAXTchlhx/Qsbxg5orIyAc+ASOpHT2skKW8V0mSYF/0rQZopsPpSDr0+E9hAM6jwjaPx00vIsOYIlutrip4YDhhgw10WRNA/1hpEFe0XfAbkhkClYeUMDTfBexkbxfMF4rQcKLBlmIfd5rgSraysWGkVd5XqsK8xnB8korM7kB29gv4w7xPqB1CTjEG65EdtvRVL75ASPrkoPZpBktwbHU+lXeVqmq3si0FCLbLEu3K78wBlcIU9/6f7wiPwC+0SCBYNOuW5h7UKBVAUGWmxiuOSqRPIhNzgQ8= orangnlp@gmail.com diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 75654b6..8e34570 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + Injectable, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; import { DataSource, Repository } from 'typeorm'; @@ -33,6 +37,13 @@ export class ReportsService { }); if (!postData) throw new BadRequestException('게시글이 존재하지 않습니다.'); + + const reportPost = await this.reportsRepository.findOne({ + where: { userKakaoId: dto.userKakaoId, postId: targetId }, + }); + if (reportPost) + throw new ConflictException('이미 신고한 게시물입니다.'); + await queryRunner.manager.update(Posts, postData.id, { blame_count: () => 'blame_count +1', }); @@ -48,6 +59,13 @@ export class ReportsService { }); if (!commentData) throw new BadRequestException('댓글이 존재하지 않습니다.'); + + const reportComment = await this.reportsRepository.findOne({ + where: { userKakaoId: dto.userKakaoId, commentId: targetId }, + }); + if (reportComment) + throw new ConflictException('이미 신고한 게시물입니다.'); + await queryRunner.manager.update(Comment, commentData.id, { blame_count: () => 'blame_count +1', }); From 0a6323af25bbde7651329681fda38c28c4ce2f15 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 7 May 2024 11:15:14 +0900 Subject: [PATCH 046/236] feat: create anmt api --- .../announcements/announcements.controller.ts | 41 ++++++++++++++++++- .../announcements/announcements.service.ts | 17 ++++++++ .../dtos/announcement-response.dto.ts | 3 ++ .../dtos/create-announcement.dto.ts | 9 ++++ .../entities/announcement.entity.ts | 7 ++++ .../announcements.service.interface.ts | 9 ++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/APIs/announcements/dtos/announcement-response.dto.ts create mode 100644 src/APIs/announcements/dtos/create-announcement.dto.ts create mode 100644 src/APIs/announcements/interfaces/announcements.service.interface.ts diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 2a16225..24bcd9b 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -1,7 +1,46 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpCode, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { AnnouncementsService } from './announcements.service'; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, +} from '@nestjs/swagger'; +import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { Request } from 'express'; +import { CreateAnouncementInput } from './dtos/create-announcement.dto'; +import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; @Controller() export class AnnouncementsController { constructor(private readonly announcementsService: AnnouncementsService) {} + + @ApiOperation({ summary: '[어드민용] 공지사항 작성' }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @ApiCreatedResponse({ type: AnnouncementResponseDto }) + @Post() + @HttpCode(201) + async createAnmt( + @Req() req: Request, + @Body() body: CreateAnouncementInput, + ): Promise { + const kakaoId = req.user.userId; + return await this.announcementsService.create({ ...body, kakaoId }); + } + + @ApiOperation({ summary: '공지사항 조회' }) + @ApiOkResponse({ type: [AnnouncementResponseDto] }) + @Get() + async fetchAnmts(): Promise { + return await this.announcementsService.fetchAll(); + } } diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index fb92692..01725a4 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -2,11 +2,28 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Announcement } from './entities/announcement.entity'; import { Repository } from 'typeorm'; +import { IAnnouncementsSerciceCreate } from './interfaces/announcements.service.interface'; +import { UsersService } from '../users/users.service'; +import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; @Injectable() export class AnnouncementsService { constructor( @InjectRepository(Announcement) private readonly annoucementsRepository: Repository, + private readonly usersService: UsersService, ) {} + + async create({ + kakaoId, + title, + content, + }: IAnnouncementsSerciceCreate): Promise { + await this.usersService.adminCheck({ kakaoId }); + return await this.annoucementsRepository.save({ title, content }); + } + + async fetchAll(): Promise { + return await this.annoucementsRepository.find(); + } } diff --git a/src/APIs/announcements/dtos/announcement-response.dto.ts b/src/APIs/announcements/dtos/announcement-response.dto.ts new file mode 100644 index 0000000..64e75a7 --- /dev/null +++ b/src/APIs/announcements/dtos/announcement-response.dto.ts @@ -0,0 +1,3 @@ +import { Announcement } from '../entities/announcement.entity'; + +export class AnnouncementResponseDto extends Announcement {} diff --git a/src/APIs/announcements/dtos/create-announcement.dto.ts b/src/APIs/announcements/dtos/create-announcement.dto.ts new file mode 100644 index 0000000..4034290 --- /dev/null +++ b/src/APIs/announcements/dtos/create-announcement.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from '@nestjs/swagger'; +import { Announcement } from '../entities/announcement.entity'; + +export class CreateAnouncementInput extends OmitType(Announcement, [ + 'date_created', + 'date_deleted', + 'date_updated', + 'id', +]) {} diff --git a/src/APIs/announcements/entities/announcement.entity.ts b/src/APIs/announcements/entities/announcement.entity.ts index cc00a64..f7de067 100644 --- a/src/APIs/announcements/entities/announcement.entity.ts +++ b/src/APIs/announcements/entities/announcement.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Column, CreateDateColumn, @@ -9,21 +10,27 @@ import { @Entity() export class Announcement { + @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() id: number; + @ApiProperty({ type: String, description: '공지 제목' }) @Column() title: string; + @ApiProperty({ type: String, description: '내용' }) @Column() content: string; + @ApiProperty({ type: Date, description: '생성된 날짜' }) @CreateDateColumn() date_created: Date; + @ApiProperty({ type: Date, description: '수정된 날짜' }) @UpdateDateColumn() date_updated: Date; + @ApiProperty({ type: Date, description: '삭제된 날짜' }) @DeleteDateColumn() date_deleted: Date; } diff --git a/src/APIs/announcements/interfaces/announcements.service.interface.ts b/src/APIs/announcements/interfaces/announcements.service.interface.ts new file mode 100644 index 0000000..c1ea868 --- /dev/null +++ b/src/APIs/announcements/interfaces/announcements.service.interface.ts @@ -0,0 +1,9 @@ +import { Announcement } from '../entities/announcement.entity'; + +export interface IAnnouncementsSerciceCreate + extends Omit< + Announcement, + 'id' | 'date_created' | 'date_updated' | 'date_deleted' + > { + kakaoId: number; +} From a58f119b38710032ab664640cfeabdf6abbaed82 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 7 May 2024 11:49:05 +0900 Subject: [PATCH 047/236] feat: delete anmt api --- .../announcements/announcements.controller.ts | 22 ++++++++++++++++++- .../announcements/announcements.module.ts | 3 ++- .../announcements/announcements.service.ts | 17 ++++++++++++-- .../announcements.service.interface.ts | 5 +++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 24bcd9b..bc7c8d4 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, + Delete, Get, HttpCode, + Param, Post, Req, UseGuards, @@ -13,13 +15,15 @@ import { ApiCreatedResponse, ApiOkResponse, ApiOperation, + ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { Request } from 'express'; import { CreateAnouncementInput } from './dtos/create-announcement.dto'; import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; -@Controller() +@ApiTags('공지 API') +@Controller('anmt') export class AnnouncementsController { constructor(private readonly announcementsService: AnnouncementsService) {} @@ -43,4 +47,20 @@ export class AnnouncementsController { async fetchAnmts(): Promise { return await this.announcementsService.fetchAll(); } + + @ApiOperation({ + summary: '[어드민용] 공지사항 삭제', + description: 'id에 해당하는 공지사항 삭제, 삭제된 공지사항을 반환', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: AnnouncementResponseDto }) + @UseGuards(AuthGuardV2) + @Delete(':id') + async removeAnmt( + @Req() req: Request, + @Param('id') id: number, + ): Promise { + const kakaoId = req.user.userId; + return await this.announcementsService.remove({ kakaoId, id }); + } } diff --git a/src/APIs/announcements/announcements.module.ts b/src/APIs/announcements/announcements.module.ts index 8cc7ac1..a70bc06 100644 --- a/src/APIs/announcements/announcements.module.ts +++ b/src/APIs/announcements/announcements.module.ts @@ -3,9 +3,10 @@ import { AnnouncementsController } from './announcements.controller'; import { AnnouncementsService } from './announcements.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Announcement } from './entities/announcement.entity'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Announcement])], + imports: [TypeOrmModule.forFeature([Announcement]), UsersModule], controllers: [AnnouncementsController], providers: [AnnouncementsService], }) diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index 01725a4..d033fda 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -1,8 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Announcement } from './entities/announcement.entity'; import { Repository } from 'typeorm'; -import { IAnnouncementsSerciceCreate } from './interfaces/announcements.service.interface'; +import { + IAnnouncementsSerciceCreate, + IAnnouncementsSerciceRemove, +} from './interfaces/announcements.service.interface'; import { UsersService } from '../users/users.service'; import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; @@ -26,4 +29,14 @@ export class AnnouncementsService { async fetchAll(): Promise { return await this.annoucementsRepository.find(); } + + async remove({ + kakaoId, + id, + }: IAnnouncementsSerciceRemove): Promise { + await this.usersService.adminCheck({ kakaoId }); + const anmt = await this.annoucementsRepository.findOne({ where: { id } }); + if (!anmt) throw new NotFoundException('공지를 찾을 수 없습니다.'); + return await this.annoucementsRepository.softRemove(anmt); + } } diff --git a/src/APIs/announcements/interfaces/announcements.service.interface.ts b/src/APIs/announcements/interfaces/announcements.service.interface.ts index c1ea868..a89fe39 100644 --- a/src/APIs/announcements/interfaces/announcements.service.interface.ts +++ b/src/APIs/announcements/interfaces/announcements.service.interface.ts @@ -7,3 +7,8 @@ export interface IAnnouncementsSerciceCreate > { kakaoId: number; } + +export interface IAnnouncementsSerciceRemove { + kakaoId: number; + id: number; +} From 03549e2fcd632ef99288e42ec3a8b0dbea59cab4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 7 May 2024 12:01:25 +0900 Subject: [PATCH 048/236] feat: patch anmt api --- src/APIs/announcements/announcements.controller.ts | 12 ++++++++++++ src/APIs/announcements/announcements.service.ts | 10 ++++++++++ src/APIs/announcements/dtos/patch-announcment.dto.ts | 11 +++++++++++ .../interfaces/announcements.service.interface.ts | 7 +++++++ 4 files changed, 40 insertions(+) create mode 100644 src/APIs/announcements/dtos/patch-announcment.dto.ts diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index bc7c8d4..26a28f7 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -5,6 +5,7 @@ import { Get, HttpCode, Param, + Patch, Post, Req, UseGuards, @@ -21,6 +22,7 @@ import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { Request } from 'express'; import { CreateAnouncementInput } from './dtos/create-announcement.dto'; import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; +import { PatchAnnouncementInput } from './dtos/patch-announcment.dto'; @ApiTags('공지 API') @Controller('anmt') @@ -48,6 +50,16 @@ export class AnnouncementsController { return await this.announcementsService.fetchAll(); } + @ApiOperation({ summary: '[어드민용] 공지사항 수정' }) + @ApiCookieAuth() + @ApiOkResponse({ type: [AnnouncementResponseDto] }) + @UseGuards(AuthGuardV2) + @Patch() + async patchAnmt(@Req() req: Request, @Body() body: PatchAnnouncementInput) { + const kakaoId = req.body.userId; + return await this.announcementsService.patch({ ...body, kakaoId }); + } + @ApiOperation({ summary: '[어드민용] 공지사항 삭제', description: 'id에 해당하는 공지사항 삭제, 삭제된 공지사항을 반환', diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index d033fda..b132d71 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -4,6 +4,7 @@ import { Announcement } from './entities/announcement.entity'; import { Repository } from 'typeorm'; import { IAnnouncementsSerciceCreate, + IAnnouncementsSercicePatch, IAnnouncementsSerciceRemove, } from './interfaces/announcements.service.interface'; import { UsersService } from '../users/users.service'; @@ -30,6 +31,15 @@ export class AnnouncementsService { return await this.annoucementsRepository.find(); } + async patch({ kakaoId, id, title, content }: IAnnouncementsSercicePatch) { + await this.usersService.adminCheck({ kakaoId }); + const anmt = await this.annoucementsRepository.findOne({ where: { id } }); + if (!anmt) throw new NotFoundException('공지를 찾을 수 없습니다.'); + if (title) anmt.title = title; + if (content) anmt.content = content; + return await this.annoucementsRepository.save(anmt); + } + async remove({ kakaoId, id, diff --git a/src/APIs/announcements/dtos/patch-announcment.dto.ts b/src/APIs/announcements/dtos/patch-announcment.dto.ts new file mode 100644 index 0000000..f30b72e --- /dev/null +++ b/src/APIs/announcements/dtos/patch-announcment.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateAnouncementInput } from './create-announcement.dto'; +import { IsNumber } from 'class-validator'; + +export class PatchAnnouncementInput extends PartialType( + CreateAnouncementInput, +) { + @IsNumber() + @ApiProperty({ type: Number, description: '수정할 id' }) + id: number; +} diff --git a/src/APIs/announcements/interfaces/announcements.service.interface.ts b/src/APIs/announcements/interfaces/announcements.service.interface.ts index a89fe39..74906c2 100644 --- a/src/APIs/announcements/interfaces/announcements.service.interface.ts +++ b/src/APIs/announcements/interfaces/announcements.service.interface.ts @@ -12,3 +12,10 @@ export interface IAnnouncementsSerciceRemove { kakaoId: number; id: number; } + +export interface IAnnouncementsSercicePatch { + kakaoId: number; + id: number; + title?: string; + content?: string; +} From fdb5449b073654beaf50e17b7a0763bfc3a2f473 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 10:22:54 +0900 Subject: [PATCH 049/236] refactor: change endpoints of authController --- src/APIs/auth/auth.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 9d8c9bc..002c8f2 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -32,7 +32,7 @@ export class AuthController { @ApiMovedPermanentlyResponse({ description: `카카오에서 인증 완료 후 클라이언트 루트 url로 리다이렉트 한다.`, }) - @Get('kakao') // 카카오 서버를 거쳐서 도착하게 될 엔드포인트 + @Get('login/kakao') // 카카오 서버를 거쳐서 도착하게 될 엔드포인트 @UseGuards(AuthGuard('kakao')) // kakao.strategy를 실행시켜 줍니다. @HttpCode(301) async kakaoLogin(@Req() req: Request, @Res() res: Response) { @@ -58,7 +58,7 @@ export class AuthController { 'refresh 토큰이 만료되었거나 없을 경우 cookie를 모두 clear한다.', }) @ApiCookieAuth() - @Get('refresh') + @Get('refresh-token') @HttpCode(201) async refresh(@Req() req: Request, @Res() res: Response) { try { From 25b8c349afff1d94bb5b31c8f98c97485872de4c Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 10:26:19 +0900 Subject: [PATCH 050/236] refactor: change endpoints of anmtController --- src/APIs/announcements/announcements.controller.ts | 10 +++++++--- src/APIs/announcements/dtos/patch-announcment.dto.ts | 9 ++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 26a28f7..5ce4f02 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -54,10 +54,14 @@ export class AnnouncementsController { @ApiCookieAuth() @ApiOkResponse({ type: [AnnouncementResponseDto] }) @UseGuards(AuthGuardV2) - @Patch() - async patchAnmt(@Req() req: Request, @Body() body: PatchAnnouncementInput) { + @Patch(':id') + async patchAnmt( + @Req() req: Request, + @Body() body: PatchAnnouncementInput, + @Param('id') id: number, + ) { const kakaoId = req.body.userId; - return await this.announcementsService.patch({ ...body, kakaoId }); + return await this.announcementsService.patch({ ...body, id, kakaoId }); } @ApiOperation({ diff --git a/src/APIs/announcements/dtos/patch-announcment.dto.ts b/src/APIs/announcements/dtos/patch-announcment.dto.ts index f30b72e..42ced37 100644 --- a/src/APIs/announcements/dtos/patch-announcment.dto.ts +++ b/src/APIs/announcements/dtos/patch-announcment.dto.ts @@ -1,11 +1,6 @@ -import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateAnouncementInput } from './create-announcement.dto'; -import { IsNumber } from 'class-validator'; export class PatchAnnouncementInput extends PartialType( CreateAnouncementInput, -) { - @IsNumber() - @ApiProperty({ type: Number, description: '수정할 id' }) - id: number; -} +) {} From 282e1ae975d7f424f1911f75728bf3e58a67be36 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 10:27:35 +0900 Subject: [PATCH 051/236] refactor: change anmt to plural --- src/APIs/announcements/announcements.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 5ce4f02..e55ca05 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -25,7 +25,7 @@ import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; import { PatchAnnouncementInput } from './dtos/patch-announcment.dto'; @ApiTags('공지 API') -@Controller('anmt') +@Controller('anmts') export class AnnouncementsController { constructor(private readonly announcementsService: AnnouncementsService) {} From ac3ced33a5c97b0ee7e7ce644d9ef173dd9809c5 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 10:56:49 +0900 Subject: [PATCH 052/236] refactor: change endpoints of commmentsController --- src/APIs/comments/comments.controller.ts | 16 +++++++++++----- src/APIs/comments/comments.service.ts | 7 +++++-- src/APIs/comments/dtos/create-comment.dto.ts | 3 ++- src/APIs/comments/dtos/delete-comment.dto.ts | 9 --------- 4 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 src/APIs/comments/dtos/delete-comment.dto.ts diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index f215c75..60dd509 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -3,6 +3,7 @@ import { Controller, Delete, HttpCode, + Param, Post, Req, UseGuards, @@ -17,12 +18,11 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { DeleteCommentDto } from './dtos/delete-comment.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { ChildrenComment } from './dtos/fetch-comments.dto'; @ApiTags('댓글 API') -@Controller('comments') +@Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @@ -37,10 +37,11 @@ export class CommentsController { @HttpCode(200) async upsertComment( @Req() req: Request, + @Param('postId') postsId: number, @Body() body: CreateCommentInput, ): Promise { const userKakaoId = req.user.userId; - return await this.commentsService.upsert({ ...body, userKakaoId }); + return await this.commentsService.upsert({ ...body, postsId, userKakaoId }); } @ApiOperation({ @@ -54,9 +55,14 @@ export class CommentsController { @HttpCode(204) async deleteComment( @Req() req: Request, - @Body() body: DeleteCommentDto, + @Param('postId') postsId: number, + @Param('commentId') id: number, ): Promise { const userKakaoId = req.user.userId; - return await this.commentsService.delete({ ...body, userKakaoId }); + return await this.commentsService.delete({ + postsId, + id, + userKakaoId, + }); } } diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 3d17a47..4801275 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -69,10 +69,13 @@ export class CommentsService { return await this.commentsRepository.fetchComments({ postsId }); } - async delete({ id, userKakaoId }): Promise { + async delete({ id, userKakaoId, postsId }): Promise { try { const data = await this.existCheck({ id }); - await this.commentsRepository.softDelete({ + if (!(data.postsId == postsId)) + throw new NotFoundException('게시글을 찾을 수 없습니다.'); + //transaction 거는 것 고려해볼 것 + const deletedComment = await this.commentsRepository.softRemove({ user: { kakaoId: userKakaoId }, id, }); diff --git a/src/APIs/comments/dtos/create-comment.dto.ts b/src/APIs/comments/dtos/create-comment.dto.ts index d881052..29903cd 100644 --- a/src/APIs/comments/dtos/create-comment.dto.ts +++ b/src/APIs/comments/dtos/create-comment.dto.ts @@ -21,7 +21,7 @@ export class CreateCommentDto { parentId?: number; @ApiProperty({ - description: '[optional] 수정 시 댓글 id', + description: '[optional] 수정 시 댓글 id', type: Number, required: false, }) @@ -32,4 +32,5 @@ export class CreateCommentDto { export class CreateCommentInput extends OmitType(CreateCommentDto, [ 'userKakaoId', + 'postsId', ]) {} diff --git a/src/APIs/comments/dtos/delete-comment.dto.ts b/src/APIs/comments/dtos/delete-comment.dto.ts deleted file mode 100644 index e6b4791..0000000 --- a/src/APIs/comments/dtos/delete-comment.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class DeleteCommentDto { - @ApiProperty({ - description: '삭제하고자 하는 댓글 id', - type: Number, - }) - id: number; -} From ca963a5d2f77c68ae792b64015ef06ce7cfef956 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 11:02:22 +0900 Subject: [PATCH 053/236] refactor: change endpoints of likesController --- src/APIs/comments/comments.controller.ts | 2 +- src/APIs/likes/dtos/toggle-like.dto.ts | 6 ------ src/APIs/likes/likes.controller.ts | 15 ++++++--------- 3 files changed, 7 insertions(+), 16 deletions(-) delete mode 100644 src/APIs/likes/dtos/toggle-like.dto.ts diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 60dd509..2664013 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -21,7 +21,7 @@ import { import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { ChildrenComment } from './dtos/fetch-comments.dto'; -@ApiTags('댓글 API') +@ApiTags('게시글 API') @Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} diff --git a/src/APIs/likes/dtos/toggle-like.dto.ts b/src/APIs/likes/dtos/toggle-like.dto.ts deleted file mode 100644 index e38d3ae..0000000 --- a/src/APIs/likes/dtos/toggle-like.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ToggleLikeDto { - @ApiProperty({ type: Number, description: 'post_id' }) - id: number; -} diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index cb51ccd..5127cde 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -1,5 +1,4 @@ import { - Body, Controller, Get, HttpCode, @@ -16,15 +15,14 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ToggleLikeDto } from './dtos/toggle-like.dto'; import { Request } from 'express'; import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -@ApiTags('좋아요 API') -@Controller('likes') +@ApiTags('게시글 API') +@Controller('posts/:postId/like') export class LikesController { constructor(private readonly likesService: LikesService) {} @@ -39,22 +37,21 @@ export class LikesController { @HttpCode(200) @Post() async toggleLike( - @Body() body: ToggleLikeDto, + @Param('postId') id: number, @Req() req: Request, ): Promise { const kakaoId = req.user.userId; - const id = body.id; return this.likesService.toggleLike({ id, kakaoId }); } @ApiOperation({ summary: '좋아요 누른 대상 조회하기', - description: '{id}인 게시글에 좋아요를 누른 사람들을 확인한다.', + description: '게시글에 좋아요를 누른 사람들을 확인한다.', }) @ApiOkResponse({ description: '조회 성공', type: [FetchLikesResponseDto] }) @HttpCode(200) - @Get(':id') - async fetchLikes(@Param('id') id: number): Promise { + @Get() + async fetchLikes(@Param('postId') id: number): Promise { return await this.likesService.fetchLikes({ id }); } } From d9a21bcac1480e5e9235aafb97c2642a284c51c6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 11:14:25 +0900 Subject: [PATCH 054/236] refactor: change endpoints of neighborsController --- src/APIs/neighbors/dtos/follow.dto.ts | 8 ---- .../neighbors/dtos/from-user-response.dto.ts | 14 +++---- .../neighbors/dtos/to-user-response.dto.ts | 14 +++---- .../neighbors/entities/neighbor.entity.ts | 2 + src/APIs/neighbors/neighbors.controller.ts | 41 +++++++++---------- 5 files changed, 34 insertions(+), 45 deletions(-) delete mode 100644 src/APIs/neighbors/dtos/follow.dto.ts diff --git a/src/APIs/neighbors/dtos/follow.dto.ts b/src/APIs/neighbors/dtos/follow.dto.ts deleted file mode 100644 index b0888cf..0000000 --- a/src/APIs/neighbors/dtos/follow.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -export class FollowDto { - @ApiProperty({ type: Number, example: 3388766789, description: '카카오 id' }) - @IsNotEmpty() - follow_id: number; -} diff --git a/src/APIs/neighbors/dtos/from-user-response.dto.ts b/src/APIs/neighbors/dtos/from-user-response.dto.ts index 4249ae0..fb155ab 100644 --- a/src/APIs/neighbors/dtos/from-user-response.dto.ts +++ b/src/APIs/neighbors/dtos/from-user-response.dto.ts @@ -1,13 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { Neighbor } from '../entities/neighbor.entity'; -export class FromUserResponseDto { - @ApiProperty({ - type: String, - example: 'b6993606-1992-427e-bf73-c3fc778a48ff', - }) - id: string; - +export class FromUserResponseDto extends OmitType(Neighbor, [ + 'from_user', + 'to_user', +]) { @ApiProperty({ type: UserResponseDto, }) diff --git a/src/APIs/neighbors/dtos/to-user-response.dto.ts b/src/APIs/neighbors/dtos/to-user-response.dto.ts index b37acc0..f35acaa 100644 --- a/src/APIs/neighbors/dtos/to-user-response.dto.ts +++ b/src/APIs/neighbors/dtos/to-user-response.dto.ts @@ -1,13 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { Neighbor } from '../entities/neighbor.entity'; -export class ToUserResponseDto { - @ApiProperty({ - type: String, - example: 'b6993606-1992-427e-bf73-c3fc778a48ff', - }) - id: string; - +export class ToUserResponseDto extends OmitType(Neighbor, [ + 'from_user', + 'to_user', +]) { @ApiProperty({ type: UserResponseDto, }) diff --git a/src/APIs/neighbors/entities/neighbor.entity.ts b/src/APIs/neighbors/entities/neighbor.entity.ts index 96417c3..2407691 100644 --- a/src/APIs/neighbors/entities/neighbor.entity.ts +++ b/src/APIs/neighbors/entities/neighbor.entity.ts @@ -33,9 +33,11 @@ export class Neighbor { }) from_user: User; + @ApiProperty({ type: Number, description: '이웃 추가를 받은 유저' }) @RelationId((neighbor: Neighbor) => neighbor.to_user) // you need to specify target relation toUserKakaoId: number; + @ApiProperty({ type: Number, description: '이웃 추가를 한 유저' }) @RelationId((neighbor: Neighbor) => neighbor.from_user) // you need to specify target relation fromUserKakaoId: number; } diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/neighbors/neighbors.controller.ts index 4e49643..7fd920c 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/neighbors/neighbors.controller.ts @@ -1,5 +1,4 @@ import { - Body, Controller, Get, HttpCode, @@ -14,25 +13,25 @@ import { ApiConflictResponse, ApiCookieAuth, ApiCreatedResponse, + ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { FollowDto } from './dtos/follow.dto'; import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -@ApiTags('이웃 API') -@Controller('neighbors') +@ApiTags('유저 API') +@Controller('users/:userId') export class NeighborsController { constructor(private readonly neighborsService: NeighborsService) {} @ApiOperation({ summary: '이웃 추가하기', - description: '로그인된 유저가 follow_id를 팔로우한다.', + description: '로그인된 유저가 userId를 팔로우한다.', }) @ApiCookieAuth() @ApiCreatedResponse({ description: '이웃 추가 성공', type: FollowUserDto }) @@ -41,11 +40,10 @@ export class NeighborsController { @Post('follow') @HttpCode(201) async followUser( - @Body() body: FollowDto, @Req() req: Request, + @Param('userId') to_user: number, ): Promise { const kakaoId = parseInt(req.user.userId); - const to_user = body.follow_id; return await this.neighborsService.followUser({ from_user: kakaoId, to_user, @@ -54,17 +52,16 @@ export class NeighborsController { @ApiOperation({ summary: '이웃 삭제하기', - description: '로그인된 유저가 follow_id를 언팔로우 한다.', + description: '로그인된 유저가 userId를 언팔로우 한다.', }) @ApiCookieAuth() - @ApiOkResponse({ description: '언팔로우 성공' }) + @ApiNoContentResponse({ description: '언팔로우 성공' }) @ApiNotFoundResponse({ description: '존재하지 않는 이웃 정보이다.' }) @UseGuards(AuthGuardV2) - @Post('unfollow') - @HttpCode(200) - unfollowUser(@Body() body: FollowDto, @Req() req: Request) { + @Post('cancel-follow') + @HttpCode(204) + unfollowUser(@Req() req: Request, @Param('userId') to_user: number) { const kakaoId = parseInt(req.user.userId); - const to_user = body.follow_id; return this.neighborsService.unfollowUser({ from_user: kakaoId, to_user, @@ -73,29 +70,31 @@ export class NeighborsController { @ApiOperation({ summary: '팔로워 목록 조회', - description: 'id의 팔로워 목록을 조회한다.', + description: 'userId의 팔로워 목록을 조회한다.', }) @ApiOkResponse({ description: '팔로워 목록 조회 성공', type: [FromUserResponseDto], }) @HttpCode(200) - @Get('followers/:id') - getFollowers(@Param('id') kakaoId: number): Promise { + @Get('followers') + getFollowers( + @Param('userId') kakaoId: number, + ): Promise { return this.neighborsService.getFollowers({ kakaoId }); } @ApiOperation({ - summary: '팔로우 목록 조회', - description: 'id의 팔로우 목록을 조회한다.', + summary: '팔로잉 목록 조회', + description: 'userId의 팔로잉 목록을 조회한다.', }) @ApiOkResponse({ - description: '팔로우 목록 조회 성공', + description: '팔로잉 목록 조회 성공', type: [ToUserResponseDto], }) @HttpCode(200) - @Get('follows/:id') - getFollows(@Param('id') kakaoId: number): Promise { + @Get('following') + getFollows(@Param('userId') kakaoId: number): Promise { return this.neighborsService.getFollows({ kakaoId }); } } From cbf4e8040f128231d66ed895be9c18def9ed5d70 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 11:22:06 +0900 Subject: [PATCH 055/236] refactor: change endpoints of notificationsController --- src/APIs/notifications/notifications.controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index b878901..869a842 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -25,7 +25,7 @@ import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @ApiTags('알림 API') -@Controller('nots') +@Controller('notifications') export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @@ -44,7 +44,7 @@ export class NotificationsController { @ApiCookieAuth() @ApiProduces('text/event-stream') @UseGuards(AuthGuardV2) - @Sse('sub') + @Sse('subscribe') sendClientAlarm( @Req() req: Request, // @Param('kakaoId') userKakaoId, @@ -60,7 +60,7 @@ export class NotificationsController { '로그인된 유저들에게 보내진 알림들을 조회한다. query를 통해 알림 조회 옵션 설정. sse 연결 이전 이니셜 데이터 fetch 시 사용', }) @ApiOkResponse({ type: [FetchNotiResponse] }) - @Get('init') + @Get() @ApiCookieAuth() @UseGuards(AuthGuardV2) async fetchNoti( @@ -75,14 +75,14 @@ export class NotificationsController { } @ApiOperation({ - summary: '알림 토글', + summary: '알림 읽기', description: '알림을 읽음 처리한다.', }) @ApiCookieAuth() @UseGuards(AuthGuardV2) @ApiOkResponse({ type: FetchNotiResponse }) @HttpCode(200) - @Post('toggle/:id') + @Post(':id/read') async toggleNoti( @Req() req: Request, @Param('id') id: number, From 4c0c54260abee461a4ebcd59f97680cecd6bfb9f Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 11:29:40 +0900 Subject: [PATCH 056/236] refactor: change endpoints of categoriesController --- .../postBackgrounds.controller.ts | 2 +- .../PostCategories.controller.ts | 60 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/APIs/postBackgrounds/postBackgrounds.controller.ts b/src/APIs/postBackgrounds/postBackgrounds.controller.ts index 5c3e1a6..dc7410b 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.controller.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.controller.ts @@ -22,7 +22,7 @@ import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dt import { PostBackground } from './entities/postBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; -@ApiTags('내지 API') +@ApiTags('[잠정 사용X] 내지 API') @Controller('postbg') export class PostBackgroundsController { constructor( diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index c171961..0ab046b 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -23,47 +23,25 @@ import { CreatePostCategoryResponseDto } from './dtos/create-post-category-respo import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; import { FetchPostCategoryDto } from './dtos/fetch-post-category.dto'; -@ApiTags('카테고리 API') -@Controller('postcg') +@ApiTags('유저 API') +@Controller('users') export class PostCategoriesController { constructor(private readonly postCategoriesService: PostCategoriesService) {} - @ApiOperation({ - summary: '카테고리 생성', - description: '로그인된 유저와 연결된 카테고리를 생성한다.', - }) - @ApiCookieAuth() - @ApiCreatedResponse({ - description: '카테고리 생성 완료', - type: CreatePostCategoryResponseDto, - }) - @UseGuards(AuthGuardV2) - @Post() - @HttpCode(201) - async createPostCategory( - @Req() req: Request, - @Body() body: CreatePostCategoryDto, - ): Promise { - const kakaoId = req.user.userId; - const name = body.name; - return await this.postCategoriesService.create({ kakaoId, name }); - } - @ApiOperation({ summary: '특정 유저의 카테고리 정보 조회', description: '특정 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', }) - @ApiCookieAuth() @ApiOkResponse({ description: '', type: [FetchPostCategoryDto], }) - @Get(':kakaoId') + @Get(':userId/categories') @HttpCode(200) async fetchPostCategories( @Req() req: Request, - @Param('kakaoId') targetKakaoId: number, + @Param('userId') targetKakaoId: number, ): Promise { const kakaoId = req.user.userId; return await this.postCategoriesService.fetchAll({ @@ -72,15 +50,39 @@ export class PostCategoriesController { }); } + @ApiOperation({ + summary: '게시글 카테고리 생성', + description: '로그인된 유저와 연결된 카테고리를 생성한다.', + }) + @ApiCookieAuth() + @ApiCreatedResponse({ + description: '카테고리 생성 완료', + type: CreatePostCategoryResponseDto, + }) + @UseGuards(AuthGuardV2) + @Post('me/categories') + @HttpCode(201) + async createPostCategory( + @Req() req: Request, + @Body() body: CreatePostCategoryDto, + ): Promise { + const kakaoId = req.user.userId; + const name = body.name; + return await this.postCategoriesService.create({ kakaoId, name }); + } + @ApiOperation({ summary: '유저의 지정 카테고리 삭제하기', description: - '로그인된 유저의 카테고리 중 param:id와 일치하는 카테고리를 삭제한다', + '로그인된 유저의 카테고리 중 categoryId 일치하는 카테고리를 삭제한다', }) @ApiCookieAuth() - @Delete(':id') + @Delete('me/categories/:categoryId') @UseGuards(AuthGuardV2) - async deletePostCategory(@Req() req: Request, @Param('id') id: string) { + async deletePostCategory( + @Req() req: Request, + @Param('categoryId') id: string, + ) { const kakaoId = req.user.userId; return await this.postCategoriesService.delete({ kakaoId, id }); } From 168eb73a9744122c9a3e41885350019530729908 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 12:40:02 +0900 Subject: [PATCH 057/236] refactor: change endpoints of postsController --- .../notifications/notifications.controller.ts | 8 +- src/APIs/posts/posts.controller.ts | 101 +++++++++--------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index 869a842..c48d5fb 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -37,7 +37,7 @@ export class NotificationsController { 브라우저를 끄거나 refetching이 한동안 일어나지 않으면 sse를 끊는다. */ @ApiOperation({ - summary: '[SSE] kakaoId로 오는 알림을 구독한다.', + summary: '[SSE] 알림을 구독한다.', description: '[swagger 불가능, postman 권장] sse를 연결한다. 로그인된 유저를 타겟으로 하는 알림이 보내졌을경우 sse를 통해 전달받는다.', }) @@ -92,11 +92,11 @@ export class NotificationsController { } @ApiOperation({ - summary: 'kakaoId에게 알림 생성', + summary: 'userId에게 알림 생성', description: - 'kakaoId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', + 'userId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', }) - @Post('send/:kakaoId') + @Post('send/:userId') async sendNoti(@Req() req: Request, @Body() body: EmitNotiInput) { const userKakaoId = req.user.userId; return await this.notificationsService.emitAlarm({ userKakaoId, ...body }); diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index ad09919..400ffa4 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -50,37 +50,49 @@ export class PostsController { constructor(private readonly postsService: PostsService) {} @ApiOperation({ - summary: '게시글 임시등록', - description: `게시글을 임시등록한다. + summary: '게시글 등록', + description: `게시글을 등록한다. id를 입력하지 않으면 생성하고 있는 아이디를 치면 update하는 로직으로 바로 게시글 생성에 사용해도 되고, 수정용으로 사용해도 된다.`, }) - @Post('temp') + @Post() @ApiCookieAuth() - @ApiCreatedResponse({ description: '임시등록 성공', type: PublishPostDto }) + @ApiCreatedResponse({ description: '등록 성공', type: PublishPostDto }) @UseGuards(AuthGuardV2) @HttpCode(201) - async updatePost(@Req() req: Request, @Body() body: CreatePostInput) { + async publishPost(@Req() req: Request, @Body() body: PublishPostInput) { const kakaoId = req.user.userId; - const dto = { ...body, userKakaoId: kakaoId, isPublished: false }; + console.log(body); + const dto = { ...body, userKakaoId: kakaoId, isPublished: true }; return await this.postsService.save(dto); } @ApiOperation({ - summary: '게시글 등록', - description: `게시글을 등록한다. + summary: '게시글 논리 삭제', + description: '로그인 된 유저의 postId에 해당하는 게시글을 논리삭제한다.', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @Delete(':postId') + async softDelete(@Req() req: Request, @Param('postId') id: number) { + const kakaoId = req.user.userId; + return await this.postsService.softDelete({ kakaoId, id }); + } + + @ApiOperation({ + summary: '게시글 임시등록', + description: `게시글을 임시등록한다. id를 입력하지 않으면 생성하고 있는 아이디를 치면 update하는 로직으로 바로 게시글 생성에 사용해도 되고, 수정용으로 사용해도 된다.`, }) - @Post() + @Post('temp') @ApiCookieAuth() - @ApiCreatedResponse({ description: '등록 성공', type: PublishPostDto }) + @ApiCreatedResponse({ description: '임시등록 성공', type: PublishPostDto }) @UseGuards(AuthGuardV2) @HttpCode(201) - async publishPost(@Req() req: Request, @Body() body: PublishPostInput) { + async updatePost(@Req() req: Request, @Body() body: CreatePostInput) { const kakaoId = req.user.userId; - console.log(body); - const dto = { ...body, userKakaoId: kakaoId, isPublished: true }; + const dto = { ...body, userKakaoId: kakaoId, isPublished: false }; return await this.postsService.save(dto); } @@ -125,30 +137,32 @@ export class PostsController { return await this.postsService.saveImage(file); } - @ApiOperation({ - summary: '게시글 soft delete', - description: - '로그인 된 유저의 {id}에 해당하는 게시글을 논리삭제한다. 발행된 게시글에 사용을 권장', - }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) - @Delete('soft/:id') - async softDelete(@Req() req: Request, @Param('id') id: number) { - const kakaoId = req.user.userId; - return await this.postsService.softDelete({ kakaoId, id }); - } + // @ApiOperation({ + // summary: '게시글 hard delete', + // description: + // '로그인 된 유저의 {id}에 해당하는 게시글을 물리삭제한다. 임시 저장된 게시글에 사용을 권장', + // }) + // @ApiCookieAuth() + // @UseGuards(AuthGuardV2) + // @Delete('hard/:id') + // async hardDelete(@Req() req: Request, @Param('id') id: number) { + // const kakaoId = req.user.userId; + // return await this.postsService.hardDelete({ kakaoId, id }); + // } @ApiOperation({ - summary: '게시글 hard delete', + summary: '게시글 디테일 뷰 fetch', description: - '로그인 된 유저의 {id}에 해당하는 게시글을 물리삭제한다. 임시 저장된 게시글에 사용을 권장', + 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) - @Delete('hard/:id') - async hardDelete(@Req() req: Request, @Param('id') id: number) { + @Get('detail/:postId') + @ApiOkResponse({ type: fetchPostDetailDto }) + async fetchPostDetail( + @Param('postId') id: number, + @Req() req: Request, + ): Promise { const kakaoId = req.user.userId; - return await this.postsService.hardDelete({ kakaoId, id }); + return await this.postsService.fetchDetail({ kakaoId, id }); } @ApiOperation({ @@ -160,30 +174,15 @@ export class PostsController { @ApiOkResponse({ type: FetchPostForUpdateDto }) @UseGuards(AuthGuardV2) @HttpCode(200) - @Get('update/:id') + @Get('update/:postId') async fetchPost( @Req() req: Request, - @Param('id') id: number, + @Param('postId') id: number, ): Promise { const kakaoId = req.user.userId; return await this.postsService.fetchPostForUpdate({ id, kakaoId }); } - @ApiOperation({ - summary: '게시글 디테일 뷰 fetch', - description: - 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', - }) - @Get('detail/:id') - @ApiOkResponse({ type: fetchPostDetailDto }) - async fetchPostDetail( - @Param('id') id: number, - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchDetail({ kakaoId, id }); - } - @ApiOperation({ summary: '[offset]전체 게시글 조회 API', description: @@ -259,10 +258,10 @@ export class PostsController { description: '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', }) - @Get('/cursor/user/:kakaoId') + @Get('/cursor/user/:userId') @ApiOkResponse({ type: CursorPagePostResponseDto }) async fetchUserPosts( - @Param('kakaoId') targetKakaoId: number, + @Param('userId') targetKakaoId: number, @Req() req: Request, @Query() cursorOption: FetchUserPostsInput, ): Promise { From 93e49627323af76bec7ccbf97d79a5d46c8fb9dc Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 8 May 2024 12:48:52 +0900 Subject: [PATCH 058/236] refactor: change endpoints of usersController --- src/APIs/users/users.controller.ts | 64 +++++++++++++++--------------- src/app.module.ts | 4 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 26e137b..7f79eb2 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -46,12 +46,38 @@ export class UsersController { // ================================ @ApiOperation({ - summary: '로그인된 유저의 정보 불러오기', - description: '로그인된 유저의 정보를 불러온다.', + summary: '이름이 포함된 유저 검색', + description: '이름에 username이 포함된 유저를 검색한다.', + }) + @ApiOkResponse({ description: '조회 성공', type: [UserResponseDto] }) + @HttpCode(200) + @Get('username/:username') + async findUsersByName( + @Param('username') username: string, + ): Promise { + return await this.usersService.findUsersByName({ username }); + } + + @ApiOperation({ + summary: '특정 유저 프로필 조회', + description: 'id가 일치하는 유저 프로필을 조회한다.', + }) + @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) + @HttpCode(200) + @Get('profile/:userId') + async findUserByKakaoId( + @Param('userId') kakaoId: number, + ): Promise { + return await this.usersService.findUserByKakaoId({ kakaoId }); + } + + @ApiOperation({ + summary: '로그인된 유저의 프로필 불러오기', + description: '로그인된 유저의 프로필을 불러온다.', }) @ApiCookieAuth() @ApiOkResponse({ description: '불러오기 완료', type: UserResponseDto }) - @Get() + @Get('me') @UseGuards(AuthGuardV2) @HttpCode(200) async fetchUser(@Req() req: Request): Promise { @@ -65,7 +91,7 @@ export class UsersController { }) @ApiOkResponse({ description: '변경 성공', type: UserResponseDto }) @ApiCookieAuth() - @Patch() + @Patch('me') @HttpCode(200) @UseGuards(AuthGuardV2) async patchUser( @@ -99,7 +125,7 @@ export class UsersController { @ApiCookieAuth() @UseInterceptors(FileInterceptor('file')) @HttpCode(201) - @Post('profile') + @Post('me/profile-image') async uploadProfileImage( @Req() req: Request, @UploadedFile() file: Express.Multer.File, @@ -128,7 +154,7 @@ export class UsersController { @ApiCookieAuth() @UseInterceptors(FileInterceptor('file')) @HttpCode(201) - @Post('background') + @Post('me/background-image') async uploadBackgroundImage( @Req() req: Request, @UploadedFile() file: Express.Multer.File, @@ -139,30 +165,4 @@ export class UsersController { file, }); } - - @ApiOperation({ - summary: '이름이 포함된 유저 검색', - description: '이름에 username이 포함된 유저를 검색한다.', - }) - @ApiOkResponse({ description: '조회 성공', type: [UserResponseDto] }) - @HttpCode(200) - @Get('username/:username') - async findUsersByName( - @Param('username') username: string, - ): Promise { - return await this.usersService.findUsersByName({ username }); - } - - @ApiOperation({ - summary: 'kakaoId에 정확히 부합하는 유저 검색', - description: 'kakaoId가 param과 일치하는 유저를 검색한다.', - }) - @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) - @HttpCode(200) - @Get('kakaoId/:kakaoId') - async findUserByKakaoId( - @Param('kakaoId') kakaoId: number, - ): Promise { - return await this.usersService.findUserByKakaoId({ kakaoId }); - } } diff --git a/src/app.module.ts b/src/app.module.ts index 9bb8cdd..596feeb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,18 +23,18 @@ import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ AnnouncementsModule, - CommentsModule, StickersModule, StickerCategoriesModule, StickerBlocksModule, PostsModule, + CommentsModule, LikesModule, UsersModule, + PostCategoriesModule, AuthModule, NeighborsModule, NotificationsModule, PostBackgroundsModule, - PostCategoriesModule, ReportsModule, JwtModule.registerAsync({ imports: [ConfigModule], From 699b489bf44f7e8ec247198ca38a184e34b1ce82 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 00:26:58 +0900 Subject: [PATCH 059/236] refactor: divide likesChecking logic --- .../likes/dtos/toggle-like-response.dto.ts | 47 ++++--------------- src/APIs/likes/likes.controller.ts | 18 ++++++- src/APIs/likes/likes.service.ts | 10 +++- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/APIs/likes/dtos/toggle-like-response.dto.ts b/src/APIs/likes/dtos/toggle-like-response.dto.ts index f594aba..eb28334 100644 --- a/src/APIs/likes/dtos/toggle-like-response.dto.ts +++ b/src/APIs/likes/dtos/toggle-like-response.dto.ts @@ -1,39 +1,8 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; - -export class ToggleLikeResponseDto { - @ApiProperty({ description: '포스트의 고유 아이디', type: Number }) - id: number; - - @ApiProperty({ description: '제목(최대 100자)', type: String }) - title: string; - - @ApiProperty({ description: '임시저장(false), 발행(true)', type: Boolean }) - isPublished: boolean; - - @ApiProperty({ description: '좋아요 카운트', type: Number }) - like_count: number; - - @ApiProperty({ description: '조회수 카운트', type: Number }) - view_count: number; - - @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) - allow_comment: boolean; - - @ApiProperty({ - description: - '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', - type: 'enum', - enum: OpenScope, - }) - scope: OpenScope; - - @ApiProperty({ description: '생성된 날짜', type: Date }) - date_created: Date; - - @ApiProperty({ description: '수정된 날짜', type: Date }) - date_updated: Date; - - @ApiProperty({ description: 'soft delete column', type: Date }) - date_deleted: Date; -} +import { OmitType } from '@nestjs/swagger'; +import { Posts } from 'src/APIs/posts/entities/posts.entity'; + +export class ToggleLikeResponseDto extends OmitType(Posts, [ + 'postBackground', + 'postCategory', + 'user', +]) {} diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index 5127cde..bd2cf68 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -44,13 +44,29 @@ export class LikesController { return this.likesService.toggleLike({ id, kakaoId }); } + @ApiOperation({ + summary: '게시글 좋아요 여부 체크', + description: '특정 게시글에 내가 좋아요를 눌렀는 지 체크', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: Boolean }) + @UseGuards(AuthGuardV2) + @Get() + async fetchIfLiked( + @Param('postId') id: number, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.likesService.fetchIfLiked({ kakaoId, id }); + } + @ApiOperation({ summary: '좋아요 누른 대상 조회하기', description: '게시글에 좋아요를 누른 사람들을 확인한다.', }) @ApiOkResponse({ description: '조회 성공', type: [FetchLikesResponseDto] }) @HttpCode(200) - @Get() + @Get('like-users') async fetchLikes(@Param('postId') id: number): Promise { return await this.likesService.fetchLikes({ id }); } diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index fb9cec9..8699e0c 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -15,6 +15,14 @@ export class LikesService { private readonly dataSource: DataSource, ) {} + async fetchIfLiked({ kakaoId, id }): Promise { + const alreadyLiked = await this.likesRepository.findOne({ + where: { posts: { id }, user: { kakaoId } }, + }); + if (alreadyLiked) return true; + return false; + } + async toggleLike({ id, kakaoId }): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); @@ -26,7 +34,7 @@ export class LikesService { if (!postData) throw new NotFoundException('게시글이 존재하지 않습니다.'); // 좋아요 눌렀는지 확인하기 const alreadyLiked = await this.likesRepository.findOne({ - where: { posts: { id } }, + where: { posts: { id }, user: { kakaoId } }, }); if (alreadyLiked) { await queryRunner.manager.delete(Likes, { id: alreadyLiked.id }); From 52dec6ac0913a4cc5e81b79b894074281046742b Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 00:37:26 +0900 Subject: [PATCH 060/236] feat: GET comments api --- src/APIs/comments/comments.controller.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 2664013..e24ae63 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, + Get, HttpCode, Param, Post, @@ -19,7 +20,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -import { ChildrenComment } from './dtos/fetch-comments.dto'; +import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; @ApiTags('게시글 API') @Controller('posts/:postId/comments') @@ -44,6 +45,17 @@ export class CommentsController { return await this.commentsService.upsert({ ...body, postsId, userKakaoId }); } + @ApiOperation({ + summary: '특정 게시글에 대한 댓글 조회', + }) + @ApiOkResponse({ type: [FetchCommentsDto] }) + @Get() + async fetchComments( + @Param('postId') postsId: number, + ): Promise { + return await this.commentsService.fetchComments({ postsId }); + } + @ApiOperation({ summary: '댓글을 삭제한다.', description: '댓글을 논리삭제한다. date_deleted 칼럼에 값이 생긴다.', From 5585d51118e3c80c55a0cba4e9dd6575c1c90a7c Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 15:34:54 +0900 Subject: [PATCH 061/236] feat: my category fetch Apis --- .../PostCategories.controller.ts | 52 ++++++++++++++++--- .../PostCategories.repository.ts | 2 +- .../postCategories/PostCategories.service.ts | 16 +++++- .../dtos/fetch-post-category.dto.ts | 6 ++- .../entities/postCategory.entity.ts | 1 + 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 0ab046b..00b30fd 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -21,7 +21,10 @@ import { Request } from 'express'; import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -import { FetchPostCategoryDto } from './dtos/fetch-post-category.dto'; +import { + FetchPostCategoryDto, + FetchPostCategoriesDto, +} from './dtos/fetch-post-category.dto'; @ApiTags('유저 API') @Controller('users') @@ -29,20 +32,19 @@ export class PostCategoriesController { constructor(private readonly postCategoriesService: PostCategoriesService) {} @ApiOperation({ - summary: '특정 유저의 카테고리 정보 조회', + summary: '특정 유저의 카테고리 전체 조회', description: '특정 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', }) @ApiOkResponse({ - description: '', - type: [FetchPostCategoryDto], + type: [FetchPostCategoriesDto], }) - @Get(':userId/categories') + @Get(':userId/categories/list') @HttpCode(200) async fetchPostCategories( @Req() req: Request, @Param('userId') targetKakaoId: number, - ): Promise { + ): Promise { const kakaoId = req.user.userId; return await this.postCategoriesService.fetchAll({ kakaoId, @@ -71,6 +73,44 @@ export class PostCategoriesController { return await this.postCategoriesService.create({ kakaoId, name }); } + @ApiOperation({ + summary: '로그인된 유저의 카테고리 전체 조회', + description: + '로그인된 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ + type: [FetchPostCategoriesDto], + }) + @UseGuards(AuthGuardV2) + @Get('me/categories') + async fetchMyCategories( + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + console.log('kakaoId: ', kakaoId); + return await this.postCategoriesService.fetchAll({ + kakaoId, + targetKakaoId: kakaoId, + }); + } + + @ApiOperation({ + summary: '로그인된 유저의 특정 카테고리 조회', + description: '로그인된 유저가 생성한, id에 해당하는 카테고리를 조회한다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: FetchPostCategoryDto }) + @UseGuards(AuthGuardV2) + @Get('me/categories/:categoryId') + async fetchMyCategory( + @Req() req: Request, + @Param('categoryId') id: string, + ): Promise { + const kakaoId = req.user.userId; + return await this.postCategoriesService.findWithId({ kakaoId, id }); + } + @ApiOperation({ summary: '유저의 지정 카테고리 삭제하기', description: diff --git a/src/APIs/postCategories/PostCategories.repository.ts b/src/APIs/postCategories/PostCategories.repository.ts index d0ad4bb..33b4d9c 100644 --- a/src/APIs/postCategories/PostCategories.repository.ts +++ b/src/APIs/postCategories/PostCategories.repository.ts @@ -23,7 +23,7 @@ export class PostCategoriesRepository extends Repository { ) // LEFT JOIN으로 연결된 엔티티의 조건을 추가 .where('pc.userKakaoId = :userKakaoId', { userKakaoId }) .groupBy('pc.id'); // postCategory.id를 기준으로 그룹화 - console.log(userKakaoId); + console.log('??', userKakaoId); return await query.getRawMany(); } diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index 9bc6e12..81ea807 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -2,7 +2,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { PostCategoriesRepository } from './PostCategories.repository'; -import { FetchPostCategoryDto } from './dtos/fetch-post-category.dto'; +import { + FetchPostCategoriesDto, + FetchPostCategoryDto, +} from './dtos/fetch-post-category.dto'; import { NeighborsService } from '../neighbors/neighbors.service'; @Injectable() @@ -29,7 +32,16 @@ export class PostCategoriesService { return result; } - async fetchAll({ kakaoId, targetKakaoId }): Promise { + async findWithId({ kakaoId, id }): Promise { + return await this.postCategoriesRepository.findOne({ + where: { user: { kakaoId }, id }, + }); + } + + async fetchAll({ + kakaoId, + targetKakaoId, + }): Promise { const scope = await this.neighborsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, diff --git a/src/APIs/postCategories/dtos/fetch-post-category.dto.ts b/src/APIs/postCategories/dtos/fetch-post-category.dto.ts index e3e1cd5..e7bbdf8 100644 --- a/src/APIs/postCategories/dtos/fetch-post-category.dto.ts +++ b/src/APIs/postCategories/dtos/fetch-post-category.dto.ts @@ -1,6 +1,8 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { PostCategory } from '../entities/postCategory.entity'; +export class FetchPostCategoryDto extends OmitType(PostCategory, ['user']) {} -export class FetchPostCategoryDto { +export class FetchPostCategoriesDto { @ApiProperty({ type: Number }) postCount: number; diff --git a/src/APIs/postCategories/entities/postCategory.entity.ts b/src/APIs/postCategories/entities/postCategory.entity.ts index 69b6cd7..14f144f 100644 --- a/src/APIs/postCategories/entities/postCategory.entity.ts +++ b/src/APIs/postCategories/entities/postCategory.entity.ts @@ -28,6 +28,7 @@ export class PostCategory { @OneToMany(() => Posts, (posts) => posts.postCategory) posts: Posts; + @ApiProperty({ type: Number, description: '유저 아이디' }) @Column() @RelationId((postCategory: PostCategory) => postCategory.user) userKakaoId: number; From 5f05f43da974c44e7e47056d1ef82967567caf47 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 17:14:26 +0900 Subject: [PATCH 062/236] refactor: change unfollow Api from post to delete --- src/APIs/neighbors/neighbors.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/neighbors/neighbors.controller.ts index 7fd920c..7d2431b 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/neighbors/neighbors.controller.ts @@ -1,5 +1,6 @@ import { Controller, + Delete, Get, HttpCode, Param, @@ -58,7 +59,7 @@ export class NeighborsController { @ApiNoContentResponse({ description: '언팔로우 성공' }) @ApiNotFoundResponse({ description: '존재하지 않는 이웃 정보이다.' }) @UseGuards(AuthGuardV2) - @Post('cancel-follow') + @Delete('follow') @HttpCode(204) unfollowUser(@Req() req: Request, @Param('userId') to_user: number) { const kakaoId = parseInt(req.user.userId); From b3f02975aff13a26f2cc1c61255ef1a4e1c0d27d Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 18:00:52 +0900 Subject: [PATCH 063/236] feat: patch category & comment API --- src/APIs/comments/comments.controller.ts | 38 ++++++++++++++--- src/APIs/comments/comments.repository.ts | 1 - src/APIs/comments/comments.service.ts | 42 +++++++++++++------ src/APIs/comments/dtos/create-comment.dto.ts | 7 ---- src/APIs/comments/dtos/fetch-comments.dto.ts | 9 +++- src/APIs/comments/dtos/patch-comment.dto.ts | 4 ++ .../PostCategories.controller.ts | 20 +++++++++ .../postCategories/PostCategories.service.ts | 16 ++++++- .../dtos/patch-post-category.dto.ts | 4 ++ 9 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 src/APIs/comments/dtos/patch-comment.dto.ts create mode 100644 src/APIs/postCategories/dtos/patch-post-category.dto.ts diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index e24ae63..f0da84c 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -5,6 +5,7 @@ import { Get, HttpCode, Param, + Patch, Post, Req, UseGuards, @@ -20,7 +21,12 @@ import { ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; -import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { + ChildrenComment, + FetchCommentDto, + FetchCommentsDto, +} from './dtos/fetch-comments.dto'; +import { PatchCommentDto } from './dtos/patch-comment.dto'; @ApiTags('게시글 API') @Controller('posts/:postId/comments') @@ -28,21 +34,21 @@ export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @ApiOperation({ - summary: '댓글을 작성하거나 수정한다.', - description: '댓글을 작성하거나 (optional)id에 해당하는 댓글을 수정한다.', + summary: '댓글을 작성한다.', + description: '댓글을 작성한다.', }) @ApiOkResponse({ type: ChildrenComment }) @ApiCookieAuth() @Post() @UseGuards(AuthGuardV2) @HttpCode(200) - async upsertComment( + async insertComment( @Req() req: Request, @Param('postId') postsId: number, @Body() body: CreateCommentInput, ): Promise { const userKakaoId = req.user.userId; - return await this.commentsService.upsert({ ...body, postsId, userKakaoId }); + return await this.commentsService.insert({ ...body, postsId, userKakaoId }); } @ApiOperation({ @@ -56,13 +62,33 @@ export class CommentsController { return await this.commentsService.fetchComments({ postsId }); } + @ApiOperation({ summary: '특정 게시글에 대한 댓글 수정' }) + @ApiCookieAuth() + @ApiOkResponse({ type: FetchCommentDto }) + @UseGuards(AuthGuardV2) + @Patch(':commentId') + async patchComment( + @Req() req: Request, + @Param('postId') postsId: number, + @Param('commentId') id: number, + @Body() dto: PatchCommentDto, + ): Promise { + const kakaoId = req.user.userId; + return await this.commentsService.patchComment({ + kakaoId, + postsId, + id, + ...dto, + }); + } + @ApiOperation({ summary: '댓글을 삭제한다.', description: '댓글을 논리삭제한다. date_deleted 칼럼에 값이 생긴다.', }) @ApiCookieAuth() @ApiNoContentResponse({ description: '삭제 성공' }) - @Delete() + @Delete(':commentId') @UseGuards(AuthGuardV2) @HttpCode(204) async deleteComment( diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index dae13ef..6cd0b88 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -14,7 +14,6 @@ export class CommentsRepository extends Repository { .insert() .into(Comment, Object.keys(createCommentDto)) .values(createCommentDto) - .orUpdate(Object.keys(createCommentDto), ['id']) .execute(); } diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 4801275..8c0fde3 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -8,7 +9,11 @@ import { UsersService } from '../users/users.service'; import { CommentsRepository } from './comments.repository'; import { DataSource } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; -import { ChildrenComment, FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { + ChildrenComment, + FetchCommentDto, + FetchCommentsDto, +} from './dtos/fetch-comments.dto'; import { USER_PRIMARY_SELECT_OPTION } from '../users/dtos/user-response.dto'; @Injectable() @@ -37,25 +42,20 @@ export class CommentsService { } return comment; } - async upsert(createCommentDto: CreateCommentDto): Promise { + async insert(createCommentDto: CreateCommentDto): Promise { if (createCommentDto.parentId) await this.postsIdValidCheck({ parentId: createCommentDto.parentId, postsId: createCommentDto.postsId, }); - if (createCommentDto.id) { - await this.existCheck({ id: createCommentDto.id }); - } else { - // id를 입력하지 않았을 경우(생성의 경우)에만 count 증가 - await this.dataSource.manager.update(Posts, createCommentDto.postsId, { - comment_count: () => 'comment_count +1', - }); - } - const upsertData = await this.commentsRepository.upsertComment({ + await this.dataSource.manager.update(Posts, createCommentDto.postsId, { + comment_count: () => 'comment_count +1', + }); + + const commentData = await this.commentsRepository.upsertComment({ createCommentDto, }); - const id = upsertData.identifiers[0]; - console.log(id); + const id = commentData.identifiers[0]; return await this.commentsRepository.findOne({ select: { user: USER_PRIMARY_SELECT_OPTION, @@ -65,6 +65,22 @@ export class CommentsService { }); } + async patchComment({ + kakaoId, + postsId, + id, + content, + }): Promise { + const commentData = await this.existCheck({ id }); + if (!commentData) throw new NotFoundException('댓글을 찾을 수 없습니다.'); + if (commentData.postsId != postsId) + throw new NotFoundException('루트 게시글의 아이디가 일치하지 않습니다.'); + if (commentData.userKakaoId != kakaoId) + throw new ForbiddenException('댓글을 수정할 권한이 없습니다.'); + commentData.content = content; + return await this.commentsRepository.save(commentData); + } + async fetchComments({ postsId }): Promise { return await this.commentsRepository.fetchComments({ postsId }); } diff --git a/src/APIs/comments/dtos/create-comment.dto.ts b/src/APIs/comments/dtos/create-comment.dto.ts index 29903cd..d7b7f54 100644 --- a/src/APIs/comments/dtos/create-comment.dto.ts +++ b/src/APIs/comments/dtos/create-comment.dto.ts @@ -20,13 +20,6 @@ export class CreateCommentDto { }) parentId?: number; - @ApiProperty({ - description: '[optional] 수정 시 댓글 id', - type: Number, - required: false, - }) - id?: number; - userKakaoId: string; } diff --git a/src/APIs/comments/dtos/fetch-comments.dto.ts b/src/APIs/comments/dtos/fetch-comments.dto.ts index e4500cd..158ef4d 100644 --- a/src/APIs/comments/dtos/fetch-comments.dto.ts +++ b/src/APIs/comments/dtos/fetch-comments.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, PickType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { Comment } from '../entities/comment.entity'; @@ -17,6 +17,13 @@ export class ChildrenComment extends PickType(Comment, [ user: UserPrimaryResponseDto; } +export class FetchCommentDto extends OmitType(Comment, [ + 'user', + 'posts', + 'parent', + 'children', +]) {} + export class FetchCommentsDto extends PickType(Comment, [ 'id', 'userKakaoId', diff --git a/src/APIs/comments/dtos/patch-comment.dto.ts b/src/APIs/comments/dtos/patch-comment.dto.ts new file mode 100644 index 0000000..3da7670 --- /dev/null +++ b/src/APIs/comments/dtos/patch-comment.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { Comment } from '../entities/comment.entity'; + +export class PatchCommentDto extends PickType(Comment, ['content']) {} diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 00b30fd..7597938 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -5,6 +5,7 @@ import { Get, HttpCode, Param, + Patch, Post, Req, UseGuards, @@ -25,6 +26,7 @@ import { FetchPostCategoryDto, FetchPostCategoriesDto, } from './dtos/fetch-post-category.dto'; +import { PatchPostCategoryDto } from './dtos/patch-post-category.dto'; @ApiTags('유저 API') @Controller('users') @@ -111,6 +113,24 @@ export class PostCategoriesController { return await this.postCategoriesService.findWithId({ kakaoId, id }); } + @ApiOperation({ summary: '로그인된 유저의 특정 카테고리 수정' }) + @ApiCookieAuth() + @ApiOkResponse({ type: FetchPostCategoryDto }) + @UseGuards(AuthGuardV2) + @Patch('me/categories/:categoryId') + async patchCategory( + @Req() req: Request, + @Param('categoryId') id: string, + @Body() body: PatchPostCategoryDto, + ): Promise { + const kakaoId = req.user.userId; + return await this.postCategoriesService.patch({ + kakaoId, + id, + ...body, + }); + } + @ApiOperation({ summary: '유저의 지정 카테고리 삭제하기', description: diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index 81ea807..8eb6bf6 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -1,4 +1,9 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { PostCategoriesRepository } from './PostCategories.repository'; @@ -32,6 +37,15 @@ export class PostCategoriesService { return result; } + async patch({ kakaoId, id, name }): Promise { + const data = await this.findWithId({ kakaoId, id }); + if (!data) throw new NotFoundException('카테고리를 찾을 수 없습니다.'); + if (data.userKakaoId != kakaoId) + throw new ForbiddenException('카테고리를 수정할 권한이 없습니다.'); + data.name = name; + return await this.postCategoriesRepository.save(data); + } + async findWithId({ kakaoId, id }): Promise { return await this.postCategoriesRepository.findOne({ where: { user: { kakaoId }, id }, diff --git a/src/APIs/postCategories/dtos/patch-post-category.dto.ts b/src/APIs/postCategories/dtos/patch-post-category.dto.ts new file mode 100644 index 0000000..4c125ef --- /dev/null +++ b/src/APIs/postCategories/dtos/patch-post-category.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { PostCategory } from '../entities/postCategory.entity'; + +export class PatchPostCategoryDto extends PickType(PostCategory, ['name']) {} From bc15a2a4081ecd7697a990e394bdaa09eff3d211 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 19:50:29 +0900 Subject: [PATCH 064/236] fix: change querystring name from postCategoryName to category-name --- src/APIs/posts/dtos/fetch-user-posts.input.ts | 2 +- src/APIs/posts/posts.repository.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/APIs/posts/dtos/fetch-user-posts.input.ts b/src/APIs/posts/dtos/fetch-user-posts.input.ts index 9dbc534..ea43759 100644 --- a/src/APIs/posts/dtos/fetch-user-posts.input.ts +++ b/src/APIs/posts/dtos/fetch-user-posts.input.ts @@ -11,5 +11,5 @@ export class FetchUserPostsInput extends CursorFetchPosts { }) @IsOptional() @Type(() => String) - postCategoryName?: string | null; + category_name?: string | null; } diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 2f56ad5..a1c7a9f 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -160,9 +160,9 @@ export class PostsRepository extends Repository { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - if (cursorOption.postCategoryName) { - queryBuilder.andWhere('postCategory.name = :postCategoryName', { - postCategoryName: cursorOption.postCategoryName, + if (cursorOption.category_name) { + queryBuilder.andWhere('postCategory.name = :category_name', { + category_name: cursorOption.category_name, }); } queryBuilder From 023490da2899f56a5a96c20cacd7b9c3524498c8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 20:02:23 +0900 Subject: [PATCH 065/236] feat: add Date-limit option on cursor based pagination --- src/APIs/posts/dtos/cursor-fetch-posts.dto.ts | 13 +++++++++ src/APIs/posts/posts.controller.ts | 6 ++-- src/APIs/posts/posts.repository.ts | 11 +++++++ src/APIs/posts/posts.service.ts | 29 +++++++++++++++++-- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts index d4fb640..ef5c6f4 100644 --- a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts @@ -1,4 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { DateOption } from 'src/commons/enums/date-option'; import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; @@ -11,4 +13,15 @@ export class CursorFetchPosts extends CustomCursorPageOptionsDto { default: PostsOrderOptionWrap.DATE, }) order?: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; + + @ApiProperty({ + type: 'enun', + enum: DateOption, + description: '특정 기간 이후 알림 조회, null 일 경우 전체 조회', + required: false, + default: null, + }) + @IsEnum(DateOption) + @IsOptional() + date_created?: DateOption; } diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 400ffa4..5ce3769 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -216,21 +216,19 @@ export class PostsController { @ApiOperation({ summary: '[cursor]전체 게시글 조회 API', description: - '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', + '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다. PUBLIC 게시글만 조회한다.', }) @Get('cursor') @ApiOkResponse({ type: CursorPagePostResponseDto }) async fetchCursor( @Query() cursorOption: CursorFetchPosts, - @Req() req: Request, ): Promise> { - const kakaoId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return this.postsService.paginateByCustomCursor({ cursorOption, kakaoId }); + return this.postsService.paginateByCustomCursor({ cursorOption }); } @ApiOperation({ diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index a1c7a9f..01bd97c 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -186,6 +186,12 @@ export class PostsRepository extends Repository { scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], }); //sql injection 방지를 위해 만드시 enum 거칠 것 + if (cursorOption.date_created) { + queryBuilder.andWhere('date_created > :date_created', { + date_created: cursorOption.date_created, + }); + } + const posts: Posts[] = await queryBuilder.getMany(); return { posts }; @@ -199,6 +205,11 @@ export class PostsRepository extends Repository { scopes: [OpenScope.PUBLIC], }); + if (cursorOption.date_created) { + queryBuilder.andWhere('date_created > :date_created', { + date_created: cursorOption.date_created, + }); + } const posts: Posts[] = await queryBuilder.getMany(); return { posts }; diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 4ca1aa1..bbdee0e 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -31,6 +31,7 @@ import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; import { NeighborsService } from '../neighbors/neighbors.service'; +import { DateOption } from 'src/commons/enums/date-option'; @Injectable() export class PostsService { @@ -238,6 +239,9 @@ export class PostsService { targetKakaoId, cursorOption, }): Promise> { + if (cursorOption.date_created) + cursorOption.date_created = this.getDate(cursorOption.date_created); + const scope = await this.neighborsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, @@ -252,9 +256,9 @@ export class PostsService { async paginateByCustomCursor({ cursorOption, - kakaoId, }): Promise> { - console.log(kakaoId); + if (cursorOption.date_created) + cursorOption.date_created = this.getDate(cursorOption.date_created); const { posts } = await this.postsRepository.paginateByCustomCursor({ cursorOption, }); @@ -285,7 +289,8 @@ export class PostsService { cursorOption, kakaoId, }): Promise> { - console.log(kakaoId); + if (cursorOption.date_created) + cursorOption.date_created = this.getDate(cursorOption.date_created); const subQuery = await this.dataSource .createQueryBuilder(Neighbor, 'n') .select('n.toUserKakaoId') @@ -297,4 +302,22 @@ export class PostsService { }); return await this.createCursorResponse({ posts, cursorOption }); } + + getDate({ date_created }) { + let currentDate; + switch (date_created) { + case DateOption.WEEK: + currentDate.setDate(currentDate.getDate() - 7); + break; + case DateOption.MONTH: + currentDate.setMonth(currentDate.getMonth() - 1); + break; + case DateOption.YEAR: + currentDate.setFullYear(currentDate.getFullYear() - 1); + break; + default: + currentDate = null; + } + return currentDate; + } } From d62cb415feebe582c3de43b77f3a8d20d4376ce4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 20:34:36 +0900 Subject: [PATCH 066/236] fix: date-sort option --- src/APIs/posts/dtos/cursor-fetch-posts.dto.ts | 2 +- src/APIs/posts/posts.repository.ts | 10 ++++++++-- src/APIs/posts/posts.service.ts | 6 ++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts index ef5c6f4..ce37657 100644 --- a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts @@ -17,7 +17,7 @@ export class CursorFetchPosts extends CustomCursorPageOptionsDto { @ApiProperty({ type: 'enun', enum: DateOption, - description: '특정 기간 이후 알림 조회, null 일 경우 전체 조회', + description: '특정 기간 이후 게시글 조회, null 일 경우 전체 조회', required: false, default: null, }) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 01bd97c..abfcee8 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -171,6 +171,12 @@ export class PostsRepository extends Repository { }) .andWhere('p.scope IN (:scope)', { scope }); + if (cursorOption.date_created) { + queryBuilder.andWhere('p.date_created > :date_created', { + date_created: cursorOption.date_created, + }); + } + const posts: Posts[] = await queryBuilder.getMany(); return { posts }; @@ -187,7 +193,7 @@ export class PostsRepository extends Repository { }); //sql injection 방지를 위해 만드시 enum 거칠 것 if (cursorOption.date_created) { - queryBuilder.andWhere('date_created > :date_created', { + queryBuilder.andWhere('p.date_created > :date_created', { date_created: cursorOption.date_created, }); } @@ -206,7 +212,7 @@ export class PostsRepository extends Repository { }); if (cursorOption.date_created) { - queryBuilder.andWhere('date_created > :date_created', { + queryBuilder.andWhere('p.date_created > :date_created', { date_created: cursorOption.date_created, }); } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index bbdee0e..82b7827 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -210,8 +210,6 @@ export class PostsService { let customCursor: string; const takePerPage = cursorOption.take; - console.log(posts.length); - console.log(cursorOption); const isLastPage = posts.length <= takePerPage; const responseData = posts.slice(0, takePerPage); const lastDataPerPage = responseData[responseData.length - 1]; @@ -303,8 +301,8 @@ export class PostsService { return await this.createCursorResponse({ posts, cursorOption }); } - getDate({ date_created }) { - let currentDate; + getDate(date_created) { + let currentDate = new Date(); switch (date_created) { case DateOption.WEEK: currentDate.setDate(currentDate.getDate() - 7); From 0e027eb065629f9d0a7aade96b8f153f7da27f05 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 9 May 2024 20:49:25 +0900 Subject: [PATCH 067/236] feat: agreements Entity --- src/APIs/agreements/agreements.controller.ts | 0 src/APIs/agreements/agreements.module.ts | 0 src/APIs/agreements/agreements.service.ts | 0 .../agreements/entities/agreement.entity.ts | 60 +++++++++++++++++++ src/commons/enums/agreement-type.enum.ts | 6 ++ 5 files changed, 66 insertions(+) create mode 100644 src/APIs/agreements/agreements.controller.ts create mode 100644 src/APIs/agreements/agreements.module.ts create mode 100644 src/APIs/agreements/agreements.service.ts create mode 100644 src/APIs/agreements/entities/agreement.entity.ts create mode 100644 src/commons/enums/agreement-type.enum.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/agreements/agreements.module.ts b/src/APIs/agreements/agreements.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts new file mode 100644 index 0000000..4feac6c --- /dev/null +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -0,0 +1,60 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; +import { User } from 'src/APIs/users/entities/user.entity'; +import { AgreementType } from 'src/commons/enums/agreement-type.enum'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Agreement { + @ApiProperty({ type: Number, description: 'PK: A_I_' }) + @PrimaryGeneratedColumn() + id: number; + + @Column() + @ManyToOne(() => User, (users) => users.kakaoId, { + nullable: false, + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', + }) + user: User; + + @ApiProperty({ type: Number, description: '약관에 동의한 유저 id' }) + @RelationId((agreement: Agreement) => agreement.user) + @IsNumber() + userKakaoId: number; + + @ApiProperty({ + type: 'enum', + enum: AgreementType, + description: '약관의 종류', + }) + @Column() + @IsEnum(AgreementType) + agreementType: AgreementType; + + @ApiProperty({ type: Boolean, description: '약관 동의 유무' }) + @Column({ default: false }) + @IsBoolean() + isAgreed: boolean; // 동의 여부, 기본값은 false + + @ApiProperty({ type: Date, description: '생성된 날짜' }) + @CreateDateColumn() + date_created: Date; + + @ApiProperty({ type: Date, description: '수정된 날짜' }) + @UpdateDateColumn() + date_updated: Date; + + @ApiProperty({ type: Date, description: '삭제된 날짜' }) + @DeleteDateColumn() + date_deleted: Date; +} diff --git a/src/commons/enums/agreement-type.enum.ts b/src/commons/enums/agreement-type.enum.ts new file mode 100644 index 0000000..d3823ef --- /dev/null +++ b/src/commons/enums/agreement-type.enum.ts @@ -0,0 +1,6 @@ +export enum AgreementType { + PRIVACY_POLICY = 'PRIVACY_POLICY', // 개인정보 처리방침 + TERMS_OF_SERVICE = 'TERMS_OF_SERVICE', // 이용약관 + MARKETING_CONSENT = 'MARKETING_CONSENT', // 마케팅 수신 동의 + CUSTOM_AGREEMENT = 'CUSTOM_AGREEMENT', // 사용자 정의 약관 +} From 98e05846d9fe75dc37884fb6396001e6a4ded4c6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 10 May 2024 12:38:08 +0900 Subject: [PATCH 068/236] refactor: directory structure rename commons => common create src/assets move readme-assets/logo.png => src/assets/readme/logo.png --- src/APIs/agreements/entities/agreement.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 4feac6c..e61b525 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; -import { AgreementType } from 'src/commons/enums/agreement-type.enum'; +import { AgreementType } from 'src/common/enums/agreement-type.enum'; import { Column, CreateDateColumn, From a88a013a41904cbb2d9266c66b0081d1e1a44aae Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 10 May 2024 12:38:35 +0900 Subject: [PATCH 069/236] refactor: directory structure rename commons => common create src/assets move readme-assets/logo.png => src/assets/readme/logo.png --- README.md | 2 +- src/APIs/agreements/agreements.controller.ts | 7 +++++++ src/APIs/agreements/agreements.repository.ts | 8 ++++++++ src/APIs/agreements/agreements.service.ts | 7 +++++++ src/APIs/agreements/dtos/create-agreements.dto.ts | 10 ++++++++++ src/APIs/announcements/announcements.controller.ts | 2 +- src/APIs/comments/comments.controller.ts | 2 +- src/APIs/likes/likes.controller.ts | 2 +- src/APIs/neighbors/neighbors.controller.ts | 2 +- src/APIs/neighbors/neighbors.service.ts | 2 +- src/APIs/notifications/dtos/fetch-noti.dto.ts | 2 +- .../notifications/entities/notification.entity.ts | 2 +- src/APIs/notifications/notifications.controller.ts | 2 +- src/APIs/notifications/notifications.service.ts | 2 +- .../postBackgrounds/postBackgrounds.controller.ts | 4 ++-- src/APIs/postBackgrounds/postBackgrounds.service.ts | 2 +- .../postCategories/PostCategories.controller.ts | 2 +- src/APIs/posts/dtos/create-post.input.ts | 2 +- src/APIs/posts/dtos/cursor-fetch-posts.dto.ts | 4 ++-- src/APIs/posts/dtos/fetch-posts.dto.ts | 4 ++-- src/APIs/posts/dtos/publish-post.input.ts | 4 ++-- src/APIs/posts/entities/posts.entity.ts | 2 +- src/APIs/posts/posts.controller.ts | 8 ++++---- src/APIs/posts/posts.repository.ts | 8 ++++---- src/APIs/posts/posts.service.ts | 6 +++--- src/APIs/reports/entities/report.entity.ts | 4 ++-- src/APIs/reports/reports.controller.ts | 2 +- src/APIs/reports/reports.service.ts | 2 +- .../stickerCategories.controller.ts | 2 +- src/APIs/stickers/stickers.controller.ts | 6 +++--- src/APIs/stickers/stickers.service.ts | 2 +- src/APIs/users/users.controller.ts | 6 +++--- src/APIs/users/users.service.ts | 2 +- src/app.module.ts | 2 +- {readme-assets => src/assets/readme}/logo.png | Bin src/{commons => common}/context.ts | 0 .../dto/image-upload-response.dto.ts | 0 src/{commons => common}/dto/image-upload.dto.ts | 0 .../enums/agreement-type.enum.ts | 0 src/{commons => common}/enums/date-option.ts | 0 src/{commons => common}/enums/not-type.enum.ts | 0 src/{commons => common}/enums/open-scope.enum.ts | 0 .../enums/posts-filter-option.ts | 0 src/{commons => common}/enums/posts-order-option.ts | 0 src/{commons => common}/enums/report-target.enum.ts | 0 src/{commons => common}/enums/report-type.enum.ts | 0 src/{commons => common}/enums/sort-option.ts | 0 .../filter/http-exception.filter.ts | 0 src/{commons => common}/guards/auth.guard.ts | 0 .../middlewares/auth-token.middleware.ts | 0 src/{commons => common}/types/express.d.ts | 0 src/{commons => common}/validators/isBoolean.ts | 0 src/main.ts | 2 +- .../cursor-pages/dtos/cursor-page-option.dto.ts | 2 +- 54 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 src/APIs/agreements/agreements.repository.ts create mode 100644 src/APIs/agreements/dtos/create-agreements.dto.ts rename {readme-assets => src/assets/readme}/logo.png (100%) rename src/{commons => common}/context.ts (100%) rename src/{commons => common}/dto/image-upload-response.dto.ts (100%) rename src/{commons => common}/dto/image-upload.dto.ts (100%) rename src/{commons => common}/enums/agreement-type.enum.ts (100%) rename src/{commons => common}/enums/date-option.ts (100%) rename src/{commons => common}/enums/not-type.enum.ts (100%) rename src/{commons => common}/enums/open-scope.enum.ts (100%) rename src/{commons => common}/enums/posts-filter-option.ts (100%) rename src/{commons => common}/enums/posts-order-option.ts (100%) rename src/{commons => common}/enums/report-target.enum.ts (100%) rename src/{commons => common}/enums/report-type.enum.ts (100%) rename src/{commons => common}/enums/sort-option.ts (100%) rename src/{commons => common}/filter/http-exception.filter.ts (100%) rename src/{commons => common}/guards/auth.guard.ts (100%) rename src/{commons => common}/middlewares/auth-token.middleware.ts (100%) rename src/{commons => common}/types/express.d.ts (100%) rename src/{commons => common}/validators/isBoolean.ts (100%) diff --git a/README.md b/README.md index e416011..1841be2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

블꾸

커스텀 극대화 블로그

blccu.com diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index e69de29..e3922df 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { AgreementsService } from './agreements.service'; + +@Controller('agreements') +export class AgreementsController { + constructor(private readonly agreementsService: AgreementsService) {} +} diff --git a/src/APIs/agreements/agreements.repository.ts b/src/APIs/agreements/agreements.repository.ts new file mode 100644 index 0000000..ba38b16 --- /dev/null +++ b/src/APIs/agreements/agreements.repository.ts @@ -0,0 +1,8 @@ +import { DataSource, Repository } from 'typeorm'; +import { Agreement } from './entities/agreement.entity'; + +export class AgreementsRepository extends Repository { + constructor(private dataSource: DataSource) { + super(Agreement, dataSource.createEntityManager()); + } +} diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index e69de29..b023a05 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { AgreementsRepository } from './agreements.repository'; + +@Injectable() +export class AgreementsService { + constructor(private readonly agreementsRepository: AgreementsRepository) {} +} diff --git a/src/APIs/agreements/dtos/create-agreements.dto.ts b/src/APIs/agreements/dtos/create-agreements.dto.ts new file mode 100644 index 0000000..7e2d7ec --- /dev/null +++ b/src/APIs/agreements/dtos/create-agreements.dto.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { Agreement } from '../entities/agreement.entity'; + +export class CreateAgreementsInput extends OmitType(Agreement, [ + 'id', + 'user', + 'date_created', + 'date_deleted', + 'date_updated', +]) {} diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index e55ca05..4ea3f36 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -18,7 +18,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; import { CreateAnouncementInput } from './dtos/create-announcement.dto'; import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index f0da84c..5b83aa9 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -20,7 +20,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { ChildrenComment, FetchCommentDto, diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index bd2cf68..75f4a76 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -19,7 +19,7 @@ import { Request } from 'express'; import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('게시글 API') @Controller('posts/:postId/like') diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/neighbors/neighbors.controller.ts index 7d2431b..053c865 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/neighbors/neighbors.controller.ts @@ -23,7 +23,7 @@ import { import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('유저 API') @Controller('users/:userId') diff --git a/src/APIs/neighbors/neighbors.service.ts b/src/APIs/neighbors/neighbors.service.ts index 1d07db4..f2960be 100644 --- a/src/APIs/neighbors/neighbors.service.ts +++ b/src/APIs/neighbors/neighbors.service.ts @@ -7,7 +7,7 @@ import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; import e from 'express'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; @Injectable() export class NeighborsService { diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts index a5062cb..4473f65 100644 --- a/src/APIs/notifications/dtos/fetch-noti.dto.ts +++ b/src/APIs/notifications/dtos/fetch-noti.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { DateOption } from 'src/commons/enums/date-option'; +import { DateOption } from 'src/common/enums/date-option'; import { Notification } from '../entities/notification.entity'; export class FetchNotiInput { diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index d750ac6..daaacae 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { User } from 'src/APIs/users/entities/user.entity'; -import { NotType } from 'src/commons/enums/not-type.enum'; +import { NotType } from 'src/common/enums/not-type.enum'; import { Column, CreateDateColumn, diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index c48d5fb..f42aee6 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -21,7 +21,7 @@ import { import { Request } from 'express'; import { EmitNotiInput } from './dtos/emit-noti.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @ApiTags('알림 API') diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index dbb456c..4f257e3 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -4,7 +4,7 @@ import { Subject, filter, map } from 'rxjs'; import { Notification } from './entities/notification.entity'; import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; -import { DateOption } from 'src/commons/enums/date-option'; +import { DateOption } from 'src/common/enums/date-option'; @Injectable() export class NotificationsService { diff --git a/src/APIs/postBackgrounds/postBackgrounds.controller.ts b/src/APIs/postBackgrounds/postBackgrounds.controller.ts index dc7410b..3cb511b 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.controller.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.controller.ts @@ -17,8 +17,8 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from '../../commons/dto/image-upload.dto'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadDto } from '../../common/dto/image-upload.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { PostBackground } from './entities/postBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; diff --git a/src/APIs/postBackgrounds/postBackgrounds.service.ts b/src/APIs/postBackgrounds/postBackgrounds.service.ts index 518ade2..0cd80fc 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.service.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.service.ts @@ -4,7 +4,7 @@ import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { PostBackground } from './entities/postBackground.entity'; import { Repository } from 'typeorm'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; @Injectable() export class PostBackgroundsService { diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 7597938..34ef062 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -21,7 +21,7 @@ import { PostCategoriesService } from './PostCategories.service'; import { Request } from 'express'; import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchPostCategoryDto, FetchPostCategoriesDto, diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index 851c51d..c846d88 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -6,7 +6,7 @@ import { IsOptional, IsString, } from 'class-validator'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; export class CreatePostInput { @ApiProperty({ diff --git a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts index ce37657..b165e6e 100644 --- a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { DateOption } from 'src/commons/enums/date-option'; -import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; +import { DateOption } from 'src/common/enums/date-option'; +import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; export class CursorFetchPosts extends CustomCursorPageOptionsDto { diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index 5fa6baf..dfbca91 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -1,8 +1,8 @@ import { IsEnum, IsOptional, IsString } from 'class-validator'; import { PageRequest } from '../../../utils/pages/page-request'; import { ApiProperty } from '@nestjs/swagger'; -import { PostsOrderOptionWrap } from 'src/commons/enums/posts-order-option'; -import { PostsFilterOptionWrap } from 'src/commons/enums/posts-filter-option'; +import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; +import { PostsFilterOptionWrap } from 'src/common/enums/posts-filter-option'; export class FetchPostsDto extends PageRequest { @ApiProperty({ diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index bc49f37..11b1a57 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; -import { IsBoolean } from 'src/commons/validators/isBoolean'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; +import { IsBoolean } from 'src/common/validators/isBoolean'; export class PublishPostInput { @ApiProperty({ diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index 4f4a6e7..745126a 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -3,7 +3,7 @@ import { IsString } from 'class-validator'; import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; import { User } from 'src/APIs/users/entities/user.entity'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; import { Column, CreateDateColumn, diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 5ce3769..d6684cf 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -28,11 +28,11 @@ import { PublishPostDto } from './dtos/publish-post.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { CreatePostInput } from './dtos/create-post.input'; import { PublishPostInput } from './dtos/publish-post.input'; -import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; +import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { PostResponseDto } from './dtos/post-response.dto'; import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; import { @@ -40,7 +40,7 @@ import { PostResponseDtoExceptCategory, } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { SortOption } from 'src/commons/enums/sort-option'; +import { SortOption } from 'src/common/enums/sort-option'; import { CursorFetchPosts } from './dtos/cursor-fetch-posts.dto'; import { CursorPagePostResponseDto } from './dtos/cursor-page-post-response.dto'; diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index abfcee8..d48e22e 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -1,12 +1,12 @@ import { DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; -import { OpenScope } from 'src/commons/enums/open-scope.enum'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; -import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; -import { PostsFilterOption } from 'src/commons/enums/posts-filter-option'; -import { SortOption } from 'src/commons/enums/sort-option'; +import { PostsOrderOption } from 'src/common/enums/posts-order-option'; +import { PostsFilterOption } from 'src/common/enums/posts-filter-option'; +import { SortOption } from 'src/common/enums/sort-option'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 82b7827..21000ae 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -17,7 +17,7 @@ import { CreatePostDto } from './dtos/create-post.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { User } from '../users/entities/user.entity'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; import { CommentsService } from '../comments/comments.service'; @@ -29,9 +29,9 @@ import { } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { PostsOrderOption } from 'src/commons/enums/posts-order-option'; +import { PostsOrderOption } from 'src/common/enums/posts-order-option'; import { NeighborsService } from '../neighbors/neighbors.service'; -import { DateOption } from 'src/commons/enums/date-option'; +import { DateOption } from 'src/common/enums/date-option'; @Injectable() export class PostsService { diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 5ad6d4b..eaaed57 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -3,8 +3,8 @@ import { IsEnum } from 'class-validator'; import { Comment } from 'src/APIs/comments/entities/comment.entity'; import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; -import { ReportTarget } from 'src/commons/enums/report-target.enum'; -import { ReportType } from 'src/commons/enums/report-type.enum'; +import { ReportTarget } from 'src/common/enums/report-target.enum'; +import { ReportType } from 'src/common/enums/report-type.enum'; import { Column, CreateDateColumn, diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 495fe07..2de11ee 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -16,7 +16,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { CreateReportInput } from './dtos/create-report.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; import { FetchReportResponse } from './dtos/fetch-report.dto'; diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 8e34570..9615508 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -9,7 +9,7 @@ import { DataSource, Repository } from 'typeorm'; import { CreateReportDto } from './dtos/create-report.dto'; import { UsersService } from '../users/users.service'; import { FetchReportResponse } from './dtos/fetch-report.dto'; -import { ReportTarget } from 'src/commons/enums/report-target.enum'; +import { ReportTarget } from 'src/common/enums/report-target.enum'; import { Posts } from '../posts/entities/posts.entity'; import { Comment } from '../comments/entities/comment.entity'; diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index c75b266..1326a0e 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -17,7 +17,7 @@ import { import { Request } from 'express'; import { MapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('스티커 카테고리 API') @Controller('stickercg') diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 82cb238..e1a6a44 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -23,15 +23,15 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; +import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; import { RemoveBgDto } from './dtos/remove-bg.dto'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { FindStickerInput } from './dtos/find-sticker.dto'; import { UpdateStickerInput } from './dtos/update-sticker.dto'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('스티커 API') @Controller('stickers') diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index f25b027..ff84b68 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -9,7 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { CreateStickerDto } from './dtos/create-sticker.dto'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; import { removeBackground } from '@imgly/background-removal-node'; import { FindStickerDto } from './dtos/find-sticker.dto'; diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 7f79eb2..f126a09 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -24,10 +24,10 @@ import { import { Request } from 'express'; import { UserResponseDto } from './dtos/user-response.dto'; import { PatchUserInput } from './dtos/patch-user.input'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; -import { ImageUploadDto } from 'src/commons/dto/image-upload.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; +import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; -import { AuthGuardV2 } from 'src/commons/guards/auth.guard'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('유저 API') @Controller('users') diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 86a3cf2..4bfd5d6 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -12,7 +12,7 @@ import { IUsersServiceFindUserByKakaoId, } from './interfaces/users.service.interface'; import { USER_SELECT_OPTION, UserResponseDto } from './dtos/user-response.dto'; -import { ImageUploadResponseDto } from 'src/commons/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; diff --git a/src/app.module.ts b/src/app.module.ts index 596feeb..47deda0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,7 +17,7 @@ import { StickerBlocksModule } from './APIs/stickerBlocks/stickerBlocks.module'; import { NotificationsModule } from './APIs/notifications/notifications.module'; import { AnnouncementsModule } from './APIs/announcements/announcements.module'; import { ReportsModule } from './APIs/reports/reports.module'; -import { AuthTokenMiddleware } from './commons/middlewares/auth-token.middleware'; +import { AuthTokenMiddleware } from './common/middlewares/auth-token.middleware'; import { JwtModule } from '@nestjs/jwt'; @Module({ diff --git a/readme-assets/logo.png b/src/assets/readme/logo.png similarity index 100% rename from readme-assets/logo.png rename to src/assets/readme/logo.png diff --git a/src/commons/context.ts b/src/common/context.ts similarity index 100% rename from src/commons/context.ts rename to src/common/context.ts diff --git a/src/commons/dto/image-upload-response.dto.ts b/src/common/dto/image-upload-response.dto.ts similarity index 100% rename from src/commons/dto/image-upload-response.dto.ts rename to src/common/dto/image-upload-response.dto.ts diff --git a/src/commons/dto/image-upload.dto.ts b/src/common/dto/image-upload.dto.ts similarity index 100% rename from src/commons/dto/image-upload.dto.ts rename to src/common/dto/image-upload.dto.ts diff --git a/src/commons/enums/agreement-type.enum.ts b/src/common/enums/agreement-type.enum.ts similarity index 100% rename from src/commons/enums/agreement-type.enum.ts rename to src/common/enums/agreement-type.enum.ts diff --git a/src/commons/enums/date-option.ts b/src/common/enums/date-option.ts similarity index 100% rename from src/commons/enums/date-option.ts rename to src/common/enums/date-option.ts diff --git a/src/commons/enums/not-type.enum.ts b/src/common/enums/not-type.enum.ts similarity index 100% rename from src/commons/enums/not-type.enum.ts rename to src/common/enums/not-type.enum.ts diff --git a/src/commons/enums/open-scope.enum.ts b/src/common/enums/open-scope.enum.ts similarity index 100% rename from src/commons/enums/open-scope.enum.ts rename to src/common/enums/open-scope.enum.ts diff --git a/src/commons/enums/posts-filter-option.ts b/src/common/enums/posts-filter-option.ts similarity index 100% rename from src/commons/enums/posts-filter-option.ts rename to src/common/enums/posts-filter-option.ts diff --git a/src/commons/enums/posts-order-option.ts b/src/common/enums/posts-order-option.ts similarity index 100% rename from src/commons/enums/posts-order-option.ts rename to src/common/enums/posts-order-option.ts diff --git a/src/commons/enums/report-target.enum.ts b/src/common/enums/report-target.enum.ts similarity index 100% rename from src/commons/enums/report-target.enum.ts rename to src/common/enums/report-target.enum.ts diff --git a/src/commons/enums/report-type.enum.ts b/src/common/enums/report-type.enum.ts similarity index 100% rename from src/commons/enums/report-type.enum.ts rename to src/common/enums/report-type.enum.ts diff --git a/src/commons/enums/sort-option.ts b/src/common/enums/sort-option.ts similarity index 100% rename from src/commons/enums/sort-option.ts rename to src/common/enums/sort-option.ts diff --git a/src/commons/filter/http-exception.filter.ts b/src/common/filter/http-exception.filter.ts similarity index 100% rename from src/commons/filter/http-exception.filter.ts rename to src/common/filter/http-exception.filter.ts diff --git a/src/commons/guards/auth.guard.ts b/src/common/guards/auth.guard.ts similarity index 100% rename from src/commons/guards/auth.guard.ts rename to src/common/guards/auth.guard.ts diff --git a/src/commons/middlewares/auth-token.middleware.ts b/src/common/middlewares/auth-token.middleware.ts similarity index 100% rename from src/commons/middlewares/auth-token.middleware.ts rename to src/common/middlewares/auth-token.middleware.ts diff --git a/src/commons/types/express.d.ts b/src/common/types/express.d.ts similarity index 100% rename from src/commons/types/express.d.ts rename to src/common/types/express.d.ts diff --git a/src/commons/validators/isBoolean.ts b/src/common/validators/isBoolean.ts similarity index 100% rename from src/commons/validators/isBoolean.ts rename to src/common/validators/isBoolean.ts diff --git a/src/main.ts b/src/main.ts index 84239e2..32f8e2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import cookieParser from 'cookie-parser'; -import { HttpExceptionFilter } from './commons/filter/http-exception.filter'; +import { HttpExceptionFilter } from './common/filter/http-exception.filter'; import { ValidationPipe } from '@nestjs/common'; import expressBasicAuth from 'express-basic-auth'; // import * as expressBasicAuth from 'express-basic-auth'; diff --git a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts index a80a60e..d793277 100644 --- a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts +++ b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsOptional } from 'class-validator'; -import { SortOption } from 'src/commons/enums/sort-option'; +import { SortOption } from 'src/common/enums/sort-option'; export class CustomCursorPageOptionsDto { @ApiProperty({ From 3aa62939827c26cfc630ea46b6640a487cf61989 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 16:22:41 +0900 Subject: [PATCH 070/236] feat: create agreements api --- src/APIs/agreements/agreements.controller.ts | 61 ++++++++++++++++++- src/APIs/agreements/agreements.module.ts | 13 ++++ src/APIs/agreements/agreements.repository.ts | 2 + src/APIs/agreements/agreements.service.ts | 9 +++ .../agreements/dtos/create-agreements.dto.ts | 1 + .../agreements/entities/agreement.entity.ts | 3 +- .../agreements.service.interface.ts | 14 +++++ src/app.module.ts | 2 + src/assets/terms/CUSTOM_AGREEMENT.txt | 0 src/assets/terms/MARKETING_CONSENT.txt | 0 src/assets/terms/PRIVACY_POLICY.txt | 0 src/assets/terms/TERMS_OF_SERVICE.txt | 0 12 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/APIs/agreements/interfaces/agreements.service.interface.ts create mode 100644 src/assets/terms/CUSTOM_AGREEMENT.txt create mode 100644 src/assets/terms/MARKETING_CONSENT.txt create mode 100644 src/assets/terms/PRIVACY_POLICY.txt create mode 100644 src/assets/terms/TERMS_OF_SERVICE.txt diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index e3922df..d197012 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -1,7 +1,64 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { AgreementsService } from './agreements.service'; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOperation, +} from '@nestjs/swagger'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request } from 'express'; +import { CreateAgreementsInput } from './dtos/create-agreements.dto'; -@Controller('agreements') +@Controller('users') export class AgreementsController { constructor(private readonly agreementsService: AgreementsService) {} + + @ApiOperation({}) + @ApiCookieAuth() + @ApiCreatedResponse({}) + @UseGuards(AuthGuardV2) + @Post('me/agreement') + async agree(@Req() req: Request, @Body() body: CreateAgreementsInput) { + const kakaoId = req.user.kakaoId; + await this.agreementsService.create({ ...body, kakaoId }); + } + + @ApiOperation({}) + @ApiCookieAuth() + @ApiCreatedResponse({}) + @UseGuards(AuthGuardV2) + @Get('me/agreements') + async fetchAgreement(@Req() req: Request) { + const kakaoId = req.user.kakaoId; + } + + @ApiOperation({}) + @ApiCookieAuth() + @ApiCreatedResponse({}) + @UseGuards(AuthGuardV2) + @Get(':userId/agreements') + async fetchAgreementAdmin( + @Req() req: Request, + @Param('userId') targetUserKakaoId: number, + ) { + const kakaoId = req.user.kakaoId; + } + + @ApiOperation({}) + @ApiCookieAuth() + @ApiCreatedResponse({}) + @UseGuards(AuthGuardV2) + @Patch('me/agreement/:agreementId') + async patchAgreement(@Req() req: Request, @Param('agreementId') id: number) { + const kakaoId = req.user.kakaoId; + } } diff --git a/src/APIs/agreements/agreements.module.ts b/src/APIs/agreements/agreements.module.ts index e69de29..1743741 100644 --- a/src/APIs/agreements/agreements.module.ts +++ b/src/APIs/agreements/agreements.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Agreement } from './entities/agreement.entity'; +import { AgreementsController } from './agreements.controller'; +import { AgreementsService } from './agreements.service'; +import { AgreementsRepository } from './agreements.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([Agreement])], + controllers: [AgreementsController], + providers: [AgreementsService, AgreementsRepository], +}) +export class AgreementsModule {} diff --git a/src/APIs/agreements/agreements.repository.ts b/src/APIs/agreements/agreements.repository.ts index ba38b16..085f061 100644 --- a/src/APIs/agreements/agreements.repository.ts +++ b/src/APIs/agreements/agreements.repository.ts @@ -1,6 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { Agreement } from './entities/agreement.entity'; +import { Injectable } from '@nestjs/common'; +@Injectable() export class AgreementsRepository extends Repository { constructor(private dataSource: DataSource) { super(Agreement, dataSource.createEntityManager()); diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index b023a05..f4fabc8 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -1,7 +1,16 @@ import { Injectable } from '@nestjs/common'; import { AgreementsRepository } from './agreements.repository'; +import { IAgreementsServiceCreate } from './interfaces/agreements.service.interface'; @Injectable() export class AgreementsService { constructor(private readonly agreementsRepository: AgreementsRepository) {} + + async create({ kakaoId, agreementType, isAgreed }: IAgreementsServiceCreate) { + await this.agreementsRepository.save({ + agreementType, + isAgreed, + user: { kakaoId }, + }); + } } diff --git a/src/APIs/agreements/dtos/create-agreements.dto.ts b/src/APIs/agreements/dtos/create-agreements.dto.ts index 7e2d7ec..bcdc0f0 100644 --- a/src/APIs/agreements/dtos/create-agreements.dto.ts +++ b/src/APIs/agreements/dtos/create-agreements.dto.ts @@ -4,6 +4,7 @@ import { Agreement } from '../entities/agreement.entity'; export class CreateAgreementsInput extends OmitType(Agreement, [ 'id', 'user', + 'userKakaoId', 'date_created', 'date_deleted', 'date_updated', diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index e61b525..6552b39 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -7,6 +7,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId, @@ -19,7 +20,7 @@ export class Agreement { @PrimaryGeneratedColumn() id: number; - @Column() + @JoinColumn() @ManyToOne(() => User, (users) => users.kakaoId, { nullable: false, onUpdate: 'NO ACTION', diff --git a/src/APIs/agreements/interfaces/agreements.service.interface.ts b/src/APIs/agreements/interfaces/agreements.service.interface.ts new file mode 100644 index 0000000..dcc7288 --- /dev/null +++ b/src/APIs/agreements/interfaces/agreements.service.interface.ts @@ -0,0 +1,14 @@ +import { Agreement } from '../entities/agreement.entity'; + +export interface IAgreementsServiceCreate + extends Omit< + Agreement, + | 'id' + | 'user' + | 'userKakaoId' + | 'date_created' + | 'date_updated' + | 'date_deleted' + > { + kakaoId: number; +} diff --git a/src/app.module.ts b/src/app.module.ts index 47deda0..9fd9e5c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,10 +19,12 @@ import { AnnouncementsModule } from './APIs/announcements/announcements.module'; import { ReportsModule } from './APIs/reports/reports.module'; import { AuthTokenMiddleware } from './common/middlewares/auth-token.middleware'; import { JwtModule } from '@nestjs/jwt'; +import { AgreementsModule } from './APIs/agreements/agreements.module'; @Module({ imports: [ AnnouncementsModule, + AgreementsModule, StickersModule, StickerCategoriesModule, StickerBlocksModule, diff --git a/src/assets/terms/CUSTOM_AGREEMENT.txt b/src/assets/terms/CUSTOM_AGREEMENT.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/terms/MARKETING_CONSENT.txt b/src/assets/terms/MARKETING_CONSENT.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/terms/PRIVACY_POLICY.txt b/src/assets/terms/PRIVACY_POLICY.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/terms/TERMS_OF_SERVICE.txt b/src/assets/terms/TERMS_OF_SERVICE.txt new file mode 100644 index 0000000..e69de29 From 9b0e8be33d734f5cf15d689b7bb87409375732b6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 16:35:30 +0900 Subject: [PATCH 071/236] refactor: neighbor -> follow --- .../dtos/follow-user.dto.ts | 0 .../dtos/from-user-response.dto.ts | 4 ++-- .../dtos/to-user-response.dto.ts | 4 ++-- .../entities/follow.entity.ts} | 6 +++--- .../follows.controller.ts} | 14 ++++++------- src/APIs/follows/follows.module.ts | 15 +++++++++++++ .../follows.service.ts} | 21 +++++++++---------- src/APIs/neighbors/neighbors.module.ts | 15 ------------- .../postCategories/PostCategories.module.ts | 4 ++-- .../postCategories/PostCategories.service.ts | 6 +++--- src/APIs/posts/posts.module.ts | 5 ++--- src/APIs/posts/posts.service.ts | 14 ++++++------- src/app.module.ts | 4 ++-- 13 files changed, 55 insertions(+), 57 deletions(-) rename src/APIs/{neighbors => follows}/dtos/follow-user.dto.ts (100%) rename src/APIs/{neighbors => follows}/dtos/from-user-response.dto.ts (67%) rename src/APIs/{neighbors => follows}/dtos/to-user-response.dto.ts (67%) rename src/APIs/{neighbors/entities/neighbor.entity.ts => follows/entities/follow.entity.ts} (83%) rename src/APIs/{neighbors/neighbors.controller.ts => follows/follows.controller.ts} (86%) create mode 100644 src/APIs/follows/follows.module.ts rename src/APIs/{neighbors/neighbors.service.ts => follows/follows.service.ts} (82%) delete mode 100644 src/APIs/neighbors/neighbors.module.ts diff --git a/src/APIs/neighbors/dtos/follow-user.dto.ts b/src/APIs/follows/dtos/follow-user.dto.ts similarity index 100% rename from src/APIs/neighbors/dtos/follow-user.dto.ts rename to src/APIs/follows/dtos/follow-user.dto.ts diff --git a/src/APIs/neighbors/dtos/from-user-response.dto.ts b/src/APIs/follows/dtos/from-user-response.dto.ts similarity index 67% rename from src/APIs/neighbors/dtos/from-user-response.dto.ts rename to src/APIs/follows/dtos/from-user-response.dto.ts index fb155ab..530fbdb 100644 --- a/src/APIs/neighbors/dtos/from-user-response.dto.ts +++ b/src/APIs/follows/dtos/from-user-response.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Neighbor } from '../entities/neighbor.entity'; +import { Follow } from '../entities/follow.entity'; -export class FromUserResponseDto extends OmitType(Neighbor, [ +export class FromUserResponseDto extends OmitType(Follow, [ 'from_user', 'to_user', ]) { diff --git a/src/APIs/neighbors/dtos/to-user-response.dto.ts b/src/APIs/follows/dtos/to-user-response.dto.ts similarity index 67% rename from src/APIs/neighbors/dtos/to-user-response.dto.ts rename to src/APIs/follows/dtos/to-user-response.dto.ts index f35acaa..10f1b2b 100644 --- a/src/APIs/neighbors/dtos/to-user-response.dto.ts +++ b/src/APIs/follows/dtos/to-user-response.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Neighbor } from '../entities/neighbor.entity'; +import { Follow } from '../entities/follow.entity'; -export class ToUserResponseDto extends OmitType(Neighbor, [ +export class ToUserResponseDto extends OmitType(Follow, [ 'from_user', 'to_user', ]) { diff --git a/src/APIs/neighbors/entities/neighbor.entity.ts b/src/APIs/follows/entities/follow.entity.ts similarity index 83% rename from src/APIs/neighbors/entities/neighbor.entity.ts rename to src/APIs/follows/entities/follow.entity.ts index 2407691..07c41d0 100644 --- a/src/APIs/neighbors/entities/neighbor.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -10,7 +10,7 @@ import { } from 'typeorm'; @Entity() -export class Neighbor { +export class Follow { @ApiProperty({ type: String, description: 'PK: uuid' }) @PrimaryGeneratedColumn('uuid') id: string; @@ -34,10 +34,10 @@ export class Neighbor { from_user: User; @ApiProperty({ type: Number, description: '이웃 추가를 받은 유저' }) - @RelationId((neighbor: Neighbor) => neighbor.to_user) // you need to specify target relation + @RelationId((follow: Follow) => follow.to_user) toUserKakaoId: number; @ApiProperty({ type: Number, description: '이웃 추가를 한 유저' }) - @RelationId((neighbor: Neighbor) => neighbor.from_user) // you need to specify target relation + @RelationId((follow: Follow) => follow.from_user) fromUserKakaoId: number; } diff --git a/src/APIs/neighbors/neighbors.controller.ts b/src/APIs/follows/follows.controller.ts similarity index 86% rename from src/APIs/neighbors/neighbors.controller.ts rename to src/APIs/follows/follows.controller.ts index 053c865..0354a04 100644 --- a/src/APIs/neighbors/neighbors.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -9,7 +9,6 @@ import { UseGuards, } from '@nestjs/common'; import { Request } from 'express'; -import { NeighborsService } from './neighbors.service'; import { ApiConflictResponse, ApiCookieAuth, @@ -24,11 +23,12 @@ import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { FollowsService } from './follows.service'; @ApiTags('유저 API') @Controller('users/:userId') -export class NeighborsController { - constructor(private readonly neighborsService: NeighborsService) {} +export class FollowsController { + constructor(private readonly followsService: FollowsService) {} @ApiOperation({ summary: '이웃 추가하기', @@ -45,7 +45,7 @@ export class NeighborsController { @Param('userId') to_user: number, ): Promise { const kakaoId = parseInt(req.user.userId); - return await this.neighborsService.followUser({ + return await this.followsService.followUser({ from_user: kakaoId, to_user, }); @@ -63,7 +63,7 @@ export class NeighborsController { @HttpCode(204) unfollowUser(@Req() req: Request, @Param('userId') to_user: number) { const kakaoId = parseInt(req.user.userId); - return this.neighborsService.unfollowUser({ + return this.followsService.unfollowUser({ from_user: kakaoId, to_user, }); @@ -82,7 +82,7 @@ export class NeighborsController { getFollowers( @Param('userId') kakaoId: number, ): Promise { - return this.neighborsService.getFollowers({ kakaoId }); + return this.followsService.getFollowers({ kakaoId }); } @ApiOperation({ @@ -96,6 +96,6 @@ export class NeighborsController { @HttpCode(200) @Get('following') getFollows(@Param('userId') kakaoId: number): Promise { - return this.neighborsService.getFollows({ kakaoId }); + return this.followsService.getFollows({ kakaoId }); } } diff --git a/src/APIs/follows/follows.module.ts b/src/APIs/follows/follows.module.ts new file mode 100644 index 0000000..cfbba67 --- /dev/null +++ b/src/APIs/follows/follows.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersModule } from '../users/users.module'; +import { User } from '../users/entities/user.entity'; +import { FollowsService } from './follows.service'; +import { FollowsController } from './follows.controller'; +import { Follow } from './entities/follow.entity'; + +@Module({ + imports: [UsersModule, TypeOrmModule.forFeature([Follow, User])], + providers: [FollowsService], + controllers: [FollowsController], + exports: [FollowsService], +}) +export class FollowsModule {} diff --git a/src/APIs/neighbors/neighbors.service.ts b/src/APIs/follows/follows.service.ts similarity index 82% rename from src/APIs/neighbors/neighbors.service.ts rename to src/APIs/follows/follows.service.ts index f2960be..9678045 100644 --- a/src/APIs/neighbors/neighbors.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -1,19 +1,18 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Neighbor } from './entities/neighbor.entity'; import { DataSource, Repository } from 'typeorm'; import { FromUserResponseDto } from './dtos/from-user-response.dto'; import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; -import e from 'express'; import { OpenScope } from 'src/common/enums/open-scope.enum'; +import { Follow } from './entities/follow.entity'; @Injectable() -export class NeighborsService { +export class FollowsService { constructor( - @InjectRepository(Neighbor) - private readonly neighborsRepository: Repository, + @InjectRepository(Follow) + private readonly followsRepository: Repository, private readonly dataSource: DataSource, ) {} @@ -28,7 +27,7 @@ export class NeighborsService { if (from_user === to_user) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; if (from_user !== null && to_user !== null) { - const neighbor = await this.neighborsRepository.findOne({ + const neighbor = await this.followsRepository.findOne({ where: { from_user, to_user }, }); if (neighbor) { @@ -41,7 +40,7 @@ export class NeighborsService { async isExist({ from_user, to_user }): Promise { console.log(from_user, to_user); - const neighbor = await this.neighborsRepository.findOne({ + const neighbor = await this.followsRepository.findOne({ where: { from_user: { kakaoId: from_user }, to_user: { kakaoId: to_user }, @@ -61,7 +60,7 @@ export class NeighborsService { if (this.isSame({ from_user, to_user })) { throw new ConflictException('you cannot follow yourself!'); } - const neighbor = await this.neighborsRepository.save({ + const neighbor = await this.followsRepository.save({ from_user, to_user, }); @@ -76,11 +75,11 @@ export class NeighborsService { if (this.isSame({ from_user, to_user })) { throw new ConflictException('you cannot unfollow yourself!'); } - return this.neighborsRepository.delete({ from_user, to_user }); + return this.followsRepository.delete({ from_user, to_user }); } async getFollows({ kakaoId }): Promise { - const follows = await this.neighborsRepository.find({ + const follows = await this.followsRepository.find({ select: { from_user: USER_SELECT_OPTION, to_user: USER_SELECT_OPTION, @@ -96,7 +95,7 @@ export class NeighborsService { } async getFollowers({ kakaoId }): Promise { - const follows = await this.neighborsRepository.find({ + const follows = await this.followsRepository.find({ select: { from_user: USER_SELECT_OPTION, }, diff --git a/src/APIs/neighbors/neighbors.module.ts b/src/APIs/neighbors/neighbors.module.ts deleted file mode 100644 index 277e407..0000000 --- a/src/APIs/neighbors/neighbors.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Neighbor } from './entities/neighbor.entity'; -import { NeighborsService } from './neighbors.service'; -import { NeighborsController } from './neighbors.controller'; -import { UsersModule } from '../users/users.module'; -import { User } from '../users/entities/user.entity'; - -@Module({ - imports: [UsersModule, TypeOrmModule.forFeature([Neighbor, User])], - providers: [NeighborsService], - controllers: [NeighborsController], - exports: [NeighborsService], -}) -export class NeighborsModule {} diff --git a/src/APIs/postCategories/PostCategories.module.ts b/src/APIs/postCategories/PostCategories.module.ts index 8e6e8c2..84f4a7e 100644 --- a/src/APIs/postCategories/PostCategories.module.ts +++ b/src/APIs/postCategories/PostCategories.module.ts @@ -4,10 +4,10 @@ import { PostCategory } from './entities/postCategory.entity'; import { PostCategoriesService } from './PostCategories.service'; import { PostCategoriesController } from './PostCategories.controller'; import { PostCategoriesRepository } from './PostCategories.repository'; -import { NeighborsModule } from '../neighbors/neighbors.module'; +import { FollowsModule } from '../follows/follows.module'; @Module({ - imports: [TypeOrmModule.forFeature([PostCategory]), NeighborsModule], + imports: [TypeOrmModule.forFeature([PostCategory]), FollowsModule], providers: [PostCategoriesService, PostCategoriesRepository], controllers: [PostCategoriesController], }) diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index 8eb6bf6..fc493e7 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -11,12 +11,12 @@ import { FetchPostCategoriesDto, FetchPostCategoryDto, } from './dtos/fetch-post-category.dto'; -import { NeighborsService } from '../neighbors/neighbors.service'; +import { FollowsService } from '../follows/follows.service'; @Injectable() export class PostCategoriesService { constructor( - private readonly neighborsService: NeighborsService, + private readonly followsService: FollowsService, private readonly postCategoriesRepository: PostCategoriesRepository, ) {} async findWithName({ kakaoId, name }) { @@ -56,7 +56,7 @@ export class PostCategoriesService { kakaoId, targetKakaoId, }): Promise { - const scope = await this.neighborsService.getScope({ + const scope = await this.followsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, }); diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 14ddd00..e7b3724 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -6,20 +6,19 @@ import { PostsController } from './posts.controller'; import { UtilsModule } from 'src/utils/utils.module'; import { AwsModule } from 'src/utils/aws/aws.module'; import { PostsService } from './posts.service'; -import { Neighbor } from '../neighbors/entities/neighbor.entity'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; import { CommentsModule } from '../comments/comments.module'; -import { NeighborsModule } from '../neighbors/neighbors.module'; +import { FollowsModule } from '../follows/follows.module'; @Module({ imports: [ TypeOrmModule.forFeature([Posts, User, PostBackground, PostCategory]), UtilsModule, AwsModule, - NeighborsModule, + FollowsModule, StickerBlocksModule, CommentsModule, ], diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 21000ae..f329a10 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -11,7 +11,6 @@ import { Posts } from './entities/posts.entity'; import { Page } from '../../utils/pages/page'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; -import { Neighbor } from '../neighbors/entities/neighbor.entity'; import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; import { CreatePostDto } from './dtos/create-post.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; @@ -30,8 +29,9 @@ import { import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { PostsOrderOption } from 'src/common/enums/posts-order-option'; -import { NeighborsService } from '../neighbors/neighbors.service'; +import { FollowsService } from '../follows/follows.service'; import { DateOption } from 'src/common/enums/date-option'; +import { Follow } from '../follows/entities/follow.entity'; @Injectable() export class PostsService { @@ -42,7 +42,7 @@ export class PostsService { private readonly stickerBlocksService: StickerBlocksService, private readonly commentsService: CommentsService, private readonly postsRepository: PostsRepository, - private readonly neighborsService: NeighborsService, + private readonly followsService: FollowsService, ) {} async saveImage(file: Express.Multer.File) { return await this.imageUpload(file); @@ -164,7 +164,7 @@ export class PostsService { page, }: FetchFriendsPostsDto): Promise { const subQuery = await this.dataSource - .createQueryBuilder(Neighbor, 'n') + .createQueryBuilder(Follow, 'n') .select('n.toUserKakaoId') .where(`n.fromUserKakaoId = ${kakaoId}`) .getQuery(); @@ -183,7 +183,7 @@ export class PostsService { async fetchDetail({ kakaoId, id }): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: false }); - const scope = await this.neighborsService.getScope({ + const scope = await this.followsService.getScope({ from_user: data.userKakaoId, to_user: kakaoId, }); @@ -240,7 +240,7 @@ export class PostsService { if (cursorOption.date_created) cursorOption.date_created = this.getDate(cursorOption.date_created); - const scope = await this.neighborsService.getScope({ + const scope = await this.followsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, }); @@ -290,7 +290,7 @@ export class PostsService { if (cursorOption.date_created) cursorOption.date_created = this.getDate(cursorOption.date_created); const subQuery = await this.dataSource - .createQueryBuilder(Neighbor, 'n') + .createQueryBuilder(Follow, 'n') .select('n.toUserKakaoId') .where(`n.fromUserKakaoId = ${kakaoId}`) .getQuery(); diff --git a/src/app.module.ts b/src/app.module.ts index 9fd9e5c..a168442 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,7 +7,7 @@ import { PostsModule } from './APIs/posts/posts.module'; import { UsersModule } from './APIs/users/users.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; -import { NeighborsModule } from './APIs/neighbors/neighbors.module'; +import { FollowsModule } from './APIs/follows/follows.module'; import { PostBackgroundsModule } from './APIs/postBackgrounds/postBackgrounds.module'; import { PostCategoriesModule } from './APIs/postCategories/PostCategories.module'; import { LikesModule } from './APIs/likes/likes.module'; @@ -34,7 +34,7 @@ import { AgreementsModule } from './APIs/agreements/agreements.module'; UsersModule, PostCategoriesModule, AuthModule, - NeighborsModule, + FollowsModule, NotificationsModule, PostBackgroundsModule, ReportsModule, From 92e43c23a67f02f6411fa436f107476b137c2fff Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 16:56:57 +0900 Subject: [PATCH 072/236] refactor: neighbor -> follow --- src/APIs/follows/follows.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index 9678045..acd254d 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -27,10 +27,10 @@ export class FollowsService { if (from_user === to_user) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; if (from_user !== null && to_user !== null) { - const neighbor = await this.followsRepository.findOne({ + const follow = await this.followsRepository.findOne({ where: { from_user, to_user }, }); - if (neighbor) { + if (follow) { return [OpenScope.PUBLIC, OpenScope.PROTECTED]; } } @@ -40,14 +40,14 @@ export class FollowsService { async isExist({ from_user, to_user }): Promise { console.log(from_user, to_user); - const neighbor = await this.followsRepository.findOne({ + const follow = await this.followsRepository.findOne({ where: { from_user: { kakaoId: from_user }, to_user: { kakaoId: to_user }, }, loadRelationIds: true, }); - if (!neighbor) { + if (!follow) { return false; } return true; @@ -60,11 +60,11 @@ export class FollowsService { if (this.isSame({ from_user, to_user })) { throw new ConflictException('you cannot follow yourself!'); } - const neighbor = await this.followsRepository.save({ + const follow = await this.followsRepository.save({ from_user, to_user, }); - return neighbor; + return follow; } async unfollowUser({ from_user, to_user }) { From 6e9f6affd3964d8ed1de6bc70fad2677513577b0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 17:09:34 +0900 Subject: [PATCH 073/236] feat: feedback entity --- .../feedbacks/entities/feedback.entity.ts | 52 +++++++++++++++++++ src/APIs/feedbacks/feedbacks.controller.ts | 7 +++ src/APIs/feedbacks/feedbacks.module.ts | 14 +++++ src/APIs/feedbacks/feedbacks.repository.ts | 10 ++++ src/APIs/feedbacks/feedbacks.service.ts | 7 +++ src/app.module.ts | 2 + 6 files changed, 92 insertions(+) create mode 100644 src/APIs/feedbacks/entities/feedback.entity.ts create mode 100644 src/APIs/feedbacks/feedbacks.controller.ts create mode 100644 src/APIs/feedbacks/feedbacks.module.ts create mode 100644 src/APIs/feedbacks/feedbacks.repository.ts create mode 100644 src/APIs/feedbacks/feedbacks.service.ts diff --git a/src/APIs/feedbacks/entities/feedback.entity.ts b/src/APIs/feedbacks/entities/feedback.entity.ts new file mode 100644 index 0000000..d1a6b72 --- /dev/null +++ b/src/APIs/feedbacks/entities/feedback.entity.ts @@ -0,0 +1,52 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsString } from 'class-validator'; +import { User } from 'src/APIs/users/entities/user.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, +} from 'typeorm'; + +@Entity() +export class Feedback { + @IsNumber() + @ApiProperty({ type: Number, description: 'PK: A_I_' }) + @PrimaryGeneratedColumn() + id: number; + + @IsString() + @ApiProperty({ type: String, description: '피드백 내용' }) + @Column() + content: string; + + @ApiProperty() + @JoinColumn() + @ManyToOne(() => User, (users) => users.kakaoId, { + nullable: true, + onUpdate: 'NO ACTION', + onDelete: 'SET NULL', + }) + user: User; + + @IsNumber() + @ApiProperty({ + type: Number, + description: '피드백 보낸 유저의 카카오 아이디', + }) + @Column({ nullable: true }) + @RelationId((feedback: Feedback) => feedback.user) + userKakaoId: number; + + @ApiProperty({ type: Date, description: '생성한 날짜' }) + @CreateDateColumn() + date_created: Date; + + @ApiProperty({ type: Date, description: '삭제한 날짜' }) + @DeleteDateColumn() + date_deleted: Date; +} diff --git a/src/APIs/feedbacks/feedbacks.controller.ts b/src/APIs/feedbacks/feedbacks.controller.ts new file mode 100644 index 0000000..9133826 --- /dev/null +++ b/src/APIs/feedbacks/feedbacks.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { FeedbacksService } from './feedbacks.service'; + +@Controller() +export class FeedbacksController { + constructor(private readonly feedbacksService: FeedbacksService) {} +} diff --git a/src/APIs/feedbacks/feedbacks.module.ts b/src/APIs/feedbacks/feedbacks.module.ts new file mode 100644 index 0000000..27d2e84 --- /dev/null +++ b/src/APIs/feedbacks/feedbacks.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Feedback } from './entities/feedback.entity'; +import { FeedbacksController } from './feedbacks.controller'; +import { FeedbacksService } from './feedbacks.service'; +import { FeedbacksRepository } from './feedbacks.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([Feedback])], + controllers: [FeedbacksController], + providers: [FeedbacksService, FeedbacksRepository], + exports: [], +}) +export class FeedbacksModule {} diff --git a/src/APIs/feedbacks/feedbacks.repository.ts b/src/APIs/feedbacks/feedbacks.repository.ts new file mode 100644 index 0000000..e9321a8 --- /dev/null +++ b/src/APIs/feedbacks/feedbacks.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Feedback } from './entities/feedback.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FeedbacksRepository extends Repository { + constructor(private datasource: DataSource) { + super(Feedback, datasource.createEntityManager()); + } +} diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts new file mode 100644 index 0000000..cc12bf1 --- /dev/null +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { FeedbacksRepository } from './feedbacks.repository'; + +@Injectable() +export class FeedbacksService { + constructor(private readonly feedbacksRepository: FeedbacksRepository) {} +} diff --git a/src/app.module.ts b/src/app.module.ts index a168442..83d39a8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -20,11 +20,13 @@ import { ReportsModule } from './APIs/reports/reports.module'; import { AuthTokenMiddleware } from './common/middlewares/auth-token.middleware'; import { JwtModule } from '@nestjs/jwt'; import { AgreementsModule } from './APIs/agreements/agreements.module'; +import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; @Module({ imports: [ AnnouncementsModule, AgreementsModule, + FeedbacksModule, StickersModule, StickerCategoriesModule, StickerBlocksModule, From e2b92e7174d0b7a02539da4bfbc6871d23d3c170 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 17:39:02 +0900 Subject: [PATCH 074/236] refactor: change endpoints of report --- src/APIs/comments/dtos/fetch-comments.dto.ts | 4 +- src/APIs/comments/entities/comment.entity.ts | 2 +- src/APIs/posts/dtos/fetch-posts.dto.ts | 2 +- src/APIs/posts/entities/posts.entity.ts | 2 +- src/APIs/reports/dtos/create-report.dto.ts | 2 + src/APIs/reports/reports.controller.ts | 47 +++++++++++++++++--- src/APIs/reports/reports.service.ts | 4 +- 7 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/APIs/comments/dtos/fetch-comments.dto.ts b/src/APIs/comments/dtos/fetch-comments.dto.ts index 158ef4d..07766fd 100644 --- a/src/APIs/comments/dtos/fetch-comments.dto.ts +++ b/src/APIs/comments/dtos/fetch-comments.dto.ts @@ -9,7 +9,7 @@ export class ChildrenComment extends PickType(Comment, [ 'date_created', 'date_updated', 'date_deleted', - 'blame_count', + 'report_count', 'parentId', 'postsId', ]) { @@ -31,7 +31,7 @@ export class FetchCommentsDto extends PickType(Comment, [ 'date_created', 'date_updated', 'date_deleted', - 'blame_count', + 'report_count', 'parentId', 'postsId', ]) { diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 3af367e..9384d3e 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -51,7 +51,7 @@ export class Comment { @ApiProperty({ type: Number, description: '신고 당한 횟수' }) @Column({ default: 0 }) - blame_count: number; + report_count: number; @ApiProperty({ type: Comment, description: '루트 댓글 정보' }) @ManyToOne(() => Comment, (comment) => comment.children, { diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index dfbca91..e301210 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -53,7 +53,7 @@ export const FETCH_POST_OPTION = { main_image_url: true, isPublished: true, like_count: true, - blame_count: true, + report_count: true, allow_comment: true, scope: true, date_created: true, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index 745126a..ceeab24 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -63,7 +63,7 @@ export class Posts { @ApiProperty({ description: '신고수 카운트', type: Number }) @Column({ default: 0 }) - blame_count: number; + report_count: number; @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) @Column({ default: true }) diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts index 32f12ef..d38b2db 100644 --- a/src/APIs/reports/dtos/create-report.dto.ts +++ b/src/APIs/reports/dtos/create-report.dto.ts @@ -17,4 +17,6 @@ export class CreateReportDto extends OmitType(Report, [ export class CreateReportInput extends OmitType(CreateReportDto, [ 'userKakaoId', + 'target', + 'targetId', ]) {} diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 2de11ee..3b1dce8 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, HttpCode, + Param, Post, Req, UseGuards, @@ -19,35 +20,67 @@ import { CreateReportInput } from './dtos/create-report.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; import { FetchReportResponse } from './dtos/fetch-report.dto'; +import { ReportTarget } from 'src/common/enums/report-target.enum'; -@ApiTags('신고 API') -@Controller('reports') +@Controller('') export class ReportsController { constructor(private readonly reportsService: ReportsService) {} + @ApiTags('게시글 API') @ApiOperation({ - summary: '게시물 || 댓글 신고', + summary: '게시물 신고', }) @ApiCookieAuth() @ApiCreatedResponse({ type: FetchReportResponse }) @UseGuards(AuthGuardV2) - @Post() + @Post('posts/:postId/report') @HttpCode(201) - async report( + async reportPost( @Req() req: Request, @Body() body: CreateReportInput, + @Param('postId') targetId: number, ): Promise { const userKakaoId = req.user.userId; - return await this.reportsService.create({ userKakaoId, ...body }); + return await this.reportsService.create({ + targetId, + target: ReportTarget.POSTS, + userKakaoId, + ...body, + }); } + @ApiTags('게시글 API') + @ApiOperation({ + summary: '댓글 신고', + }) + @ApiCookieAuth() + @ApiCreatedResponse({ type: FetchReportResponse }) + @UseGuards(AuthGuardV2) + @Post('post/:postId/comments/:commentId/report') + @HttpCode(201) + async reportComment( + @Req() req: Request, + @Body() body: CreateReportInput, + @Param('postId') postId: number, + @Param('commentId') targetId: number, + ): Promise { + const userKakaoId = req.user.userId; + return await this.reportsService.create({ + targetId, + target: ReportTarget.COMMENTS, + userKakaoId, + ...body, + }); + } + + @ApiTags('유저 API') @ApiOperation({ summary: '[어드민용] 신고 내역 조회', }) @ApiCookieAuth() @ApiOkResponse({ type: [FetchReportResponse] }) @UseGuards(AuthGuardV2) - @Get() + @Get('users/reports') async fetchAll(@Req() req: Request): Promise { const kakaoId = req.user.userId; return await this.reportsService.fetchAll({ kakaoId }); diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 9615508..aa6b7d5 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -45,7 +45,7 @@ export class ReportsService { throw new ConflictException('이미 신고한 게시물입니다.'); await queryRunner.manager.update(Posts, postData.id, { - blame_count: () => 'blame_count +1', + report_count: () => 'report_count +1', }); data = await queryRunner.manager.save(Report, { ...rest, @@ -67,7 +67,7 @@ export class ReportsService { throw new ConflictException('이미 신고한 게시물입니다.'); await queryRunner.manager.update(Comment, commentData.id, { - blame_count: () => 'blame_count +1', + report_count: () => 'report_count +1', }); data = await queryRunner.manager.save(Report, { ...rest, From 94460f418622f9261f1eb6656897f8eb347cd412 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 11 May 2024 17:52:24 +0900 Subject: [PATCH 075/236] feat: fetch agreements APIs --- src/APIs/agreements/agreements.controller.ts | 32 ++++++++++++------- src/APIs/agreements/agreements.service.ts | 23 +++++++++++-- .../agreements/dtos/fetch-agreement.dto.ts | 4 +++ src/app.module.ts | 2 +- 4 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 src/APIs/agreements/dtos/fetch-agreement.dto.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index d197012..6513203 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -12,45 +12,55 @@ import { AgreementsService } from './agreements.service'; import { ApiCookieAuth, ApiCreatedResponse, + ApiOkResponse, ApiOperation, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; import { CreateAgreementsInput } from './dtos/create-agreements.dto'; +import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; @Controller('users') export class AgreementsController { constructor(private readonly agreementsService: AgreementsService) {} - @ApiOperation({}) + @ApiOperation({ summary: '온보딩 동의' }) @ApiCookieAuth() - @ApiCreatedResponse({}) + @ApiCreatedResponse({ type: FetchAgreementDto }) @UseGuards(AuthGuardV2) @Post('me/agreement') - async agree(@Req() req: Request, @Body() body: CreateAgreementsInput) { + async agree( + @Req() req: Request, + @Body() body: CreateAgreementsInput, + ): Promise { const kakaoId = req.user.kakaoId; - await this.agreementsService.create({ ...body, kakaoId }); + return await this.agreementsService.create({ ...body, kakaoId }); } - @ApiOperation({}) + @ApiOperation({ summary: '로그인된 유저의 온보딩 동의 내용들을 fetch' }) @ApiCookieAuth() - @ApiCreatedResponse({}) + @ApiOkResponse({ type: [FetchAgreementDto] }) @UseGuards(AuthGuardV2) @Get('me/agreements') - async fetchAgreement(@Req() req: Request) { + async fetchAgreements(@Req() req: Request): Promise { const kakaoId = req.user.kakaoId; + return await this.agreementsService.fetchAll({ kakaoId }); } - @ApiOperation({}) + @ApiOperation({ summary: '[어드민용] 특정 유저의 온보딩 동의 내용을 조회' }) @ApiCookieAuth() - @ApiCreatedResponse({}) + @ApiOkResponse({ type: [FetchAgreementDto] }) @UseGuards(AuthGuardV2) - @Get(':userId/agreements') + @Get('admin/:userId/agreements') async fetchAgreementAdmin( @Req() req: Request, @Param('userId') targetUserKakaoId: number, - ) { + ): Promise { const kakaoId = req.user.kakaoId; + await this.agreementsService.adminCheck({ kakaoId }); + return await this.agreementsService.fetchAll({ + kakaoId: targetUserKakaoId, + }); } @ApiOperation({}) diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index f4fabc8..e4c108b 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -1,16 +1,33 @@ import { Injectable } from '@nestjs/common'; import { AgreementsRepository } from './agreements.repository'; import { IAgreementsServiceCreate } from './interfaces/agreements.service.interface'; +import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; +import { UsersService } from '../users/users.service'; @Injectable() export class AgreementsService { - constructor(private readonly agreementsRepository: AgreementsRepository) {} + constructor( + private readonly agreementsRepository: AgreementsRepository, + private readonly usersService: UsersService, + ) {} - async create({ kakaoId, agreementType, isAgreed }: IAgreementsServiceCreate) { - await this.agreementsRepository.save({ + async adminCheck({ kakaoId }) { + await this.usersService.adminCheck({ kakaoId }); + } + + async create({ + kakaoId, + agreementType, + isAgreed, + }: IAgreementsServiceCreate): Promise { + return await this.agreementsRepository.save({ agreementType, isAgreed, user: { kakaoId }, }); } + + async fetchAll({ kakaoId }): Promise { + return await this.agreementsRepository.find({ where: { user: kakaoId } }); + } } diff --git a/src/APIs/agreements/dtos/fetch-agreement.dto.ts b/src/APIs/agreements/dtos/fetch-agreement.dto.ts new file mode 100644 index 0000000..a45445d --- /dev/null +++ b/src/APIs/agreements/dtos/fetch-agreement.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Agreement } from '../entities/agreement.entity'; + +export class FetchAgreementDto extends OmitType(Agreement, ['user']) {} diff --git a/src/app.module.ts b/src/app.module.ts index 9fd9e5c..154a07c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -58,7 +58,7 @@ import { AgreementsModule } from './APIs/agreements/agreements.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: false, + synchronize: true, logging: true, }), ], From eac0f31b6a33e2d288a1d16e715f4fa744d3d2f1 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 12 May 2024 15:20:03 +0900 Subject: [PATCH 076/236] feat: patch agreements API --- src/APIs/agreements/agreements.controller.ts | 14 ++++++--- src/APIs/agreements/agreements.service.ts | 29 +++++++++++++++++-- .../agreements/dtos/patch-agreement.dto.ts | 4 +++ .../agreements.service.interface.ts | 3 ++ 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/APIs/agreements/dtos/patch-agreement.dto.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index 6513203..75d5e02 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -19,6 +19,7 @@ import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; import { CreateAgreementsInput } from './dtos/create-agreements.dto'; import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; +import { PatchAgreementInput } from './dtos/patch-agreement.dto'; @Controller('users') export class AgreementsController { @@ -63,12 +64,17 @@ export class AgreementsController { }); } - @ApiOperation({}) + @ApiOperation({ summary: '동의 여부를 수정' }) @ApiCookieAuth() - @ApiCreatedResponse({}) + @ApiOkResponse({ type: FetchAgreementDto }) @UseGuards(AuthGuardV2) @Patch('me/agreement/:agreementId') - async patchAgreement(@Req() req: Request, @Param('agreementId') id: number) { - const kakaoId = req.user.kakaoId; + async patchAgreement( + @Req() req: Request, + @Param('agreementId') id: number, + @Body() body: PatchAgreementInput, + ): Promise { + const userKakaoId = req.user.kakaoId; + return await this.agreementsService.patch({ ...body, id, userKakaoId }); } } diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index e4c108b..d7234f8 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -1,6 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { AgreementsRepository } from './agreements.repository'; -import { IAgreementsServiceCreate } from './interfaces/agreements.service.interface'; +import { + IAgreementsServiceCreate, + IAgreementsServicePatch, +} from './interfaces/agreements.service.interface'; import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; import { UsersService } from '../users/users.service'; @@ -27,7 +34,25 @@ export class AgreementsService { }); } + async fetchOne({ id }): Promise { + return await this.agreementsRepository.findOne({ where: { id } }); + } + async fetchAll({ kakaoId }): Promise { return await this.agreementsRepository.find({ where: { user: kakaoId } }); } + + async patch({ + userKakaoId, + id, + isAgreed, + }: IAgreementsServicePatch): Promise { + const data = await this.fetchOne({ id }); + if (!data) throw new NotFoundException('데이터를 찾을 수 없습니다.'); + if (data.userKakaoId != userKakaoId) + throw new ForbiddenException('권한이 없습니다.'); + // if(data.agreementType != AgreementType.MARKETING_CONSENT) + data.isAgreed = isAgreed; + return await this.agreementsRepository.save(data); + } } diff --git a/src/APIs/agreements/dtos/patch-agreement.dto.ts b/src/APIs/agreements/dtos/patch-agreement.dto.ts new file mode 100644 index 0000000..7f3bd1e --- /dev/null +++ b/src/APIs/agreements/dtos/patch-agreement.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { Agreement } from '../entities/agreement.entity'; + +export class PatchAgreementInput extends PickType(Agreement, ['isAgreed']) {} diff --git a/src/APIs/agreements/interfaces/agreements.service.interface.ts b/src/APIs/agreements/interfaces/agreements.service.interface.ts index dcc7288..4f0f006 100644 --- a/src/APIs/agreements/interfaces/agreements.service.interface.ts +++ b/src/APIs/agreements/interfaces/agreements.service.interface.ts @@ -12,3 +12,6 @@ export interface IAgreementsServiceCreate > { kakaoId: number; } + +export interface IAgreementsServicePatch +extends Pick From c73e2fd2f9e6127f83fb30dab4c6ca6fdee403c6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 13 May 2024 09:59:37 +0900 Subject: [PATCH 077/236] feat: serving static policy --- src/APIs/agreements/agreements.controller.ts | 11 +++++++++++ src/APIs/agreements/agreements.module.ts | 3 ++- src/APIs/agreements/agreements.service.ts | 10 ++++++++++ src/APIs/agreements/dtos/fetch-contract.dto.ts | 4 ++++ .../interfaces/agreements.service.interface.ts | 2 +- src/assets/terms/CUSTOM_AGREEMENT.txt | 1 + src/assets/terms/MARKETING_CONSENT.txt | 1 + src/assets/terms/PRIVACY_POLICY.txt | 1 + src/assets/terms/TERMS_OF_SERVICE.txt | 1 + 9 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/APIs/agreements/dtos/fetch-contract.dto.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index 75d5e02..9f01d65 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -5,6 +5,7 @@ import { Param, Patch, Post, + Query, Req, UseGuards, } from '@nestjs/common'; @@ -20,11 +21,21 @@ import { Request } from 'express'; import { CreateAgreementsInput } from './dtos/create-agreements.dto'; import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; import { PatchAgreementInput } from './dtos/patch-agreement.dto'; +import { FetchContractDto } from './dtos/fetch-contract.dto'; @Controller('users') export class AgreementsController { constructor(private readonly agreementsService: AgreementsService) {} + @ApiOperation({ summary: 'contract fetch' }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @Get('contracts') + async fetchContract(@Query() query: FetchContractDto) { + const data = await this.agreementsService.fetchContract({ ...query }); + return data; + } + @ApiOperation({ summary: '온보딩 동의' }) @ApiCookieAuth() @ApiCreatedResponse({ type: FetchAgreementDto }) diff --git a/src/APIs/agreements/agreements.module.ts b/src/APIs/agreements/agreements.module.ts index 1743741..63b55a3 100644 --- a/src/APIs/agreements/agreements.module.ts +++ b/src/APIs/agreements/agreements.module.ts @@ -4,9 +4,10 @@ import { Agreement } from './entities/agreement.entity'; import { AgreementsController } from './agreements.controller'; import { AgreementsService } from './agreements.service'; import { AgreementsRepository } from './agreements.repository'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Agreement])], + imports: [TypeOrmModule.forFeature([Agreement]), UsersModule], controllers: [AgreementsController], providers: [AgreementsService, AgreementsRepository], }) diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index d7234f8..6d10338 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -10,6 +10,8 @@ import { } from './interfaces/agreements.service.interface'; import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; import { UsersService } from '../users/users.service'; +import path from 'path'; +import fs from 'fs'; @Injectable() export class AgreementsService { @@ -34,6 +36,14 @@ export class AgreementsService { }); } + async fetchContract({ agreementType }) { + const fileName = agreementType + '.txt'; + const rootPath = process.cwd(); + const filePath = path.join(rootPath, 'src', 'assets', 'terms', fileName); + const data = await fs.promises.readFile(filePath, 'utf8'); + return data; + } + async fetchOne({ id }): Promise { return await this.agreementsRepository.findOne({ where: { id } }); } diff --git a/src/APIs/agreements/dtos/fetch-contract.dto.ts b/src/APIs/agreements/dtos/fetch-contract.dto.ts new file mode 100644 index 0000000..861da4a --- /dev/null +++ b/src/APIs/agreements/dtos/fetch-contract.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { Agreement } from '../entities/agreement.entity'; + +export class FetchContractDto extends PickType(Agreement, ['agreementType']) {} diff --git a/src/APIs/agreements/interfaces/agreements.service.interface.ts b/src/APIs/agreements/interfaces/agreements.service.interface.ts index 4f0f006..b833c99 100644 --- a/src/APIs/agreements/interfaces/agreements.service.interface.ts +++ b/src/APIs/agreements/interfaces/agreements.service.interface.ts @@ -14,4 +14,4 @@ export interface IAgreementsServiceCreate } export interface IAgreementsServicePatch -extends Pick + extends Pick {} diff --git a/src/assets/terms/CUSTOM_AGREEMENT.txt b/src/assets/terms/CUSTOM_AGREEMENT.txt index e69de29..425819e 100644 --- a/src/assets/terms/CUSTOM_AGREEMENT.txt +++ b/src/assets/terms/CUSTOM_AGREEMENT.txt @@ -0,0 +1 @@ +custom_agreement \ No newline at end of file diff --git a/src/assets/terms/MARKETING_CONSENT.txt b/src/assets/terms/MARKETING_CONSENT.txt index e69de29..dff1857 100644 --- a/src/assets/terms/MARKETING_CONSENT.txt +++ b/src/assets/terms/MARKETING_CONSENT.txt @@ -0,0 +1 @@ +marketing consent \ No newline at end of file diff --git a/src/assets/terms/PRIVACY_POLICY.txt b/src/assets/terms/PRIVACY_POLICY.txt index e69de29..8c4196d 100644 --- a/src/assets/terms/PRIVACY_POLICY.txt +++ b/src/assets/terms/PRIVACY_POLICY.txt @@ -0,0 +1 @@ +privacy policy \ No newline at end of file diff --git a/src/assets/terms/TERMS_OF_SERVICE.txt b/src/assets/terms/TERMS_OF_SERVICE.txt index e69de29..020b583 100644 --- a/src/assets/terms/TERMS_OF_SERVICE.txt +++ b/src/assets/terms/TERMS_OF_SERVICE.txt @@ -0,0 +1 @@ +terms of service \ No newline at end of file From 000791237730f33273c1b5fe830fb8531b7c8a02 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 13 May 2024 10:43:56 +0900 Subject: [PATCH 078/236] feat: create & read feedback apis --- src/APIs/agreements/agreements.controller.ts | 3 ++ .../announcements/announcements.controller.ts | 13 +++--- .../feedbacks/dtos/create-feedback.dto.ts | 4 ++ src/APIs/feedbacks/dtos/fetch-feedback.dto.ts | 4 ++ src/APIs/feedbacks/feedbacks.controller.ts | 40 ++++++++++++++++++- src/APIs/feedbacks/feedbacks.module.ts | 3 +- src/APIs/feedbacks/feedbacks.service.ts | 20 +++++++++- .../interfaces/feedbacks.service.interface.ts | 5 +++ src/APIs/reports/reports.controller.ts | 5 ++- 9 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 src/APIs/feedbacks/dtos/create-feedback.dto.ts create mode 100644 src/APIs/feedbacks/dtos/fetch-feedback.dto.ts create mode 100644 src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index 9f01d65..2fff8ca 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -15,6 +15,7 @@ import { ApiCreatedResponse, ApiOkResponse, ApiOperation, + ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; @@ -23,6 +24,7 @@ import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; import { PatchAgreementInput } from './dtos/patch-agreement.dto'; import { FetchContractDto } from './dtos/fetch-contract.dto'; +@ApiTags('유저 API') @Controller('users') export class AgreementsController { constructor(private readonly agreementsService: AgreementsService) {} @@ -59,6 +61,7 @@ export class AgreementsController { return await this.agreementsService.fetchAll({ kakaoId }); } + @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 특정 유저의 온보딩 동의 내용을 조회' }) @ApiCookieAuth() @ApiOkResponse({ type: [FetchAgreementDto] }) diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 4ea3f36..d1fff16 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -25,15 +25,16 @@ import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; import { PatchAnnouncementInput } from './dtos/patch-announcment.dto'; @ApiTags('공지 API') -@Controller('anmts') +@Controller() export class AnnouncementsController { constructor(private readonly announcementsService: AnnouncementsService) {} + @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 공지사항 작성' }) @ApiCookieAuth() @UseGuards(AuthGuardV2) @ApiCreatedResponse({ type: AnnouncementResponseDto }) - @Post() + @Post('users/admin/anmts') @HttpCode(201) async createAnmt( @Req() req: Request, @@ -45,16 +46,17 @@ export class AnnouncementsController { @ApiOperation({ summary: '공지사항 조회' }) @ApiOkResponse({ type: [AnnouncementResponseDto] }) - @Get() + @Get('anmts') async fetchAnmts(): Promise { return await this.announcementsService.fetchAll(); } + @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 공지사항 수정' }) @ApiCookieAuth() @ApiOkResponse({ type: [AnnouncementResponseDto] }) @UseGuards(AuthGuardV2) - @Patch(':id') + @Patch('users/admin/anmts/:id') async patchAnmt( @Req() req: Request, @Body() body: PatchAnnouncementInput, @@ -64,6 +66,7 @@ export class AnnouncementsController { return await this.announcementsService.patch({ ...body, id, kakaoId }); } + @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 공지사항 삭제', description: 'id에 해당하는 공지사항 삭제, 삭제된 공지사항을 반환', @@ -71,7 +74,7 @@ export class AnnouncementsController { @ApiCookieAuth() @ApiOkResponse({ type: AnnouncementResponseDto }) @UseGuards(AuthGuardV2) - @Delete(':id') + @Delete('users/admin/anmts/:id') async removeAnmt( @Req() req: Request, @Param('id') id: number, diff --git a/src/APIs/feedbacks/dtos/create-feedback.dto.ts b/src/APIs/feedbacks/dtos/create-feedback.dto.ts new file mode 100644 index 0000000..68bf05e --- /dev/null +++ b/src/APIs/feedbacks/dtos/create-feedback.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { Feedback } from '../entities/feedback.entity'; + +export class CreateFeedbackInput extends PickType(Feedback, ['content']) {} diff --git a/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts b/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts new file mode 100644 index 0000000..4351808 --- /dev/null +++ b/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Feedback } from '../entities/feedback.entity'; + +export class FetchFeedbackDto extends OmitType(Feedback, ['user']) {} diff --git a/src/APIs/feedbacks/feedbacks.controller.ts b/src/APIs/feedbacks/feedbacks.controller.ts index 9133826..d8b094a 100644 --- a/src/APIs/feedbacks/feedbacks.controller.ts +++ b/src/APIs/feedbacks/feedbacks.controller.ts @@ -1,7 +1,43 @@ -import { Controller } from '@nestjs/common'; +import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; import { FeedbacksService } from './feedbacks.service'; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { CreateFeedbackInput } from './dtos/create-feedback.dto'; +import { Request } from 'express'; +import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; -@Controller() +@ApiTags('유저 API') +@Controller('users') export class FeedbacksController { constructor(private readonly feedbacksService: FeedbacksService) {} + + @ApiOperation({ summary: '피드백 작성하기' }) + @ApiCookieAuth() + @ApiCreatedResponse({ type: FetchFeedbackDto }) + @UseGuards(AuthGuardV2) + @Post('feedback') + async createFeedback( + @Body() body: CreateFeedbackInput, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.feedbacksService.create({ ...body, kakaoId }); + } + + @ApiTags('어드민 API') + @ApiOperation({ summary: '[어드민용] 피드백 내용 조회' }) + @ApiCookieAuth() + @ApiOkResponse({ type: [FetchFeedbackDto] }) + @UseGuards(AuthGuardV2) + @Get('admin/feedbacks') + async getFeedbacks(@Req() req: Request): Promise { + const kakaoId = req.user.userId; + return await this.feedbacksService.fetchAll({ kakaoId }); + } } diff --git a/src/APIs/feedbacks/feedbacks.module.ts b/src/APIs/feedbacks/feedbacks.module.ts index 27d2e84..95856a2 100644 --- a/src/APIs/feedbacks/feedbacks.module.ts +++ b/src/APIs/feedbacks/feedbacks.module.ts @@ -4,9 +4,10 @@ import { Feedback } from './entities/feedback.entity'; import { FeedbacksController } from './feedbacks.controller'; import { FeedbacksService } from './feedbacks.service'; import { FeedbacksRepository } from './feedbacks.repository'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Feedback])], + imports: [TypeOrmModule.forFeature([Feedback]), UsersModule], controllers: [FeedbacksController], providers: [FeedbacksService, FeedbacksRepository], exports: [], diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts index cc12bf1..c3396be 100644 --- a/src/APIs/feedbacks/feedbacks.service.ts +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -1,7 +1,25 @@ import { Injectable } from '@nestjs/common'; import { FeedbacksRepository } from './feedbacks.repository'; +import { IFeedbacksServiceCreate } from './interfaces/feedbacks.service.interface'; +import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; +import { UsersService } from '../users/users.service'; @Injectable() export class FeedbacksService { - constructor(private readonly feedbacksRepository: FeedbacksRepository) {} + constructor( + private readonly feedbacksRepository: FeedbacksRepository, + private readonly usersService: UsersService, + ) {} + + async create({ + kakaoId, + content, + }: IFeedbacksServiceCreate): Promise { + return await this.feedbacksRepository.save({ content, user: { kakaoId } }); + } + + async fetchAll({ kakaoId }): Promise { + await this.usersService.adminCheck({ kakaoId }); + return await this.feedbacksRepository.find(); + } } diff --git a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts new file mode 100644 index 0000000..433804c --- /dev/null +++ b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts @@ -0,0 +1,5 @@ +import { Feedback } from '../entities/feedback.entity'; + +export interface IFeedbacksServiceCreate extends Pick { + kakaoId: number; +} diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 3b1dce8..912a33d 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -56,7 +56,7 @@ export class ReportsController { @ApiCookieAuth() @ApiCreatedResponse({ type: FetchReportResponse }) @UseGuards(AuthGuardV2) - @Post('post/:postId/comments/:commentId/report') + @Post('posts/:postId/comments/:commentId/report') @HttpCode(201) async reportComment( @Req() req: Request, @@ -73,6 +73,7 @@ export class ReportsController { }); } + @ApiTags('어드민 API') @ApiTags('유저 API') @ApiOperation({ summary: '[어드민용] 신고 내역 조회', @@ -80,7 +81,7 @@ export class ReportsController { @ApiCookieAuth() @ApiOkResponse({ type: [FetchReportResponse] }) @UseGuards(AuthGuardV2) - @Get('users/reports') + @Get('users/admin/reports') async fetchAll(@Req() req: Request): Promise { const kakaoId = req.user.userId; return await this.reportsService.fetchAll({ kakaoId }); From 4b2121bd64ba4466b470aaa8d925d1609221183a Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 13 May 2024 11:04:45 +0900 Subject: [PATCH 079/236] feat: user handle column --- src/APIs/users/dtos/patch-user.input.ts | 10 ++++++++ src/APIs/users/dtos/user-response.dto.ts | 3 +++ src/APIs/users/entities/user.entity.ts | 4 +++ .../interfaces/users.service.interface.ts | 4 +++ src/APIs/users/users.controller.ts | 25 ++++++++++++++----- src/APIs/users/users.service.ts | 21 ++++++++++++++-- 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/APIs/users/dtos/patch-user.input.ts b/src/APIs/users/dtos/patch-user.input.ts index da71ba2..382f129 100644 --- a/src/APIs/users/dtos/patch-user.input.ts +++ b/src/APIs/users/dtos/patch-user.input.ts @@ -2,6 +2,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, IsString } from 'class-validator'; export class PatchUserInput { + @ApiProperty({ + description: '[optional] 핸들러 변경', + type: String, + example: 'optional', + required: false, + }) + @IsString() + @IsOptional() + handle?: string; + @ApiProperty({ description: '[optional] username 변경', type: String, diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index 1593ce1..c12434b 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -3,6 +3,7 @@ import { User } from '../entities/user.entity'; export const USER_SELECT_OPTION = { kakaoId: true, + handle: true, isAdmin: true, username: true, description: true, @@ -13,6 +14,7 @@ export const USER_SELECT_OPTION = { }; export const USER_PRIMARY_SELECT_OPTION = { kakaoId: true, + handle: true, username: true, description: true, profile_image: true, @@ -22,6 +24,7 @@ export class UserPrimaryResponseDto extends PickType(User, [ 'username', 'profile_image', 'description', + 'handle', ]) {} export class UserResponseDto extends OmitType(User, ['current_refresh_token']) { // @ApiProperty({ description: '카카오 id', type: Number }) diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 414611e..841582c 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -14,6 +14,10 @@ export class User { @ApiProperty({ description: '카카오 id', type: Number }) kakaoId: number; + @Column({ unique: true }) + @ApiProperty({ description: '유저 핸들러', type: String }) + handle: string; + @Column({ default: '' }) @ApiProperty({ description: 'crypted refresh token', type: String }) current_refresh_token: string; diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index 78d3065..a69b396 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -1,3 +1,5 @@ +import { User } from '../entities/user.entity'; + export interface IUsersServiceCreate { kakaoId: number; } @@ -6,6 +8,8 @@ export interface IUsersServiceFindUserByKakaoId { kakaoId: number; } +export interface IUsersServiceFindUserByHandle extends Pick {} + export interface IUsersServiceFindUser { id: string; } diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index f126a09..7da48df 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -59,18 +59,31 @@ export class UsersController { } @ApiOperation({ - summary: '특정 유저 프로필 조회', + summary: '특정 유저 프로필 조회(id)', description: 'id가 일치하는 유저 프로필을 조회한다.', }) @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) @HttpCode(200) - @Get('profile/:userId') + @Get('profile/id/:userId') async findUserByKakaoId( @Param('userId') kakaoId: number, ): Promise { return await this.usersService.findUserByKakaoId({ kakaoId }); } + @ApiOperation({ + summary: '특정 유저 프로필 조회(handle)', + description: 'handle이 일치하는 유저 프로필을 조회한다.', + }) + @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) + @HttpCode(200) + @Get('profile/handle/:handle') + async findUserByHandle( + @Param('handle') handle: string, + ): Promise { + return await this.usersService.findUserByHandle({ handle }); + } + @ApiOperation({ summary: '로그인된 유저의 프로필 불러오기', description: '로그인된 유저의 프로필을 불러온다.', @@ -86,8 +99,8 @@ export class UsersController { } @ApiOperation({ - summary: '로그인된 유저의 이름이나 설명을 변경', - description: '로그인된 유저의 이름이나 설명, 혹은 둘 다를 변경한다.', + summary: '로그인된 유저의 이름이나 설명, 핸들을 변경', + description: '로그인된 유저의 이름이나 설명, 핸들, 혹은 모두를 변경한다.', }) @ApiOkResponse({ description: '변경 성공', type: UserResponseDto }) @ApiCookieAuth() @@ -99,12 +112,12 @@ export class UsersController { @Body() body: PatchUserInput, ): Promise { const kakaoId = req.user.userId; - const description = body.description; - const username = body.username; + const { description, username, handle } = body; return await this.usersService.patchUser({ kakaoId, description, username, + handle, }); } diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 4bfd5d6..b8b162b 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -9,6 +9,7 @@ import { ILike, Repository } from 'typeorm'; import { User } from './entities/user.entity'; import { IUsersServiceCreate, + IUsersServiceFindUserByHandle, IUsersServiceFindUserByKakaoId, } from './interfaces/users.service.interface'; import { USER_SELECT_OPTION, UserResponseDto } from './dtos/user-response.dto'; @@ -46,10 +47,11 @@ export class UsersService { } async create({ kakaoId }: IUsersServiceCreate) { - const userTempName = 'user' + this.utilsService.getUUID().substring(0, 8); + const userTempName = 'USER' + this.utilsService.getUUID().substring(0, 8); const result = await this.usersRepository.save({ kakaoId, username: userTempName, + handle: userTempName, }); return result; } @@ -63,6 +65,17 @@ export class UsersService { }); return result; } + + async findUserByHandle({ + handle, + }: IUsersServiceFindUserByHandle): Promise { + const result = await this.usersRepository.findOne({ + select: USER_SELECT_OPTION, + where: { handle }, + }); + return result; + } + async findUserByKakaoIdWithToken({ kakaoId, }: IUsersServiceFindUserByKakaoId) { @@ -82,6 +95,7 @@ export class UsersService { async patchUser({ kakaoId, + handle, description, username, }): Promise { @@ -92,11 +106,14 @@ export class UsersService { if (username) { user.username = username; } + if (handle) user.handle = handle; try { const data = await this.usersRepository.save(user); return data; } catch (e) { - throw new ConflictException('UK: username이 중복됩니다.'); + throw new ConflictException( + 'username || handle 값이 Unique하지 않습니다.', + ); } } From a46e2f7e35f3e26c800736ee7c4470827960d622 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 13 May 2024 11:15:37 +0900 Subject: [PATCH 080/236] fix: change user posts fetch api handler from cg name to id --- src/APIs/posts/dtos/fetch-user-posts.input.ts | 4 ++-- src/APIs/posts/posts.repository.ts | 4 ++-- src/app.module.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/APIs/posts/dtos/fetch-user-posts.input.ts b/src/APIs/posts/dtos/fetch-user-posts.input.ts index ea43759..c424316 100644 --- a/src/APIs/posts/dtos/fetch-user-posts.input.ts +++ b/src/APIs/posts/dtos/fetch-user-posts.input.ts @@ -5,11 +5,11 @@ import { CursorFetchPosts } from './cursor-fetch-posts.dto'; export class FetchUserPostsInput extends CursorFetchPosts { @ApiProperty({ - description: '필터링할 카테고리 이름', + description: '필터링할 카테고리 아이디', type: String, required: false, }) @IsOptional() @Type(() => String) - category_name?: string | null; + categoryId?: string | null; } diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index d48e22e..31c384c 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -161,8 +161,8 @@ export class PostsRepository extends Repository { const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); if (cursorOption.category_name) { - queryBuilder.andWhere('postCategory.name = :category_name', { - category_name: cursorOption.category_name, + queryBuilder.andWhere('postCategory.id = :categoryId', { + categoryId: cursorOption.categoryId, }); } queryBuilder diff --git a/src/app.module.ts b/src/app.module.ts index d20ceeb..83d39a8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -60,7 +60,7 @@ import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: true, + synchronize: false, logging: true, }), ], From f908d1d28a632215a52d94db45a196f5305caa5f Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 13 May 2024 11:29:24 +0900 Subject: [PATCH 081/236] fix: divide toggle like into separate create & delete logic --- .../likes/dtos/toggle-like-response.dto.ts | 3 + src/APIs/likes/likes.controller.ts | 43 +++++++++-- src/APIs/likes/likes.service.ts | 74 ++++++++++++++++++- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/APIs/likes/dtos/toggle-like-response.dto.ts b/src/APIs/likes/dtos/toggle-like-response.dto.ts index eb28334..f207fca 100644 --- a/src/APIs/likes/dtos/toggle-like-response.dto.ts +++ b/src/APIs/likes/dtos/toggle-like-response.dto.ts @@ -1,8 +1,11 @@ import { OmitType } from '@nestjs/swagger'; import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Likes } from '../entities/like.entity'; export class ToggleLikeResponseDto extends OmitType(Posts, [ 'postBackground', 'postCategory', 'user', ]) {} + +export class FetchLikeDto extends OmitType(Likes, ['user', 'posts']) {} diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index 75f4a76..3f5742e 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -1,5 +1,6 @@ import { Controller, + Delete, Get, HttpCode, Param, @@ -10,13 +11,15 @@ import { import { LikesService } from './likes.service'; import { ApiCookieAuth, + ApiCreatedResponse, + ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; +import { FetchLikeDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @@ -27,21 +30,45 @@ export class LikesController { constructor(private readonly likesService: LikesService) {} @ApiOperation({ - summary: '좋아요 토글하기', - description: '로그인 된 유저가 {id}인 게시글에 좋아요를 토글한다.', + summary: '좋아요', + description: '로그인 된 유저가 {id}인 게시글에 좋아요를 한다.', }) @ApiCookieAuth() - @ApiOkResponse({ description: '토글 성공', type: ToggleLikeResponseDto }) + @ApiCreatedResponse({ + description: '좋아요 성공', + type: FetchLikeDto, + }) @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) @UseGuards(AuthGuardV2) - @HttpCode(200) + @HttpCode(201) @Post() - async toggleLike( + async like( + @Param('postId') id: number, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.likesService.like({ id, kakaoId }); + } + + @ApiOperation({ + summary: '좋아요 취소', + description: '로그인 된 유저가 {id}인 게시글에 좋아요를 취소한다.', + }) + @ApiCookieAuth() + @ApiNoContentResponse({ + description: '좋아요 취소 성공', + }) + @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) + @UseGuards(AuthGuardV2) + @HttpCode(204) + @Delete() + async deleteLike( @Param('postId') id: number, @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; - return this.likesService.toggleLike({ id, kakaoId }); + await this.likesService.cancel_like({ id, kakaoId }); + return; } @ApiOperation({ diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 8699e0c..d371e43 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -1,9 +1,16 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Posts } from '../posts/entities/posts.entity'; -import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; +import { + FetchLikeDto, + ToggleLikeResponseDto, +} from './dtos/toggle-like-response.dto'; import { FetchLikesDto } from './dtos/fetch-likes.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; @@ -23,6 +30,69 @@ export class LikesService { return false; } + async like({ id, kakaoId }): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const postData = await queryRunner.manager.findOne(Posts, { + where: { id }, + }); + const alreadyLiked = await this.likesRepository.findOne({ + where: { posts: { id }, user: { kakaoId } }, + }); + if (alreadyLiked) { + throw new ConflictException('이미 좋아요 한 게시글입니다.'); + } else { + const likeData = await queryRunner.manager.save(Likes, { + user: kakaoId, + posts: postData, + }); + await queryRunner.manager.update(Posts, postData.id, { + like_count: () => 'like_count +1', + }); + await queryRunner.commitTransaction(); + return likeData; + } + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } + + async cancel_like({ id, kakaoId }) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const postData = await queryRunner.manager.findOne(Posts, { + where: { id }, + }); + const alreadyLiked = await this.likesRepository.findOne({ + where: { posts: { id }, user: { kakaoId } }, + }); + if (!alreadyLiked) { + throw new ConflictException('좋아요 내역을 찾을 수 없습니다.'); + } else { + const likeData = await queryRunner.manager.delete(Likes, { + id: alreadyLiked.id, + }); + await queryRunner.manager.update(Posts, postData.id, { + like_count: () => 'like_count -1', + }); + await queryRunner.commitTransaction(); + return likeData; + } + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } + async toggleLike({ id, kakaoId }): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); From 535eb1c6400ad82326b6ef465f8f377ba85bfa94 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 14 May 2024 00:39:10 +0900 Subject: [PATCH 082/236] feat: exclude comment-fetching in posts detail response API --- src/APIs/posts/posts.controller.ts | 5 ++--- src/APIs/posts/posts.service.ts | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index d6684cf..da06f5a 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -34,7 +34,6 @@ import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { PostResponseDto } from './dtos/post-response.dto'; -import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, @@ -156,11 +155,11 @@ export class PostsController { 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', }) @Get('detail/:postId') - @ApiOkResponse({ type: fetchPostDetailDto }) + @ApiOkResponse({ type: PostResponseDto }) async fetchPostDetail( @Param('postId') id: number, @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; return await this.postsService.fetchDetail({ kakaoId, id }); } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index f329a10..28b81f9 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -21,7 +21,6 @@ import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; import { CommentsService } from '../comments/comments.service'; import { PostResponseDto } from './dtos/post-response.dto'; -import { fetchPostDetailDto } from './dtos/fetch-post-detail.dto'; import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, @@ -180,16 +179,16 @@ export class PostsService { return await this.postsRepository.fetchTempPosts(kakaoId); } - async fetchDetail({ kakaoId, id }): Promise { + async fetchDetail({ kakaoId, id }): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: false }); const scope = await this.followsService.getScope({ from_user: data.userKakaoId, to_user: kakaoId, }); - const comments = await this.commentsService.fetchComments({ postsId: id }); + // const comments = await this.commentsService.fetchComments({ postsId: id }); const post = await this.postsRepository.fetchPostDetail({ id, scope }); - return { comments, post }; + return post; } async softDelete({ kakaoId, id }) { From 00b070618bd5301f9035a545db9f2039ea555ccf Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 14 May 2024 11:36:42 +0900 Subject: [PATCH 083/236] feat: add interfaces on postsService & Repo --- src/APIs/posts/dtos/create-post.dto.ts | 7 - src/APIs/posts/dtos/create-post.input.ts | 19 +-- src/APIs/posts/dtos/publish-post.input.ts | 11 +- src/APIs/posts/entities/posts.entity.ts | 4 +- .../interfaces/posts.repository.interface.ts | 33 +++++ .../interfaces/posts.service.interface.ts | 42 ++++++ src/APIs/posts/posts.controller.ts | 14 +- src/APIs/posts/posts.repository.ts | 86 ++++++----- src/APIs/posts/posts.service.ts | 135 ++++++++++-------- 9 files changed, 216 insertions(+), 135 deletions(-) delete mode 100644 src/APIs/posts/dtos/create-post.dto.ts create mode 100644 src/APIs/posts/interfaces/posts.repository.interface.ts create mode 100644 src/APIs/posts/interfaces/posts.service.interface.ts diff --git a/src/APIs/posts/dtos/create-post.dto.ts b/src/APIs/posts/dtos/create-post.dto.ts deleted file mode 100644 index 9b5ee02..0000000 --- a/src/APIs/posts/dtos/create-post.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CreatePostInput } from './create-post.input'; - -export class CreatePostDto extends CreatePostInput { - userKakaoId: number; - - isPublished: boolean; -} diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index c846d88..d6913f1 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -1,23 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - IsBoolean, - IsEnum, - IsNumber, - IsOptional, - IsString, -} from 'class-validator'; +import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator'; import { OpenScope } from 'src/common/enums/open-scope.enum'; export class CreatePostInput { - @ApiProperty({ - description: '포스트의 고유 아이디', - type: Number, - required: false, - }) - @IsNumber() - @IsOptional() - id?: number; - @ApiProperty({ description: '연결된 카테고리 fk', type: String, @@ -29,6 +14,7 @@ export class CreatePostInput { @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) @IsString() + @IsOptional() postBackgroundId?: string; @ApiProperty({ @@ -55,6 +41,7 @@ export class CreatePostInput { required: false, }) @IsEnum(OpenScope) + @IsOptional() scope?: OpenScope; @ApiProperty({ description: '게시글 내용', type: String }) diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 11b1a57..3a6091a 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -1,18 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsEnum, IsString } from 'class-validator'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { IsBoolean } from 'src/common/validators/isBoolean'; export class PublishPostInput { - @ApiProperty({ - description: '포스트의 고유 아이디', - type: Number, - required: false, - }) - @IsNumber() - @IsOptional() - id?: number; - @ApiProperty({ description: '연결된 카테고리 fk', type: String, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index ceeab24..cdb60b1 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -32,7 +32,7 @@ export class Posts { @IsString() @ApiProperty({ description: '연결된 내지 fk', type: String }) - @Column({ nullable: false }) + @Column({ nullable: true }) @RelationId((posts: Posts) => posts.postBackground) postBackgroundId: string; @@ -115,7 +115,7 @@ export class Posts { @ApiProperty({ description: '연결된 내지', type: PostBackground }) @JoinColumn() @ManyToOne(() => PostBackground, { - nullable: false, + nullable: true, onUpdate: 'CASCADE', onDelete: 'CASCADE', }) diff --git a/src/APIs/posts/interfaces/posts.repository.interface.ts b/src/APIs/posts/interfaces/posts.repository.interface.ts new file mode 100644 index 0000000..8b505fa --- /dev/null +++ b/src/APIs/posts/interfaces/posts.repository.interface.ts @@ -0,0 +1,33 @@ +import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; +import { SortOption } from 'src/common/enums/sort-option'; +import { + IPostsServiceFetchPostsCursor, + IPostsServiceFetchFriendsPostsCursor, + IPostsServiceFetchUserPostsCursor, +} from './posts.service.interface'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; + +export interface IPostsRepoGetCursorQuery { + order: PostsOrderOptionWrap; + sort: SortOption; + take: number; + cursor: string; +} + +export interface IPostsRepoFetchPostsCursor + extends IPostsServiceFetchPostsCursor { + date_filter: Date; +} + +export interface IPostsRepoFetchFriendsPostsCursor + extends Pick { + date_filter: Date; + subQuery: string; +} + +export interface IPostsRepoFetchUserPostsCursor + extends Pick { + date_filter: Date; + scope: OpenScope[]; + userKakaoId: number; +} diff --git a/src/APIs/posts/interfaces/posts.service.interface.ts b/src/APIs/posts/interfaces/posts.service.interface.ts new file mode 100644 index 0000000..39d5ecb --- /dev/null +++ b/src/APIs/posts/interfaces/posts.service.interface.ts @@ -0,0 +1,42 @@ +import { CreatePostInput } from '../dtos/create-post.input'; +import { CursorFetchPosts } from '../dtos/cursor-fetch-posts.dto'; +import { FetchUserPostsInput } from '../dtos/fetch-user-posts.input'; +import { Posts } from '../entities/posts.entity'; + +export interface IPostsServicePostId extends Pick {} + +export interface IPostsServicePostUserIdPair { + id: number; + kakaoId: number; +} + +export interface IPostsServiceCreate extends CreatePostInput { + userKakaoId: number; + + isPublished: boolean; +} + +export interface IPostsServiceFetchPostForUpdate { + id: number; + kakaoId: number; +} + +export interface IPostsServiceCreateCursorResponse { + cursorOption: CursorFetchPosts; + posts: Posts[]; +} + +export interface IPostsServiceFetchPostsCursor { + cursorOption: CursorFetchPosts; +} + +export interface IPostsServiceFetchFriendsPostsCursor { + cursorOption: CursorFetchPosts; + kakaoId: number; +} + +export interface IPostsServiceFetchUserPostsCursor { + cursorOption: FetchUserPostsInput; + targetKakaoId: number; + kakaoId: number; +} diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index da06f5a..1e7fc14 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -50,9 +50,7 @@ export class PostsController { @ApiOperation({ summary: '게시글 등록', - description: `게시글을 등록한다. - id를 입력하지 않으면 생성하고 있는 아이디를 치면 update하는 로직으로 - 바로 게시글 생성에 사용해도 되고, 수정용으로 사용해도 된다.`, + description: '게시글을 등록한다.', }) @Post() @ApiCookieAuth() @@ -80,9 +78,7 @@ export class PostsController { @ApiOperation({ summary: '게시글 임시등록', - description: `게시글을 임시등록한다. - id를 입력하지 않으면 생성하고 있는 아이디를 치면 update하는 로직으로 - 바로 게시글 생성에 사용해도 되고, 수정용으로 사용해도 된다.`, + description: '게시글을 임시등록한다.', }) @Post('temp') @ApiCookieAuth() @@ -227,7 +223,7 @@ export class PostsController { } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return this.postsService.paginateByCustomCursor({ cursorOption }); + return this.postsService.fetchPostsCursor({ cursorOption }); } @ApiOperation({ @@ -247,7 +243,7 @@ export class PostsController { } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return this.postsService.fetchFriendsCursor({ cursorOption, kakaoId }); + return this.postsService.fetchFriendsPostsCursor({ cursorOption, kakaoId }); } @ApiOperation({ @@ -268,7 +264,7 @@ export class PostsController { } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return await this.postsService.fetchUserPosts({ + return await this.postsService.fetchUserPostsCursor({ kakaoId, targetKakaoId, cursorOption, diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 31c384c..b1cb6a7 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -7,6 +7,12 @@ import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto' import { PostsOrderOption } from 'src/common/enums/posts-order-option'; import { PostsFilterOption } from 'src/common/enums/posts-filter-option'; import { SortOption } from 'src/common/enums/sort-option'; +import { + IPostsRepoFetchFriendsPostsCursor, + IPostsRepoFetchPostsCursor, + IPostsRepoFetchUserPostsCursor, + IPostsRepoGetCursorQuery, +} from './interfaces/posts.repository.interface'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -17,9 +23,6 @@ export class PostsRepository extends Repository { .insert() .into(Posts, Object.keys(post)) .values(post) - .orUpdate(Object.keys(post), ['id'], { - skipUpdateIfNoValuesChanged: true, - }) .execute(); } @@ -107,7 +110,9 @@ export class PostsRepository extends Repository { .getManyAndCount(); } - async fetchTempPosts(kakaoId): Promise { + async fetchTempPosts( + kakaoId: number, + ): Promise { return ( this.createQueryBuilder('p') .innerJoin('p.user', 'user') @@ -126,14 +131,14 @@ export class PostsRepository extends Repository { ); } - getCursorQuery({ order, sort, take, cursor }) { - order = PostsOrderOption[order]; + getCursorQuery({ order, sort, take, cursor }: IPostsRepoGetCursorQuery) { + const _order = PostsOrderOption[order]; const queryBuilder = this.createQueryBuilder('p'); const queryByOrderSort = sort === SortOption.ASC - ? `CONCAT(LPAD(p.${order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` - : `CONCAT(LPAD(p.${order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + ? `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; queryBuilder .take(take + 1) @@ -150,30 +155,26 @@ export class PostsRepository extends Repository { .andWhere(queryByOrderSort, { customCursor: cursor, }) - .orderBy(`p.${order}`, sort as any) + .orderBy(`p.${_order}`, sort as any) .addOrderBy('p.id', sort as any); return queryBuilder; } - async fetchUserPosts({ cursorOption, scope, userKakaoId }) { + async fetchPostsCursor({ + cursorOption, + date_filter, + }: IPostsRepoFetchPostsCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - if (cursorOption.category_name) { - queryBuilder.andWhere('postCategory.id = :categoryId', { - categoryId: cursorOption.categoryId, - }); - } - queryBuilder - .andWhere('p.userKakaoId = :userKakaoId', { - userKakaoId, - }) - .andWhere('p.scope IN (:scope)', { scope }); + queryBuilder.andWhere('p.scope IN (:...scopes)', { + scopes: [OpenScope.PUBLIC], + }); - if (cursorOption.date_created) { - queryBuilder.andWhere('p.date_created > :date_created', { - date_created: cursorOption.date_created, + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, }); } @@ -182,7 +183,11 @@ export class PostsRepository extends Repository { return { posts }; } - async paginateByCustomCursorFriends({ cursorOption, subQuery }) { + async fetchFriendsPostsCursor({ + cursorOption, + subQuery, + date_filter, + }: IPostsRepoFetchFriendsPostsCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -192,9 +197,9 @@ export class PostsRepository extends Repository { scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], }); //sql injection 방지를 위해 만드시 enum 거칠 것 - if (cursorOption.date_created) { - queryBuilder.andWhere('p.date_created > :date_created', { - date_created: cursorOption.date_created, + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, }); } @@ -203,19 +208,32 @@ export class PostsRepository extends Repository { return { posts }; } - async paginateByCustomCursor({ cursorOption }) { + async fetchUserPosts({ + cursorOption, + scope, + userKakaoId, + date_filter, + }: IPostsRepoFetchUserPostsCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - queryBuilder.andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC], - }); + if (cursorOption.categoryId) { + queryBuilder.andWhere('postCategory.id = :categoryId', { + categoryId: cursorOption.categoryId, + }); + } + queryBuilder + .andWhere('p.userKakaoId = :userKakaoId', { + userKakaoId, + }) + .andWhere('p.scope IN (:scope)', { scope }); - if (cursorOption.date_created) { - queryBuilder.andWhere('p.date_created > :date_created', { - date_created: cursorOption.date_created, + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, }); } + const posts: Posts[] = await queryBuilder.getMany(); return { posts }; diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 28b81f9..b48c0eb 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -12,7 +12,6 @@ import { Page } from '../../utils/pages/page'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; -import { CreatePostDto } from './dtos/create-post.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { User } from '../users/entities/user.entity'; @@ -31,6 +30,16 @@ import { PostsOrderOption } from 'src/common/enums/posts-order-option'; import { FollowsService } from '../follows/follows.service'; import { DateOption } from 'src/common/enums/date-option'; import { Follow } from '../follows/entities/follow.entity'; +import { + IPostsServiceCreate, + IPostsServiceCreateCursorResponse, + IPostsServiceFetchFriendsPostsCursor, + IPostsServiceFetchPostForUpdate, + IPostsServiceFetchPostsCursor, + IPostsServiceFetchUserPostsCursor, + IPostsServicePostId, + IPostsServicePostUserIdPair, +} from './interfaces/posts.service.interface'; @Injectable() export class PostsService { @@ -61,11 +70,11 @@ export class PostsService { return { image_url }; } - async findPostsById({ id }) { + async findPostsById({ id }: IPostsServicePostId) { return await this.postsRepository.findOne({ where: { id } }); } - async existCheck({ id }) { + async existCheck({ id }: IPostsServicePostId) { const data = await this.findPostsById({ id }); if (!data) throw new NotFoundException('게시글을 찾을 수 없습니다.'); return data; @@ -84,7 +93,7 @@ export class PostsService { .createQueryBuilder('pg') .where('pg.id = :id', { id: posts.postBackgroundId }) .getOne(); - if (!pg) + if (!pg && !passNonEssentail) throw new BadRequestException('존재하지 않는 post_background입니다.'); const us = await this.dataSource .getRepository(User) @@ -94,24 +103,12 @@ export class PostsService { if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); } - async save(createPostDto: CreatePostDto) { + async save(createPostDto: IPostsServiceCreate) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - let post = {}; + const post = {}; try { - if (createPostDto.id) { - post = await queryRunner.manager.findOne(Posts, { - where: { - id: createPostDto.id, - user: { kakaoId: createPostDto.userKakaoId }, - }, - }); - if (!post) { - await delete createPostDto.id; - post = {}; - } - } Object.keys(createPostDto).map((el) => { const value = createPostDto[el]; if (createPostDto[el]) { @@ -132,7 +129,10 @@ export class PostsService { }) .execute(); await queryRunner.commitTransaction(); - return data; + const result = this.postsRepository.findOne({ + where: { id: data.identifiers[0].id }, + }); + return result; } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -146,7 +146,10 @@ export class PostsService { return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); } - async fetchPostForUpdate({ id, kakaoId }): Promise { + async fetchPostForUpdate({ + id, + kakaoId, + }: IPostsServiceFetchPostForUpdate): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: true }); if (data.userKakaoId !== kakaoId) @@ -179,7 +182,10 @@ export class PostsService { return await this.postsRepository.fetchTempPosts(kakaoId); } - async fetchDetail({ kakaoId, id }): Promise { + async fetchDetail({ + kakaoId, + id, + }: IPostsServicePostUserIdPair): Promise { const data = await this.existCheck({ id }); await this.fkValidCheck({ posts: data, passNonEssentail: false }); const scope = await this.followsService.getScope({ @@ -191,11 +197,11 @@ export class PostsService { return post; } - async softDelete({ kakaoId, id }) { + async softDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { return await this.postsRepository.softDelete({ user: { kakaoId }, id }); } - async hardDelete({ kakaoId, id }) { + async hardDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { return await this.postsRepository.delete({ user: { kakaoId }, id }); } @@ -203,7 +209,9 @@ export class PostsService { async createCursorResponse({ cursorOption, posts, - }): Promise> { + }: IPostsServiceCreateCursorResponse): Promise< + CustomCursorPageDto + > { const order = PostsOrderOption[cursorOption.order]; let hasNextData: boolean = true; let customCursor: string; @@ -231,13 +239,54 @@ export class PostsService { return new CustomCursorPageDto(responseData, customCursorPageMetaDto); } - async fetchUserPosts({ + + async fetchPostsCursor({ + cursorOption, + }: IPostsServiceFetchPostsCursor): Promise< + CustomCursorPageDto + > { + let date_filter: Date; + if (cursorOption.date_created) + date_filter = this.getDate(cursorOption.date_created); + const { posts } = await this.postsRepository.fetchPostsCursor({ + cursorOption, + date_filter, + }); + return await this.createCursorResponse({ posts, cursorOption }); + } + + async fetchFriendsPostsCursor({ + cursorOption, + kakaoId, + }: IPostsServiceFetchFriendsPostsCursor): Promise< + CustomCursorPageDto + > { + let date_filter: Date; + if (cursorOption.date_created) + date_filter = this.getDate(cursorOption.date_created); + const subQuery = await this.dataSource + .createQueryBuilder(Follow, 'n') + .select('n.toUserKakaoId') + .where(`n.fromUserKakaoId = ${kakaoId}`) + .getQuery(); + const { posts } = await this.postsRepository.fetchFriendsPostsCursor({ + cursorOption, + subQuery, + date_filter, + }); + return await this.createCursorResponse({ posts, cursorOption }); + } + + async fetchUserPostsCursor({ kakaoId, targetKakaoId, cursorOption, - }): Promise> { + }: IPostsServiceFetchUserPostsCursor): Promise< + CustomCursorPageDto + > { + let date_filter: Date; if (cursorOption.date_created) - cursorOption.date_created = this.getDate(cursorOption.date_created); + date_filter = this.getDate(cursorOption.date_created); const scope = await this.followsService.getScope({ from_user: targetKakaoId, @@ -245,23 +294,13 @@ export class PostsService { }); const { posts } = await this.postsRepository.fetchUserPosts({ cursorOption, + date_filter, scope, userKakaoId: targetKakaoId, }); return await this.createCursorResponse({ posts, cursorOption }); } - async paginateByCustomCursor({ - cursorOption, - }): Promise> { - if (cursorOption.date_created) - cursorOption.date_created = this.getDate(cursorOption.date_created); - const { posts } = await this.postsRepository.paginateByCustomCursor({ - cursorOption, - }); - return await this.createCursorResponse({ posts, cursorOption }); - } - async createCustomCursor({ post, order }): Promise { const id = post.id; const _order = post[order]; @@ -282,25 +321,7 @@ export class PostsService { return defaultCustomCursor; } - async fetchFriendsCursor({ - cursorOption, - kakaoId, - }): Promise> { - if (cursorOption.date_created) - cursorOption.date_created = this.getDate(cursorOption.date_created); - const subQuery = await this.dataSource - .createQueryBuilder(Follow, 'n') - .select('n.toUserKakaoId') - .where(`n.fromUserKakaoId = ${kakaoId}`) - .getQuery(); - const { posts } = await this.postsRepository.paginateByCustomCursorFriends({ - cursorOption, - subQuery, - }); - return await this.createCursorResponse({ posts, cursorOption }); - } - - getDate(date_created) { + getDate(date_created: DateOption): Date { let currentDate = new Date(); switch (date_created) { case DateOption.WEEK: From e6efdc3955156ef62b59e1471f8d218f33bf2f13 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 14 May 2024 12:17:35 +0900 Subject: [PATCH 084/236] fix: revised jwt decode on agreements API --- src/APIs/agreements/agreements.controller.ts | 8 +-- src/APIs/agreements/agreements.service.ts | 2 +- .../agreements/entities/agreement.entity.ts | 5 +- src/APIs/feedbacks/feedbacks.service.ts | 5 +- src/APIs/likes/dtos/fetch-likes.dto.ts | 14 +++++- src/APIs/likes/entities/like.entity.ts | 4 ++ src/APIs/likes/likes.controller.ts | 5 +- src/APIs/likes/likes.service.ts | 5 +- src/APIs/posts/dtos/create-post.input.ts | 8 +-- src/APIs/posts/dtos/publish-post.input.ts | 6 +-- src/APIs/posts/posts.controller.ts | 2 +- src/APIs/posts/posts.repository.ts | 50 +++++++++---------- src/APIs/posts/posts.service.ts | 17 ++++--- src/app.module.ts | 2 +- 14 files changed, 77 insertions(+), 56 deletions(-) diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index 2fff8ca..f1897fe 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -47,7 +47,7 @@ export class AgreementsController { @Req() req: Request, @Body() body: CreateAgreementsInput, ): Promise { - const kakaoId = req.user.kakaoId; + const kakaoId = req.user.userId; return await this.agreementsService.create({ ...body, kakaoId }); } @@ -57,7 +57,7 @@ export class AgreementsController { @UseGuards(AuthGuardV2) @Get('me/agreements') async fetchAgreements(@Req() req: Request): Promise { - const kakaoId = req.user.kakaoId; + const kakaoId = req.user.userId; return await this.agreementsService.fetchAll({ kakaoId }); } @@ -71,7 +71,7 @@ export class AgreementsController { @Req() req: Request, @Param('userId') targetUserKakaoId: number, ): Promise { - const kakaoId = req.user.kakaoId; + const kakaoId = req.user.userId; await this.agreementsService.adminCheck({ kakaoId }); return await this.agreementsService.fetchAll({ kakaoId: targetUserKakaoId, @@ -88,7 +88,7 @@ export class AgreementsController { @Param('agreementId') id: number, @Body() body: PatchAgreementInput, ): Promise { - const userKakaoId = req.user.kakaoId; + const userKakaoId = req.user.userId; return await this.agreementsService.patch({ ...body, id, userKakaoId }); } } diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index 6d10338..63a56a8 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -32,7 +32,7 @@ export class AgreementsService { return await this.agreementsRepository.save({ agreementType, isAgreed, - user: { kakaoId }, + userKakaoId: kakaoId, }); } diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 6552b39..12fb5d0 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -21,7 +21,7 @@ export class Agreement { id: number; @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { + @ManyToOne(() => User, (user) => user.kakaoId, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', @@ -29,8 +29,9 @@ export class Agreement { user: User; @ApiProperty({ type: Number, description: '약관에 동의한 유저 id' }) + @Column() @RelationId((agreement: Agreement) => agreement.user) - @IsNumber() + // @IsNumber() userKakaoId: number; @ApiProperty({ diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts index c3396be..1790410 100644 --- a/src/APIs/feedbacks/feedbacks.service.ts +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -15,7 +15,10 @@ export class FeedbacksService { kakaoId, content, }: IFeedbacksServiceCreate): Promise { - return await this.feedbacksRepository.save({ content, user: { kakaoId } }); + return await this.feedbacksRepository.save({ + content, + userKakaoId: kakaoId, + }); } async fetchAll({ kakaoId }): Promise { diff --git a/src/APIs/likes/dtos/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts index 63247ba..5e4f3ee 100644 --- a/src/APIs/likes/dtos/fetch-likes.dto.ts +++ b/src/APIs/likes/dtos/fetch-likes.dto.ts @@ -1,6 +1,18 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; +import { Likes } from '../entities/like.entity'; +import { Posts } from 'src/APIs/posts/entities/posts.entity'; export class FetchLikesDto { @ApiProperty({ type: Number, description: 'post_id' }) id: number; } + +export class FetchLikeResponseDto extends PickType(Likes, ['id', 'postsId']) { + @ApiProperty({ + type: OmitType(Posts, ['user', 'postCategory', 'postBackground']), + }) + posts: Omit; + + @ApiProperty({ type: Number }) + user: number; +} diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index a188688..4fd959f 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -36,6 +36,10 @@ export class Likes { }) posts: Posts; + @ApiProperty({ + type: Number, + description: '게시글 아이디', + }) @RelationId((like: Likes) => like.posts) postsId: number; } diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index 3f5742e..ae2cda4 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -23,6 +23,7 @@ import { FetchLikeDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; @ApiTags('게시글 API') @Controller('posts/:postId/like') @@ -36,7 +37,7 @@ export class LikesController { @ApiCookieAuth() @ApiCreatedResponse({ description: '좋아요 성공', - type: FetchLikeDto, + type: FetchLikeResponseDto, }) @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) @UseGuards(AuthGuardV2) @@ -45,7 +46,7 @@ export class LikesController { async like( @Param('postId') id: number, @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; return await this.likesService.like({ id, kakaoId }); } diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index d371e43..4dc3cba 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -11,8 +11,9 @@ import { FetchLikeDto, ToggleLikeResponseDto, } from './dtos/toggle-like-response.dto'; -import { FetchLikesDto } from './dtos/fetch-likes.dto'; +import { FetchLikeResponseDto, FetchLikesDto } from './dtos/fetch-likes.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; +import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; @Injectable() export class LikesService { @@ -30,7 +31,7 @@ export class LikesService { return false; } - async like({ id, kakaoId }): Promise { + async like({ id, kakaoId }): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index d6913f1..f1cb19f 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -12,10 +12,10 @@ export class CreatePostInput { @IsOptional() postCategoryId?: string; - @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) - @IsString() - @IsOptional() - postBackgroundId?: string; + // @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) + // @IsString() + // @IsOptional() + // postBackgroundId?: string; @ApiProperty({ description: '제목(최대 100자)', diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 3a6091a..2dfb30e 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -11,9 +11,9 @@ export class PublishPostInput { @IsString() postCategoryId: string; - @ApiProperty({ description: '연결된 내지 fk', type: String }) - @IsString() - postBackgroundId: string; + // @ApiProperty({ description: '연결된 내지 fk', type: String }) + // @IsString() + // postBackgroundId: string; @ApiProperty({ description: '제목(최대 100자)', diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 1e7fc14..99994f6 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -148,7 +148,7 @@ export class PostsController { @ApiOperation({ summary: '게시글 디테일 뷰 fetch', description: - 'id에 해당하는 게시글과 댓글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', + 'id에 해당하는 게시글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', }) @Get('detail/:postId') @ApiOkResponse({ type: PostResponseDto }) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index b1cb6a7..ee570bd 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -30,8 +30,8 @@ export class PostsRepository extends Repository { return ( this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ 'user.kakaoId', 'user.description', @@ -57,8 +57,8 @@ export class PostsRepository extends Repository { }); return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ 'user.kakaoId', 'user.description', @@ -72,8 +72,8 @@ export class PostsRepository extends Repository { async fetchPostForUpdate(id) { return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - // .innerJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ 'user.kakaoId', 'user.description', @@ -87,8 +87,8 @@ export class PostsRepository extends Repository { async fetchFriendsPosts(subQuery, page) { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ 'user.kakaoId', 'user.description', @@ -113,22 +113,20 @@ export class PostsRepository extends Repository { async fetchTempPosts( kakaoId: number, ): Promise { - return ( - this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - // .innerJoinAndSelect('p.postCategory', 'postCategory') - .addSelect([ - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.userKakaoId = :kakaoId', { kakaoId }) - .andWhere(`p.isPublished = false`) - .orderBy('p.id', 'DESC') - .getMany() - ); + return this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') + .addSelect([ + 'user.kakaoId', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.userKakaoId = :kakaoId', { kakaoId }) + .andWhere(`p.isPublished = false`) + .orderBy('p.id', 'DESC') + .getMany(); } getCursorQuery({ order, sort, take, cursor }: IPostsRepoGetCursorQuery) { @@ -143,8 +141,8 @@ export class PostsRepository extends Repository { queryBuilder .take(take + 1) .innerJoin('p.user', 'user') - .innerJoinAndSelect('p.postBackground', 'postBackground') - .innerJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.postBackground', 'postBackground') + .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ 'user.kakaoId', 'user.description', diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index b48c0eb..48f640f 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -13,7 +13,7 @@ import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; -import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; +// import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; @@ -88,13 +88,13 @@ export class PostsService { .getOne(); if (!pc && !passNonEssentail) throw new BadRequestException('존재하지 않는 post_category입니다.'); - const pg = await this.dataSource - .getRepository(PostBackground) - .createQueryBuilder('pg') - .where('pg.id = :id', { id: posts.postBackgroundId }) - .getOne(); - if (!pg && !passNonEssentail) - throw new BadRequestException('존재하지 않는 post_background입니다.'); + // const pg = await this.dataSource + // .getRepository(PostBackground) + // .createQueryBuilder('pg') + // .where('pg.id = :id', { id: posts.postBackgroundId }) + // .getOne(); + // if (!pg && !passNonEssentail) + // throw new BadRequestException('존재하지 않는 post_background입니다.'); const us = await this.dataSource .getRepository(User) .createQueryBuilder('us') @@ -194,6 +194,7 @@ export class PostsService { }); // const comments = await this.commentsService.fetchComments({ postsId: id }); const post = await this.postsRepository.fetchPostDetail({ id, scope }); + console.log(data, post); return post; } diff --git a/src/app.module.ts b/src/app.module.ts index 83d39a8..d20ceeb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -60,7 +60,7 @@ import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: false, + synchronize: true, logging: true, }), ], From 95cc466c0d9e23ce45265ec80f4d01de497aef45 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 15 May 2024 13:16:57 +0900 Subject: [PATCH 085/236] refactor: change endpoints of sticker related APIs --- .../dtos/create-stickerBlock.dto.ts | 9 +- .../stickerBlocks/stickerBlocks.controller.ts | 22 +++- .../dtos/create-sticker-category.dto.ts | 6 + .../stickerCategories.controller.ts | 61 +++++---- .../stickerCategories.service.ts | 6 +- src/APIs/stickers/dtos/update-sticker.dto.ts | 23 ++-- src/APIs/stickers/stickers.controller.ts | 124 ++++++------------ src/APIs/stickers/stickers.service.ts | 30 ++--- 8 files changed, 129 insertions(+), 152 deletions(-) create mode 100644 src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts index 8d036a0..66876a0 100644 --- a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts +++ b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts @@ -1,8 +1,15 @@ import { OmitType } from '@nestjs/swagger'; import { StickerBlock } from '../entities/stickerblock.entity'; -export class CreateStickerBlockDto extends OmitType(StickerBlock, [ +export class CreateStickerBlockInput extends OmitType(StickerBlock, [ 'id', 'posts', + 'postsId', 'sticker', + 'stickerId', ]) {} + +export class CreateStickerBlockDto extends CreateStickerBlockInput { + postsId: number; + stickerId: number; +} diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index cde7217..9f05fa3 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -1,10 +1,10 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Param, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; -import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; +import { CreateStickerBlockInput } from './dtos/create-stickerBlock.dto'; -@ApiTags('스티커 블록 API') -@Controller('stickerBlocks') +@ApiTags('게시글 API') +@Controller('posts/:postId/stickers') export class StickerBlocksController { constructor(private readonly stickerBlocksService: StickerBlocksService) {} @@ -13,8 +13,16 @@ export class StickerBlocksController { description: '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', }) - @Post() - async createStickerBlock(@Body() body: CreateStickerBlockDto) { - return await this.stickerBlocksService.create(body); + @Post(':stickerId') + async createStickerBlock( + @Body() body: CreateStickerBlockInput, + @Param('postId') postsId: number, + @Param('stickerId') stickerId: number, + ) { + return await this.stickerBlocksService.create({ + ...body, + postsId, + stickerId, + }); } } diff --git a/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts b/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts new file mode 100644 index 0000000..5c16f74 --- /dev/null +++ b/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateStickerCategoryInput { + @ApiProperty({ type: String, description: '스티커 이름' }) + name: string; +} diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 1326a0e..2d08eed 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -18,37 +18,63 @@ import { Request } from 'express'; import { MapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { CreateStickerCategoryInput } from './dtos/create-sticker-category.dto'; -@ApiTags('스티커 카테고리 API') -@Controller('stickercg') +@ApiTags('스티커 API') +@Controller() export class StickerCategoriesController { constructor( private readonly stickerCategoriesService: StickerCategoriesService, ) {} @ApiOperation({ - summary: '스티커 카테고리 생성', + summary: '카테고리 fetchAll', + description: '카테고리를 모두 조회한다.', + }) + @Get('stickers/categories') + async fetchCategories() { + return await this.stickerCategoriesService.fetchCategories(); + } + + @ApiOperation({ + summary: '카테고리 id에 해당하는 스티커를 fetchAll', + description: '카테고리를 id로 찾고, 이에 매핑된 스티커들을 가져온다', + }) + @Get('stickers/categories/:id') + async fetchStickersByCategoryName(@Param('id') id: string) { + return await this.stickerCategoriesService.fetchStickersByCategoryId({ + id, + }); + } + + @ApiTags('어드민 API') + @ApiOperation({ + summary: '[어드민용] 스티커 카테고리 생성', description: '[어드민 전용] 스티커 카테고리를 만든다.', }) @ApiOkResponse({ description: '생성 완료', type: StickerCategory }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @Post('create/:name') - async createCategory(@Req() req: Request, @Param('name') name: string) { + @Post('users/admin/stickers/categories') + async createCategory( + @Req() req: Request, + @Body() body: CreateStickerCategoryInput, + ) { const kakaoId = req.user.userId; return await this.stickerCategoriesService.createCategory({ kakaoId, - name, + ...body, }); } + @ApiTags('어드민 API') @ApiOperation({ - summary: '스티커와 카테고리 매핑', + summary: '[어드민용] 스티커와 카테고리 매핑', description: '[어드민 전용] 스티커에 카테고리를 매핑한다.', }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @Post('map') + @Post('users/admin/stickers/map') async mapCategory( @Req() req: Request, @Body() mapCategoryDto: MapCategoryDto, @@ -59,23 +85,4 @@ export class StickerCategoriesController { ...mapCategoryDto, }); } - @ApiOperation({ - summary: '카테고리 fetchAll', - description: '카테고리를 모두 조회한다.', - }) - @Get() - async fetchCategories() { - return await this.stickerCategoriesService.fetchCategories(); - } - - @ApiOperation({ - summary: '카테고리 이름에 해당하는 스티커를 fetchAll', - description: '카테고리를 이름으로 찾고, 이에 매핑된 스티커들을 가져온다', - }) - @Get('fetch/:name') - async fetchStickersByCategoryName(@Param('name') name: string) { - return await this.stickerCategoriesService.fetchStickersByCategoryName({ - name, - }); - } } diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 0ed1437..64209d7 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -55,11 +55,11 @@ export class StickerCategoriesService { .execute(); } - async fetchStickersByCategoryName({ name }) { - await this.existCheckByName({ name }); + async fetchStickersByCategoryId({ id }) { + await this.existCheckById({ id }); return await this.stickerCategoryMappersRepository.find({ relations: { sticker: true, stickerCategory: true }, - where: { stickerCategory: { name }, sticker: { isDefault: true } }, + where: { stickerCategory: { id }, sticker: { isDefault: true } }, }); } } diff --git a/src/APIs/stickers/dtos/update-sticker.dto.ts b/src/APIs/stickers/dtos/update-sticker.dto.ts index 7d41d73..c715c6d 100644 --- a/src/APIs/stickers/dtos/update-sticker.dto.ts +++ b/src/APIs/stickers/dtos/update-sticker.dto.ts @@ -1,17 +1,24 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsUrl } from 'class-validator'; -import { URL } from 'url'; +import { IsBoolean, IsNumber, IsOptional, IsUrl } from 'class-validator'; export class UpdateStickerInput { - @ApiProperty({ description: '찾을 스티커의 id', type: Number }) - @IsNumber() - id: number; - - @ApiProperty({ description: '변경할 url', type: String }) + @ApiProperty({ description: '변경할 url', type: String, required: false }) @IsUrl() - image_url: string; + @IsOptional() + image_url?: string; + + @ApiProperty({ + description: '재사용 가능 여부 설정', + type: Boolean, + required: false, + }) + @IsBoolean() + @IsOptional() + isReusable?: boolean; } export class UpdateStickerDto extends UpdateStickerInput { @IsNumber() kakaoId: number; + + id: number; } diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index e1a6a44..9bfec68 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -21,20 +21,17 @@ import { ApiOkResponse, ApiOperation, ApiTags, - ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; -import { RemoveBgDto } from './dtos/remove-bg.dto'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { FindStickerInput } from './dtos/find-sticker.dto'; import { UpdateStickerInput } from './dtos/update-sticker.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('스티커 API') -@Controller('stickers') +@Controller() export class StickersController { constructor(private readonly stickersService: StickersService) {} @@ -53,7 +50,7 @@ export class StickersController { }) @UseGuards(AuthGuardV2) @ApiCookieAuth() - @Post('private') + @Post('stickers/private') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async createPrivateSticker( @@ -67,38 +64,6 @@ export class StickersController { }); } - @ApiOperation({ - summary: '[어드민용] 공용 스티커를 업로드한다.', - description: - '블꾸에서 제작한 스티커를 업로드한다. 어드민 권한이 있는 유저 전용. 카테고리와 매핑을 해주어야 조회 가능.', - }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: '업로드 할 파일', - type: ImageUploadDto, - }) - @ApiCreatedResponse({ - description: '이미지 서버에 파일 업로드 완료', - type: Sticker, - }) - @ApiUnauthorizedResponse({ description: '어드민이 아님' }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @Post('public') - @UseInterceptors(FileInterceptor('file')) - @HttpCode(201) - async createPublicSticker( - @Req() req: Request, - @UploadedFile() file: Express.Multer.File, - ): Promise { - const userKakaoId = req.user.userId; - return await this.stickersService.createPublicSticker({ - userKakaoId, - file, - }); - } - - @Get('private') @ApiOperation({ summary: '재사용 가능한 private 스티커를 fetch한다.', description: @@ -108,23 +73,32 @@ export class StickersController { @UseGuards(AuthGuardV2) @ApiCookieAuth() @HttpCode(200) + @Get('stickers/private') async fetchPrivateStickers(@Req() req: Request): Promise { const userKakaoId = req.user.userId; return await this.stickersService.fetchUserStickers({ userKakaoId }); } @ApiOperation({ - summary: '스티커 재사용 여부를 토글한다.', + summary: '스티커의 image_url 혹은 재사용 여부를 설정한다.', description: - '본인이 만든 스티커의 재사용 여부를 토글한다. 보관함 저장 혹은 삭제 용도로 사용할 것', + '본인이 만든 스티커를 patch한다. image_url 변경 시 기존의 이미지는 s3에서 제거된다.', }) - @Post('toggle/:id') + @Patch('stickers/:id') @UseGuards(AuthGuardV2) @ApiCookieAuth() @HttpCode(200) - async toggleReusable(@Req() req: Request, @Param('id') id: number) { - const userKakaoId = req.user.userId; - return await this.stickersService.toggleReusable({ userKakaoId, id }); + async toggleReusable( + @Req() req: Request, + @Param('id') id: number, + @Body() body: UpdateStickerInput, + ) { + const kakaoId = req.user.userId; + return await this.stickersService.updateSticker({ + kakaoId, + id, + ...body, + }); } @ApiOperation({ @@ -132,60 +106,40 @@ export class StickersController { description: '블꾸가 만든 스티커들을 fetch한다.', }) @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) - @Get('public') + @Get('stickers') @HttpCode(200) async fetchPublicStickers(): Promise { return await this.stickersService.fetchPublicStickers(); } + @ApiTags('어드민 API') @ApiOperation({ - summary: '스티커 배경을 제거한다.', - description: `스티커 배경을 제거하고, url을 받는다. 스티커에 적용하려면 update 필요.\n - workflow: post('background') => delete('s3') => patch('image') - `, - }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @Post('background') - async removeBg(@Body() body: RemoveBgDto): Promise { - return await this.stickersService.removeBg({ url: body.url }); - } - - @Patch('image') - @ApiOperation({ - summary: '스티커 객체 이미지 수정', + summary: '[어드민용] 공용 스티커를 업로드한다.', description: - '스티커 객체의 이미지 url을 변경한다. 호출 이전에 기존의 이미지 제거를 권장.', + '블꾸에서 제작한 스티커를 업로드한다. 어드민 권한이 있는 유저 전용. 카테고리와 매핑을 해주어야 조회 가능.', + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: '업로드 할 파일', + type: ImageUploadDto, + }) + @ApiCreatedResponse({ + description: '이미지 서버에 파일 업로드 완료', + type: Sticker, }) @UseGuards(AuthGuardV2) @ApiCookieAuth() - async updateSticker( + @Post('users/admin/stickers') + @UseInterceptors(FileInterceptor('file')) + @HttpCode(201) + async createPublicSticker( @Req() req: Request, - @Body() body: UpdateStickerInput, + @UploadedFile() file: Express.Multer.File, ): Promise { - const kakaoId = req.user.userId; - return await this.stickersService.updateSticker({ - kakaoId, - ...body, - }); - } - - @ApiOperation({ - summary: 's3에 업로드된 이미지 삭제', - description: `s3에 올라간 파일을 삭제한다. 스티커 객체가 삭제되지는 않는다.
- sticker 객채에 새로운 이미지를 업데이트 해줄 때, 기존의 이미지를 제거할 때만 사용.
- 로직 순서: delete('s3') => patch('image')
- **만약 사용중인 객체의 이미지만 제거 할 경우 이미지가 깨진다.**`, - }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @Delete('s3') - @HttpCode(200) - async removeS3(@Body() body: FindStickerInput, @Req() req: Request) { - const kakaoId = req.user.userId; - return await this.stickersService.removeFromS3({ - id: body.id, - kakaoId, + const userKakaoId = req.user.userId; + return await this.stickersService.createPublicSticker({ + userKakaoId, + file, }); } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index ff84b68..f19b18b 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -1,8 +1,4 @@ -import { - Injectable, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Sticker } from './entities/sticker.entity'; import { InjectRepository } from '@nestjs/typeorm'; @@ -87,15 +83,6 @@ export class StickersService { return data; } - async toggleReusable({ userKakaoId, id }) { - const sticker = await this.stickersRepository.findOne({ where: { id } }); - if (!sticker) throw new NotFoundException('스티커가 존재하지 않습니다.'); - if (sticker.userKakaoId != userKakaoId) - throw new UnauthorizedException('스티커 제작자가 아닙니다.'); - return await this.stickersRepository.update(id, { - isReusable: () => '!isReusable', - }); - } async fetchUserStickers({ userKakaoId }): Promise { return await this.stickersRepository.find({ where: { userKakaoId, isReusable: true, isDefault: false }, @@ -125,6 +112,7 @@ export class StickersService { async updateSticker({ image_url, + isReusable, kakaoId, id, }: UpdateStickerDto): Promise { @@ -136,14 +124,14 @@ export class StickersService { throw new NotFoundException( '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', ); - await this.stickersRepository - .createQueryBuilder() - .update(Sticker) - .set({ image_url }) - .where('id = :id', { id }) - .execute(); const data = await this.stickersRepository.findOne({ where: { id } }); - return data; + if (isReusable) data.isReusable = isReusable; + if (image_url) { + await this.removeFromS3({ id, kakaoId }); + data.image_url = image_url; + } + const result = await this.stickersRepository.save(data); + return result; } catch (e) { throw e; } From 7690367b3dcdbec72db0c92f9e9cefbd6bbc5ce1 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 19:29:21 +0900 Subject: [PATCH 086/236] fix: local hosting --- src/APIs/auth/auth.controller.ts | 22 +++++++++++++++++++--- src/APIs/posts/dtos/create-post.input.ts | 8 ++++---- src/APIs/posts/dtos/fetch-posts.dto.ts | 2 ++ src/APIs/posts/dtos/publish-post.input.ts | 9 ++++++--- src/APIs/posts/entities/posts.entity.ts | 4 ++++ src/APIs/stickers/stickers.controller.ts | 2 -- src/app.module.ts | 2 +- 7 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 002c8f2..8609e30 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -40,9 +40,25 @@ export class AuthController { const { accessToken, refreshToken } = await this.authService.getJWT({ kakaoId: req.user.kakaoId, }); - res.cookie('accessToken', accessToken, { httpOnly: true }); - res.cookie('refreshToken', refreshToken, { httpOnly: true }); - res.cookie('isLoggedIn', true, { httpOnly: false }); + + // 클라이언트 도메인 설정 + const clientHost = req.headers.host; + let clientDomain; + if (clientHost.includes('localhost')) { + clientDomain = 'localhost'; + } else { + clientDomain = process.env.CLIENT_URL; + } + res.cookie('accessToken', accessToken, { + httpOnly: true, + domain: clientDomain, + }); + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + domain: clientDomain, + }); + res.cookie('isLoggedIn', true, { httpOnly: false, domain: clientDomain }); + return res.redirect(process.env.CLIENT_URL); } diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index f1cb19f..d6913f1 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -12,10 +12,10 @@ export class CreatePostInput { @IsOptional() postCategoryId?: string; - // @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) - // @IsString() - // @IsOptional() - // postBackgroundId?: string; + @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) + @IsString() + @IsOptional() + postBackgroundId?: string; @ApiProperty({ description: '제목(최대 100자)', diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index e301210..efbef5d 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -49,6 +49,8 @@ export const FETCH_POST_OPTION = { date_deleted: true, }, title: true, + content: true, + main_description: true, image_url: true, main_image_url: true, isPublished: true, diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 2dfb30e..24d102e 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -11,9 +11,9 @@ export class PublishPostInput { @IsString() postCategoryId: string; - // @ApiProperty({ description: '연결된 내지 fk', type: String }) - // @IsString() - // postBackgroundId: string; + @ApiProperty({ description: '연결된 내지 fk', type: String }) + @IsString() + postBackgroundId: string; @ApiProperty({ description: '제목(최대 100자)', @@ -42,6 +42,9 @@ export class PublishPostInput { @IsString() content: string; + @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) + main_description: string; + @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) @IsString() image_url: string; diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index cdb60b1..de965e5 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -95,6 +95,10 @@ export class Posts { @Column('longtext') content: string; + @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) + @Column() + main_description: string; + @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) @Column() image_url: string; diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 9bfec68..aa6044f 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, - Delete, Get, HttpCode, Param, @@ -26,7 +25,6 @@ import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; -import { FindStickerInput } from './dtos/find-sticker.dto'; import { UpdateStickerInput } from './dtos/update-sticker.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; diff --git a/src/app.module.ts b/src/app.module.ts index d20ceeb..83d39a8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -60,7 +60,7 @@ import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: true, + synchronize: false, logging: true, }), ], From db637cb2070cc8fa5b027eac1ca91ca60c3a7900 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 20:02:52 +0900 Subject: [PATCH 087/236] fix: local hosting option --- src/APIs/auth/auth.controller.ts | 5 +++++ src/APIs/posts/posts.repository.ts | 5 +++++ src/app.module.ts | 3 ++- src/common/validators/isBoolean.ts | 4 ++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 8609e30..ff1bb96 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -49,13 +49,18 @@ export class AuthController { } else { clientDomain = process.env.CLIENT_URL; } + res.cookie('accessToken', accessToken, { httpOnly: true, domain: clientDomain, + sameSite: 'lax', + secure: true, }); res.cookie('refreshToken', refreshToken, { httpOnly: true, domain: clientDomain, + sameSite: 'lax', + secure: true, }); res.cookie('isLoggedIn', true, { httpOnly: false, domain: clientDomain }); diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index ee570bd..afbe906 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -33,6 +33,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', @@ -60,6 +61,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', @@ -75,6 +77,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', @@ -118,6 +121,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', @@ -144,6 +148,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', diff --git a/src/app.module.ts b/src/app.module.ts index 83d39a8..cd922a1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -21,6 +21,7 @@ import { AuthTokenMiddleware } from './common/middlewares/auth-token.middleware' import { JwtModule } from '@nestjs/jwt'; import { AgreementsModule } from './APIs/agreements/agreements.module'; import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; +import { parseBoolean } from './common/validators/isBoolean'; @Module({ imports: [ @@ -60,7 +61,7 @@ import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_DATABASE, entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: false, + synchronize: parseBoolean(process.env.DATABASE_SYNCHRO), logging: true, }), ], diff --git a/src/common/validators/isBoolean.ts b/src/common/validators/isBoolean.ts index cebff6d..d297c7c 100644 --- a/src/common/validators/isBoolean.ts +++ b/src/common/validators/isBoolean.ts @@ -2,6 +2,10 @@ import { applyDecorators } from '@nestjs/common'; import { Transform } from 'class-transformer'; import { IsBoolean as OriginalIsBoolean } from 'class-validator'; +export function parseBoolean(value: string): boolean { + return value.toLowerCase() === 'true'; +} + export function IsBoolean() { return applyDecorators(ToBoolean(), OriginalIsBoolean()); } From 9332de91f10cf0077173849beb6fe72791cf8637 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 21:21:27 +0900 Subject: [PATCH 088/236] fix: sameSite policy --- src/APIs/auth/auth.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index ff1bb96..31b7e17 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -53,13 +53,13 @@ export class AuthController { res.cookie('accessToken', accessToken, { httpOnly: true, domain: clientDomain, - sameSite: 'lax', + sameSite: 'none', secure: true, }); res.cookie('refreshToken', refreshToken, { httpOnly: true, domain: clientDomain, - sameSite: 'lax', + sameSite: 'none', secure: true, }); res.cookie('isLoggedIn', true, { httpOnly: false, domain: clientDomain }); From 92bdc64739fb9cdaac1703200d9217a1b502128a Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 21:28:01 +0900 Subject: [PATCH 089/236] fix: delete cookie domain distribution --- src/APIs/auth/auth.controller.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 31b7e17..6294f64 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -42,13 +42,7 @@ export class AuthController { }); // 클라이언트 도메인 설정 - const clientHost = req.headers.host; - let clientDomain; - if (clientHost.includes('localhost')) { - clientDomain = 'localhost'; - } else { - clientDomain = process.env.CLIENT_URL; - } + const clientDomain = process.env.CLIENT_URL; res.cookie('accessToken', accessToken, { httpOnly: true, From f00427312b5b972b4c8d7d1ff13a9c6e9c3134c0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 21:32:42 +0900 Subject: [PATCH 090/236] fix: client-domain --- src/APIs/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 6294f64..08697b5 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -42,7 +42,7 @@ export class AuthController { }); // 클라이언트 도메인 설정 - const clientDomain = process.env.CLIENT_URL; + const clientDomain = process.env.CLIENT_DOMAIN; res.cookie('accessToken', accessToken, { httpOnly: true, From 3ded2222e244771ba204cdcf459544e706dff84b Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 21:57:38 +0900 Subject: [PATCH 091/236] feat: change category detail fetching API for public --- .../PostCategories.controller.ts | 73 +++++++++---------- .../postCategories/PostCategories.service.ts | 6 +- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/postCategories/PostCategories.controller.ts index 34ef062..01daf82 100644 --- a/src/APIs/postCategories/PostCategories.controller.ts +++ b/src/APIs/postCategories/PostCategories.controller.ts @@ -41,7 +41,7 @@ export class PostCategoriesController { @ApiOkResponse({ type: [FetchPostCategoriesDto], }) - @Get(':userId/categories/list') + @Get(':userId/categories') @HttpCode(200) async fetchPostCategories( @Req() req: Request, @@ -54,6 +54,19 @@ export class PostCategoriesController { }); } + @ApiOperation({ + summary: '특정 카테고리 조회', + description: 'id에 해당하는 카테고리를 조회한다.', + }) + @ApiOkResponse({ type: FetchPostCategoryDto }) + @Get('categories/:categoryId') + async fetchMyCategory( + @Req() req: Request, + @Param('categoryId') id: string, + ): Promise { + return await this.postCategoriesService.findWithId({ id }); + } + @ApiOperation({ summary: '게시글 카테고리 생성', description: '로그인된 유저와 연결된 카테고리를 생성한다.', @@ -75,43 +88,27 @@ export class PostCategoriesController { return await this.postCategoriesService.create({ kakaoId, name }); } - @ApiOperation({ - summary: '로그인된 유저의 카테고리 전체 조회', - description: - '로그인된 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', - }) - @ApiCookieAuth() - @ApiOkResponse({ - type: [FetchPostCategoriesDto], - }) - @UseGuards(AuthGuardV2) - @Get('me/categories') - async fetchMyCategories( - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - console.log('kakaoId: ', kakaoId); - return await this.postCategoriesService.fetchAll({ - kakaoId, - targetKakaoId: kakaoId, - }); - } - - @ApiOperation({ - summary: '로그인된 유저의 특정 카테고리 조회', - description: '로그인된 유저가 생성한, id에 해당하는 카테고리를 조회한다.', - }) - @ApiCookieAuth() - @ApiOkResponse({ type: FetchPostCategoryDto }) - @UseGuards(AuthGuardV2) - @Get('me/categories/:categoryId') - async fetchMyCategory( - @Req() req: Request, - @Param('categoryId') id: string, - ): Promise { - const kakaoId = req.user.userId; - return await this.postCategoriesService.findWithId({ kakaoId, id }); - } + // @ApiOperation({ + // summary: '로그인된 유저의 카테고리 전체 조회', + // description: + // '로그인된 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', + // }) + // @ApiCookieAuth() + // @ApiOkResponse({ + // type: [FetchPostCategoriesDto], + // }) + // @UseGuards(AuthGuardV2) + // @Get('me/categories') + // async fetchMyCategories( + // @Req() req: Request, + // ): Promise { + // const kakaoId = req.user.userId; + // console.log('kakaoId: ', kakaoId); + // return await this.postCategoriesService.fetchAll({ + // kakaoId, + // targetKakaoId: kakaoId, + // }); + // } @ApiOperation({ summary: '로그인된 유저의 특정 카테고리 수정' }) @ApiCookieAuth() diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/postCategories/PostCategories.service.ts index fc493e7..27aee71 100644 --- a/src/APIs/postCategories/PostCategories.service.ts +++ b/src/APIs/postCategories/PostCategories.service.ts @@ -38,7 +38,7 @@ export class PostCategoriesService { } async patch({ kakaoId, id, name }): Promise { - const data = await this.findWithId({ kakaoId, id }); + const data = await this.findWithId({ id }); if (!data) throw new NotFoundException('카테고리를 찾을 수 없습니다.'); if (data.userKakaoId != kakaoId) throw new ForbiddenException('카테고리를 수정할 권한이 없습니다.'); @@ -46,9 +46,9 @@ export class PostCategoriesService { return await this.postCategoriesRepository.save(data); } - async findWithId({ kakaoId, id }): Promise { + async findWithId({ id }): Promise { return await this.postCategoriesRepository.findOne({ - where: { user: { kakaoId }, id }, + where: { id }, }); } From edb79935842605c68d69fdc4cc232a8cbfde11a8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 22:19:03 +0900 Subject: [PATCH 092/236] feat: follow exist check --- src/APIs/follows/follows.controller.ts | 43 +++++++++++++++++++++++--- src/APIs/likes/likes.controller.ts | 9 +++--- src/APIs/likes/likes.service.ts | 6 +--- src/APIs/users/users.service.ts | 1 + 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index 0354a04..5eba200 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -26,7 +26,7 @@ import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FollowsService } from './follows.service'; @ApiTags('유저 API') -@Controller('users/:userId') +@Controller('users') export class FollowsController { constructor(private readonly followsService: FollowsService) {} @@ -38,7 +38,7 @@ export class FollowsController { @ApiCreatedResponse({ description: '이웃 추가 성공', type: FollowUserDto }) @ApiConflictResponse({ description: '이미 팔로우한 상태이다.' }) @UseGuards(AuthGuardV2) - @Post('follow') + @Post(':userId/follow') @HttpCode(201) async followUser( @Req() req: Request, @@ -59,7 +59,7 @@ export class FollowsController { @ApiNoContentResponse({ description: '언팔로우 성공' }) @ApiNotFoundResponse({ description: '존재하지 않는 이웃 정보이다.' }) @UseGuards(AuthGuardV2) - @Delete('follow') + @Delete(':userId/follow') @HttpCode(204) unfollowUser(@Req() req: Request, @Param('userId') to_user: number) { const kakaoId = parseInt(req.user.userId); @@ -69,6 +69,39 @@ export class FollowsController { }); } + @ApiOperation({ + summary: '팔로워 유무 조회', + description: '나와 팔로우되었는지 유무 체크를 한다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: Boolean }) + @UseGuards(AuthGuardV2) + @HttpCode(200) + @Get('me/follower/:userId') + async checkFollower( + @Req() req: Request, + @Param('userId') to_user: number, + ): Promise { + const from_user = req.user.userId; + return await this.followsService.isExist({ from_user, to_user }); + } + + @ApiOperation({ + summary: '팔로잉 유무 조회', + description: '나의 팔로잉인지 유무 체크를 한다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: Boolean }) + @UseGuards(AuthGuardV2) + @HttpCode(200) + @Get('me/following/:userId') + async checkFollowing( + @Req() req: Request, + @Param('userId') from_user: number, + ): Promise { + const to_user = req.user.userId; + return await this.followsService.isExist({ from_user, to_user }); + } @ApiOperation({ summary: '팔로워 목록 조회', description: 'userId의 팔로워 목록을 조회한다.', @@ -78,7 +111,7 @@ export class FollowsController { type: [FromUserResponseDto], }) @HttpCode(200) - @Get('followers') + @Get(':userId/followers') getFollowers( @Param('userId') kakaoId: number, ): Promise { @@ -94,7 +127,7 @@ export class FollowsController { type: [ToUserResponseDto], }) @HttpCode(200) - @Get('following') + @Get(':userId/followings') getFollows(@Param('userId') kakaoId: number): Promise { return this.followsService.getFollows({ kakaoId }); } diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index ae2cda4..f5dd98a 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -19,14 +19,13 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { FetchLikeDto } from './dtos/toggle-like-response.dto'; import { Likes } from './entities/like.entity'; import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; @ApiTags('게시글 API') -@Controller('posts/:postId/like') +@Controller('posts/:postId') export class LikesController { constructor(private readonly likesService: LikesService) {} @@ -42,7 +41,7 @@ export class LikesController { @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) @UseGuards(AuthGuardV2) @HttpCode(201) - @Post() + @Post('like') async like( @Param('postId') id: number, @Req() req: Request, @@ -62,7 +61,7 @@ export class LikesController { @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) @UseGuards(AuthGuardV2) @HttpCode(204) - @Delete() + @Delete('like') async deleteLike( @Param('postId') id: number, @Req() req: Request, @@ -79,7 +78,7 @@ export class LikesController { @ApiCookieAuth() @ApiOkResponse({ type: Boolean }) @UseGuards(AuthGuardV2) - @Get() + @Get('like') async fetchIfLiked( @Param('postId') id: number, @Req() req: Request, diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 4dc3cba..068d340 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -7,13 +7,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Posts } from '../posts/entities/posts.entity'; -import { - FetchLikeDto, - ToggleLikeResponseDto, -} from './dtos/toggle-like-response.dto'; +import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { FetchLikeResponseDto, FetchLikesDto } from './dtos/fetch-likes.dto'; import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; -import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; @Injectable() export class LikesService { diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index b8b162b..bda92ab 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -32,6 +32,7 @@ export class UsersService { return this.usersRepository.find(); } // =========================== + async adminCheck({ kakaoId }) { const user = await this.findUserByKakaoId({ kakaoId, From b8cd10f4a528c63ccea18e77736de54fa8f735c4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 18 May 2024 22:38:43 +0900 Subject: [PATCH 093/236] fix: update user handle --- src/APIs/follows/follows.service.ts | 1 + src/APIs/posts/posts.repository.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index acd254d..c26240f 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -52,6 +52,7 @@ export class FollowsService { } return true; } + async followUser({ from_user, to_user }): Promise { const isExist = await this.isExist({ from_user, to_user }); if (isExist) { diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index afbe906..01b2137 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -93,6 +93,7 @@ export class PostsRepository extends Repository { .leftJoinAndSelect('p.postBackground', 'postBackground') .leftJoinAndSelect('p.postCategory', 'postCategory') .addSelect([ + 'user.handle', 'user.kakaoId', 'user.description', 'user.profile_image', From de8c8b565df81b3087a4145da0718c1d4c2bf09e Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 03:57:16 +0900 Subject: [PATCH 094/236] feat: many things --- .../follows/dtos/from-user-response.dto.ts | 13 ---- src/APIs/follows/dtos/to-user-response.dto.ts | 13 ---- src/APIs/follows/follows.controller.ts | 13 ++-- src/APIs/follows/follows.module.ts | 3 +- src/APIs/follows/follows.repository.ts | 71 +++++++++++++++++++ src/APIs/follows/follows.service.ts | 41 +++-------- src/APIs/likes/dtos/fetch-likes.dto.ts | 2 +- src/APIs/likes/likes.controller.ts | 16 +++-- src/APIs/likes/likes.module.ts | 3 +- src/APIs/likes/likes.repository.ts | 56 +++++++++++++++ src/APIs/likes/likes.service.ts | 22 +++--- src/APIs/reports/reports.controller.ts | 3 +- src/APIs/users/dtos/user-response.dto.ts | 7 +- src/APIs/users/users.controller.ts | 16 +++-- src/APIs/users/users.module.ts | 3 +- src/APIs/users/users.repository.ts | 65 +++++++++++++++++ src/APIs/users/users.service.ts | 26 +++---- 17 files changed, 268 insertions(+), 105 deletions(-) delete mode 100644 src/APIs/follows/dtos/from-user-response.dto.ts delete mode 100644 src/APIs/follows/dtos/to-user-response.dto.ts create mode 100644 src/APIs/follows/follows.repository.ts create mode 100644 src/APIs/likes/likes.repository.ts create mode 100644 src/APIs/users/users.repository.ts diff --git a/src/APIs/follows/dtos/from-user-response.dto.ts b/src/APIs/follows/dtos/from-user-response.dto.ts deleted file mode 100644 index 530fbdb..0000000 --- a/src/APIs/follows/dtos/from-user-response.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Follow } from '../entities/follow.entity'; - -export class FromUserResponseDto extends OmitType(Follow, [ - 'from_user', - 'to_user', -]) { - @ApiProperty({ - type: UserResponseDto, - }) - from_user: UserResponseDto; -} diff --git a/src/APIs/follows/dtos/to-user-response.dto.ts b/src/APIs/follows/dtos/to-user-response.dto.ts deleted file mode 100644 index 10f1b2b..0000000 --- a/src/APIs/follows/dtos/to-user-response.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Follow } from '../entities/follow.entity'; - -export class ToUserResponseDto extends OmitType(Follow, [ - 'from_user', - 'to_user', -]) { - @ApiProperty({ - type: UserResponseDto, - }) - to_user: UserResponseDto; -} diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index 5eba200..cd47015 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -19,11 +19,10 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { FromUserResponseDto } from './dtos/from-user-response.dto'; -import { ToUserResponseDto } from './dtos/to-user-response.dto'; import { FollowUserDto } from './dtos/follow-user.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FollowsService } from './follows.service'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @ApiTags('유저 API') @Controller('users') @@ -108,13 +107,13 @@ export class FollowsController { }) @ApiOkResponse({ description: '팔로워 목록 조회 성공', - type: [FromUserResponseDto], + type: [UserResponseDtoWithFollowing], }) @HttpCode(200) @Get(':userId/followers') getFollowers( @Param('userId') kakaoId: number, - ): Promise { + ): Promise { return this.followsService.getFollowers({ kakaoId }); } @@ -124,11 +123,13 @@ export class FollowsController { }) @ApiOkResponse({ description: '팔로잉 목록 조회 성공', - type: [ToUserResponseDto], + type: [UserResponseDtoWithFollowing], }) @HttpCode(200) @Get(':userId/followings') - getFollows(@Param('userId') kakaoId: number): Promise { + getFollows( + @Param('userId') kakaoId: number, + ): Promise { return this.followsService.getFollows({ kakaoId }); } } diff --git a/src/APIs/follows/follows.module.ts b/src/APIs/follows/follows.module.ts index cfbba67..b9c5423 100644 --- a/src/APIs/follows/follows.module.ts +++ b/src/APIs/follows/follows.module.ts @@ -5,10 +5,11 @@ import { User } from '../users/entities/user.entity'; import { FollowsService } from './follows.service'; import { FollowsController } from './follows.controller'; import { Follow } from './entities/follow.entity'; +import { FollowsRepository } from './follows.repository'; @Module({ imports: [UsersModule, TypeOrmModule.forFeature([Follow, User])], - providers: [FollowsService], + providers: [FollowsService, FollowsRepository], controllers: [FollowsController], exports: [FollowsService], }) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts new file mode 100644 index 0000000..cae845b --- /dev/null +++ b/src/APIs/follows/follows.repository.ts @@ -0,0 +1,71 @@ +import { DataSource, Repository } from 'typeorm'; +import { Follow } from './entities/follow.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FollowsRepository extends Repository { + constructor(private dataSource: DataSource) { + super(Follow, dataSource.createEntityManager()); + } + + async getFollowers({ kakaoId }) { + const queryBuilder = this.getFollowQuery({ kakaoId }); + const followings = await queryBuilder + .innerJoin('follow.from_user', 'user') + .andWhere('follow.toUserKakaoId = :kakaoId') + .getRawMany(); + return followings.map((follower) => ({ + username: follower.username, + kakaoId: follower.kakaoId, + handle: follower.handle, + isAdmin: follower.isAdmin === 1, + description: follower.description, + profile_image: follower.profile_image, + background_image: follower.background_image, + date_created: follower.date_created, + date_deleted: follower.date_deleted, + isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 + })); + } + + async getFollowings({ kakaoId }) { + const queryBuilder = this.getFollowQuery({ kakaoId }); + const followings = await queryBuilder + .innerJoin('follow.to_user', 'user') + .andWhere('follow.fromUserKakaoId = :kakaoId') + .getRawMany(); + return followings.map((follower) => ({ + username: follower.username, + kakaoId: follower.kakaoId, + handle: follower.handle, + isAdmin: follower.isAdmin === 1, + description: follower.description, + profile_image: follower.profile_image, + background_image: follower.background_image, + date_created: follower.date_created, + date_deleted: follower.date_deleted, + isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 + })); + } + + getFollowQuery({ kakaoId }) { + const queryBuilder = this.createQueryBuilder('follow') + .where('user.date_deleted IS NULL') + .select([ + 'user.username AS username', + 'user.kakaoId AS kakaoId', + 'user.handle AS handle', + 'user.isAdmin AS isAdmin', + 'user.username AS username', + 'user.description AS description', + 'user.profile_image AS profile_image', + 'user.background_image AS background_image', + 'user.date_created AS date_created', + 'user.date_deleted AS date_deleted', + 'CASE WHEN follow.fromUserKakaoId = :kakaoId THEN true ELSE false END AS isFollowing', + ]) + .setParameters({ kakaoId }); + + return queryBuilder; + } +} diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index c26240f..7cdc265 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -1,18 +1,14 @@ import { ConflictException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; -import { FromUserResponseDto } from './dtos/from-user-response.dto'; -import { ToUserResponseDto } from './dtos/to-user-response.dto'; +import { DataSource } from 'typeorm'; import { FollowUserDto } from './dtos/follow-user.dto'; -import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; import { OpenScope } from 'src/common/enums/open-scope.enum'; -import { Follow } from './entities/follow.entity'; +import { FollowsRepository } from './follows.repository'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @Injectable() export class FollowsService { constructor( - @InjectRepository(Follow) - private readonly followsRepository: Repository, + private readonly followsRepository: FollowsRepository, private readonly dataSource: DataSource, ) {} @@ -79,34 +75,13 @@ export class FollowsService { return this.followsRepository.delete({ from_user, to_user }); } - async getFollows({ kakaoId }): Promise { - const follows = await this.followsRepository.find({ - select: { - from_user: USER_SELECT_OPTION, - to_user: USER_SELECT_OPTION, - }, - where: { - from_user: { kakaoId: kakaoId }, - }, - relations: { - to_user: true, - }, - }); + async getFollows({ kakaoId }): Promise { + const follows = await this.followsRepository.getFollowings({ kakaoId }); return follows; } - async getFollowers({ kakaoId }): Promise { - const follows = await this.followsRepository.find({ - select: { - from_user: USER_SELECT_OPTION, - }, - where: { - to_user: { kakaoId: kakaoId }, - }, - relations: { - from_user: true, - }, - }); + async getFollowers({ kakaoId }): Promise { + const follows = await this.followsRepository.getFollowers({ kakaoId }); return follows; } } diff --git a/src/APIs/likes/dtos/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts index 5e4f3ee..4d04e7a 100644 --- a/src/APIs/likes/dtos/fetch-likes.dto.ts +++ b/src/APIs/likes/dtos/fetch-likes.dto.ts @@ -4,7 +4,7 @@ import { Posts } from 'src/APIs/posts/entities/posts.entity'; export class FetchLikesDto { @ApiProperty({ type: Number, description: 'post_id' }) - id: number; + postsId: number; } export class FetchLikeResponseDto extends PickType(Likes, ['id', 'postsId']) { diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index f5dd98a..491b2b7 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -19,10 +19,9 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { Likes } from './entities/like.entity'; -import { FetchLikesResponseDto } from './dtos/fetch-likes-response.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @ApiTags('게시글 API') @Controller('posts/:postId') @@ -91,10 +90,17 @@ export class LikesController { summary: '좋아요 누른 대상 조회하기', description: '게시글에 좋아요를 누른 사람들을 확인한다.', }) - @ApiOkResponse({ description: '조회 성공', type: [FetchLikesResponseDto] }) + @ApiOkResponse({ + description: '조회 성공', + type: [UserResponseDtoWithFollowing], + }) @HttpCode(200) @Get('like-users') - async fetchLikes(@Param('postId') id: number): Promise { - return await this.likesService.fetchLikes({ id }); + async fetchLikes( + @Param('postId') postsId: number, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.userId; + return await this.likesService.fetchLikes({ postsId, kakaoId }); } } diff --git a/src/APIs/likes/likes.module.ts b/src/APIs/likes/likes.module.ts index 966089d..512943a 100644 --- a/src/APIs/likes/likes.module.ts +++ b/src/APIs/likes/likes.module.ts @@ -4,10 +4,11 @@ import { Posts } from '../posts/entities/posts.entity'; import { LikesController } from './likes.controller'; import { LikesService } from './likes.service'; import { Likes } from './entities/like.entity'; +import { LikesRepository } from './likes.repository'; @Module({ imports: [TypeOrmModule.forFeature([Posts, Likes])], - providers: [LikesService], + providers: [LikesService, LikesRepository], controllers: [LikesController], }) export class LikesModule {} diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts new file mode 100644 index 0000000..6d48f68 --- /dev/null +++ b/src/APIs/likes/likes.repository.ts @@ -0,0 +1,56 @@ +import { DataSource, Repository } from 'typeorm'; +import { Likes } from './entities/like.entity'; +import { Follow } from '../follows/entities/follow.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LikesRepository extends Repository { + constructor(private dataSource: DataSource) { + super(Likes, dataSource.createEntityManager()); + } + async getLikes({ kakaoId, postsId }) { + const users = await this.createQueryBuilder('likes') + .innerJoin('likes.posts', 'posts') + .leftJoin('likes.user', 'user') + .leftJoinAndSelect( + (subQuery) => { + return subQuery + .select('follow.toUserKakaoId', 'toUserKakaoId') + .from(Follow, 'follow') + .where('follow.fromUserKakaoId = :kakaoId'); + }, + 'follow', + 'follow.toUserKakaoId = user.kakaoId', + ) + .where('user.date_deleted IS NULL') + .andWhere('posts.date_deleted IS NULL') + .andWhere('likes.postsId = :postsId') + .select([ + 'user.username AS username', + 'user.kakaoId AS kakaoId', + 'user.handle AS handle', + 'user.isAdmin AS isAdmin', + 'user.description AS description', + 'user.profile_image AS profile_image', + 'user.background_image AS background_image', + 'user.date_created AS date_created', + 'user.date_deleted AS date_deleted', + 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ]) + .setParameters({ postsId, kakaoId }) + .getRawMany(); + + return users.map((user) => ({ + username: user.username, + kakaoId: user.kakaoId, + handle: user.handle, + isAdmin: user.isAdmin === 1, + description: user.description, + profile_image: user.profile_image, + background_image: user.background_image, + date_created: user.date_created, + date_deleted: user.date_deleted, + isFollowing: user.isFollowing === 1, + })); + } +} diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 068d340..d374eee 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -3,19 +3,18 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Posts } from '../posts/entities/posts.entity'; import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; -import { FetchLikeResponseDto, FetchLikesDto } from './dtos/fetch-likes.dto'; -import { USER_SELECT_OPTION } from '../users/dtos/user-response.dto'; +import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; +import { LikesRepository } from './likes.repository'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @Injectable() export class LikesService { constructor( - @InjectRepository(Likes) - private readonly likesRepository: Repository, + private readonly likesRepository: LikesRepository, private readonly dataSource: DataSource, ) {} @@ -129,11 +128,10 @@ export class LikesService { } } - async fetchLikes({ id }: FetchLikesDto): Promise { - return await this.likesRepository.find({ - select: { user: USER_SELECT_OPTION, id: true }, - relations: { user: true }, - where: { posts: { id } }, - }); + async fetchLikes({ + postsId, + kakaoId, + }): Promise { + return await this.likesRepository.getLikes({ postsId, kakaoId }); } } diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 912a33d..fe5c763 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -56,12 +56,11 @@ export class ReportsController { @ApiCookieAuth() @ApiCreatedResponse({ type: FetchReportResponse }) @UseGuards(AuthGuardV2) - @Post('posts/:postId/comments/:commentId/report') + @Post('posts/comments/:commentId/report') @HttpCode(201) async reportComment( @Req() req: Request, @Body() body: CreateReportInput, - @Param('postId') postId: number, @Param('commentId') targetId: number, ): Promise { const userKakaoId = req.user.userId; diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index c12434b..c530770 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -1,4 +1,4 @@ -import { OmitType, PickType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; import { User } from '../entities/user.entity'; export const USER_SELECT_OPTION = { @@ -44,3 +44,8 @@ export class UserResponseDto extends OmitType(User, ['current_refresh_token']) { // @ApiProperty({ description: '삭제된 날짜', type: Date }) // date_deleted: Date; } + +export class UserResponseDtoWithFollowing extends UserResponseDto { + @ApiProperty({ type: Boolean, description: '팔로잉 유무' }) + isFollowing: boolean; +} diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 7da48df..ba2e4a6 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -22,7 +22,10 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { UserResponseDto } from './dtos/user-response.dto'; +import { + UserResponseDto, + UserResponseDtoWithFollowing, +} from './dtos/user-response.dto'; import { PatchUserInput } from './dtos/patch-user.input'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; @@ -49,13 +52,18 @@ export class UsersController { summary: '이름이 포함된 유저 검색', description: '이름에 username이 포함된 유저를 검색한다.', }) - @ApiOkResponse({ description: '조회 성공', type: [UserResponseDto] }) + @ApiOkResponse({ + description: '조회 성공', + type: [UserResponseDtoWithFollowing], + }) @HttpCode(200) @Get('username/:username') async findUsersByName( + @Req() req: Request, @Param('username') username: string, - ): Promise { - return await this.usersService.findUsersByName({ username }); + ): Promise { + const kakaoId = req.user.userId; + return await this.usersService.findUsersByName({ kakaoId, username }); } @ApiOperation({ diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index 102800f..c70996f 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -5,10 +5,11 @@ import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; +import { UsersRepository } from './users.repository'; @Module({ imports: [TypeOrmModule.forFeature([User])], - providers: [UsersService, AwsService, UtilsService], + providers: [UsersService, UsersRepository, AwsService, UtilsService], controllers: [UsersController], exports: [UsersService], }) diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts new file mode 100644 index 0000000..c44d802 --- /dev/null +++ b/src/APIs/users/users.repository.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { User } from './entities/user.entity'; +import { DataSource, Repository } from 'typeorm'; +import { Follow } from '../follows/entities/follow.entity'; + +@Injectable() +export class UsersRepository extends Repository { + constructor(private dataSource: DataSource) { + super(User, dataSource.createEntityManager()); + } + + getFollowQuery({ kakaoId }) { + const queryBuilder = this.createQueryBuilder('user') + .leftJoinAndSelect( + (subQuery) => { + return subQuery + .select('follow.to_user', 'toUserKakaoId') + .from(Follow, 'follow') + .where('follow.fromUserKakaoId = :kakaoId', { kakaoId }); + }, + 'follow', + 'follow.toUserKakaoId = user.kakaoId', + ) + .where('user.date_deleted IS NULL') + .select([ + 'user.username AS username', + 'user.kakaoId AS kakaoId', + 'user.handle AS handle', + 'user.isAdmin AS isAdmin', + 'user.username AS username', + 'user.description AS description', + 'user.profile_image AS profile_image', + 'user.background_image AS background_image', + 'user.date_created AS date_created', + 'user.date_deleted AS date_deleted', + 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ]) + .setParameters({ kakaoId }); + + return queryBuilder; + } + // 팔로잉 유무 포함 조회 + async fetchUsersWithNameAndFollowing({ kakaoId, username }) { + const queryBuilder = this.getFollowQuery({ kakaoId }); + const users = await queryBuilder + .andWhere('LOWER(user.username) LIKE LOWER(:username)', { + username: `%${username}%`, + }) + .setParameters({ username: `%${username}%` }) + .getRawMany(); + + return users.map((user) => ({ + username: user.username, + kakaoId: user.kakaoId, + handle: user.handle, + isAdmin: user.isAdmin === 1, + description: user.description, + profile_image: user.profile_image, + background_image: user.background_image, + date_created: user.date_created, + date_deleted: user.date_deleted, + isFollowing: user.isFollowing === 1, + })); + } +} diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index bda92ab..5f527f6 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -4,25 +4,26 @@ import { Injectable, UnauthorizedException, } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { ILike, Repository } from 'typeorm'; -import { User } from './entities/user.entity'; import { IUsersServiceCreate, IUsersServiceFindUserByHandle, IUsersServiceFindUserByKakaoId, } from './interfaces/users.service.interface'; -import { USER_SELECT_OPTION, UserResponseDto } from './dtos/user-response.dto'; +import { + USER_SELECT_OPTION, + UserResponseDto, + UserResponseDtoWithFollowing, +} from './dtos/user-response.dto'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; +import { UsersRepository } from './users.repository'; @Injectable() export class UsersService { constructor( - @InjectRepository(User) - private readonly usersRepository: Repository, + private readonly usersRepository: UsersRepository, private readonly awsService: AwsService, private readonly utilsService: UtilsService, ) {} @@ -118,12 +119,13 @@ export class UsersService { } } - async findUsersByName({ username }): Promise { - const users = await this.usersRepository.find({ - select: USER_SELECT_OPTION, - where: { - username: ILike(`%${username}%`), - }, + async findUsersByName({ + kakaoId, + username, + }): Promise { + const users = await this.usersRepository.fetchUsersWithNameAndFollowing({ + kakaoId, + username, }); return users; } From 4da2c1c6daba9edd4fa0a9dd0e63f64b12869a45 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 04:16:54 +0900 Subject: [PATCH 095/236] fix: add loggedUser validation on follows repo --- src/APIs/follows/follows.controller.ts | 8 ++++++-- src/APIs/follows/follows.repository.ts | 14 +++++++------- src/APIs/follows/follows.service.ts | 20 ++++++++++++++++---- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index cd47015..52586c2 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -112,9 +112,11 @@ export class FollowsController { @HttpCode(200) @Get(':userId/followers') getFollowers( + @Req() req: Request, @Param('userId') kakaoId: number, ): Promise { - return this.followsService.getFollowers({ kakaoId }); + const loggedUser = req.user.userId; + return this.followsService.getFollowers({ kakaoId, loggedUser }); } @ApiOperation({ @@ -128,8 +130,10 @@ export class FollowsController { @HttpCode(200) @Get(':userId/followings') getFollows( + @Req() req: Request, @Param('userId') kakaoId: number, ): Promise { - return this.followsService.getFollows({ kakaoId }); + const loggedUser = req.user.userId; + return this.followsService.getFollows({ kakaoId, loggedUser }); } } diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index cae845b..8e2f4c8 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -8,8 +8,8 @@ export class FollowsRepository extends Repository { super(Follow, dataSource.createEntityManager()); } - async getFollowers({ kakaoId }) { - const queryBuilder = this.getFollowQuery({ kakaoId }); + async getFollowers({ kakaoId, loggedUser }) { + const queryBuilder = this.getFollowQuery({ kakaoId, loggedUser }); const followings = await queryBuilder .innerJoin('follow.from_user', 'user') .andWhere('follow.toUserKakaoId = :kakaoId') @@ -28,8 +28,8 @@ export class FollowsRepository extends Repository { })); } - async getFollowings({ kakaoId }) { - const queryBuilder = this.getFollowQuery({ kakaoId }); + async getFollowings({ kakaoId, loggedUser }) { + const queryBuilder = this.getFollowQuery({ kakaoId, loggedUser }); const followings = await queryBuilder .innerJoin('follow.to_user', 'user') .andWhere('follow.fromUserKakaoId = :kakaoId') @@ -48,7 +48,7 @@ export class FollowsRepository extends Repository { })); } - getFollowQuery({ kakaoId }) { + getFollowQuery({ kakaoId, loggedUser }) { const queryBuilder = this.createQueryBuilder('follow') .where('user.date_deleted IS NULL') .select([ @@ -62,9 +62,9 @@ export class FollowsRepository extends Repository { 'user.background_image AS background_image', 'user.date_created AS date_created', 'user.date_deleted AS date_deleted', - 'CASE WHEN follow.fromUserKakaoId = :kakaoId THEN true ELSE false END AS isFollowing', + 'CASE WHEN follow.fromUserKakaoId = :loggedUser THEN true ELSE false END AS isFollowing', ]) - .setParameters({ kakaoId }); + .setParameters({ kakaoId, loggedUser }); return queryBuilder; } diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index 7cdc265..7751b6b 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -75,13 +75,25 @@ export class FollowsService { return this.followsRepository.delete({ from_user, to_user }); } - async getFollows({ kakaoId }): Promise { - const follows = await this.followsRepository.getFollowings({ kakaoId }); + async getFollows({ + kakaoId, + loggedUser, + }): Promise { + const follows = await this.followsRepository.getFollowings({ + kakaoId, + loggedUser, + }); return follows; } - async getFollowers({ kakaoId }): Promise { - const follows = await this.followsRepository.getFollowers({ kakaoId }); + async getFollowers({ + kakaoId, + loggedUser, + }): Promise { + const follows = await this.followsRepository.getFollowers({ + kakaoId, + loggedUser, + }); return follows; } } From 1984b531100f00ef39ed68ad233ffd63ff987fa3 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 05:07:22 +0900 Subject: [PATCH 096/236] feat: add follow_count on user table --- src/APIs/follows/follows.repository.ts | 3 +++ src/APIs/likes/likes.repository.ts | 2 ++ src/APIs/users/dtos/user-response.dto.ts | 1 + src/APIs/users/entities/user.entity.ts | 9 +++++++++ src/APIs/users/users.repository.ts | 2 ++ 5 files changed, 17 insertions(+) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 8e2f4c8..dccae66 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -18,6 +18,7 @@ export class FollowsRepository extends Repository { username: follower.username, kakaoId: follower.kakaoId, handle: follower.handle, + follow_count: follower.follow_count, isAdmin: follower.isAdmin === 1, description: follower.description, profile_image: follower.profile_image, @@ -38,6 +39,7 @@ export class FollowsRepository extends Repository { username: follower.username, kakaoId: follower.kakaoId, handle: follower.handle, + follow_count: follower.follow_count, isAdmin: follower.isAdmin === 1, description: follower.description, profile_image: follower.profile_image, @@ -55,6 +57,7 @@ export class FollowsRepository extends Repository { 'user.username AS username', 'user.kakaoId AS kakaoId', 'user.handle AS handle', + 'user.follow_count AS follow_count', 'user.isAdmin AS isAdmin', 'user.username AS username', 'user.description AS description', diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index 6d48f68..4cce195 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -29,6 +29,7 @@ export class LikesRepository extends Repository { 'user.username AS username', 'user.kakaoId AS kakaoId', 'user.handle AS handle', + 'user.follow_count AS follow_count', 'user.isAdmin AS isAdmin', 'user.description AS description', 'user.profile_image AS profile_image', @@ -44,6 +45,7 @@ export class LikesRepository extends Repository { username: user.username, kakaoId: user.kakaoId, handle: user.handle, + follow_count: user.follow_count, isAdmin: user.isAdmin === 1, description: user.description, profile_image: user.profile_image, diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index c530770..eb879c7 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -6,6 +6,7 @@ export const USER_SELECT_OPTION = { handle: true, isAdmin: true, username: true, + follow_count: true, description: true, profile_image: true, background_image: true, diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 841582c..2721c18 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -26,6 +26,15 @@ export class User { @ApiProperty({ description: '어드민 유저 여부', type: Boolean }) isAdmin: boolean; + @Column({ default: 0 }) + @ApiProperty({ + description: '팔로우 수', + type: Number, + required: false, + default: 0, + }) + follow_count: number; + @Column({ unique: true }) @ApiProperty({ description: '유저 이름', type: String }) username: string; diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index c44d802..f6b724b 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -28,6 +28,7 @@ export class UsersRepository extends Repository { 'user.handle AS handle', 'user.isAdmin AS isAdmin', 'user.username AS username', + 'user.follow_count AS follow_count', 'user.description AS description', 'user.profile_image AS profile_image', 'user.background_image AS background_image', @@ -53,6 +54,7 @@ export class UsersRepository extends Repository { username: user.username, kakaoId: user.kakaoId, handle: user.handle, + follow_count: user.follow_count, isAdmin: user.isAdmin === 1, description: user.description, profile_image: user.profile_image, From ec0ae3b642af3655b67668f5571070564271741f Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 20:39:07 +0900 Subject: [PATCH 097/236] feat: add follow_count transaction logic --- src/APIs/follows/follows.repository.ts | 9 ++- src/APIs/follows/follows.service.ts | 88 +++++++++++++++++++----- src/APIs/likes/likes.repository.ts | 6 +- src/APIs/users/dtos/user-response.dto.ts | 3 +- src/APIs/users/entities/user.entity.ts | 13 +++- src/APIs/users/users.repository.ts | 6 +- 6 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index dccae66..a32d2e6 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -18,7 +18,8 @@ export class FollowsRepository extends Repository { username: follower.username, kakaoId: follower.kakaoId, handle: follower.handle, - follow_count: follower.follow_count, + follower_count: follower.follower_count, + following_count: follower.following_count, isAdmin: follower.isAdmin === 1, description: follower.description, profile_image: follower.profile_image, @@ -39,7 +40,8 @@ export class FollowsRepository extends Repository { username: follower.username, kakaoId: follower.kakaoId, handle: follower.handle, - follow_count: follower.follow_count, + follower_count: follower.follower_count, + following_count: follower.following_count, isAdmin: follower.isAdmin === 1, description: follower.description, profile_image: follower.profile_image, @@ -57,7 +59,8 @@ export class FollowsRepository extends Repository { 'user.username AS username', 'user.kakaoId AS kakaoId', 'user.handle AS handle', - 'user.follow_count AS follow_count', + 'user.follower_count AS follower_count', + 'user.following_count AS following_count', 'user.isAdmin AS isAdmin', 'user.username AS username', 'user.description AS description', diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index 7751b6b..d8ef49d 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -4,6 +4,7 @@ import { FollowUserDto } from './dtos/follow-user.dto'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { FollowsRepository } from './follows.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { User } from '../users/entities/user.entity'; @Injectable() export class FollowsService { @@ -50,29 +51,80 @@ export class FollowsService { } async followUser({ from_user, to_user }): Promise { - const isExist = await this.isExist({ from_user, to_user }); - if (isExist) { - throw new ConflictException('already exists'); - } - if (this.isSame({ from_user, to_user })) { - throw new ConflictException('you cannot follow yourself!'); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const toUserData = await queryRunner.manager.findOne(User, { + where: { kakaoId: to_user }, + }); + const fromUserData = await queryRunner.manager.findOne(User, { + where: { kakaoId: from_user }, + }); + + const isExist = await this.isExist({ from_user, to_user }); + if (isExist) { + throw new ConflictException('already exists'); + } + if (this.isSame({ from_user, to_user })) { + throw new ConflictException('you cannot follow yourself!'); + } + const follow = await this.followsRepository.save({ + from_user, + to_user, + }); + await queryRunner.manager.update(User, fromUserData.kakaoId, { + following_count: () => 'following_count +1', + }); + await queryRunner.manager.update(User, toUserData.kakaoId, { + follower_count: () => 'follower_count +1', + }); + await queryRunner.commitTransaction(); + return follow; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); } - const follow = await this.followsRepository.save({ - from_user, - to_user, - }); - return follow; } async unfollowUser({ from_user, to_user }) { - const isExist = await this.isExist({ from_user, to_user }); - if (!isExist) { - throw new ConflictException('no data exists'); - } - if (this.isSame({ from_user, to_user })) { - throw new ConflictException('you cannot unfollow yourself!'); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const toUserData = await queryRunner.manager.findOne(User, { + where: { kakaoId: to_user }, + }); + const fromUserData = await queryRunner.manager.findOne(User, { + where: { kakaoId: from_user }, + }); + + const isExist = await this.isExist({ from_user, to_user }); + + if (!isExist) { + throw new ConflictException('no data exists'); + } + + if (this.isSame({ from_user, to_user })) { + throw new ConflictException('you cannot unfollow yourself!'); + } + + await queryRunner.manager.update(User, fromUserData.kakaoId, { + following_count: () => 'following_count -1', + }); + await queryRunner.manager.update(User, toUserData.kakaoId, { + follower_count: () => 'follower_count -1', + }); + await queryRunner.commitTransaction(); + return this.followsRepository.delete({ from_user, to_user }); + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); } - return this.followsRepository.delete({ from_user, to_user }); } async getFollows({ diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index 4cce195..fd26b38 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -29,7 +29,8 @@ export class LikesRepository extends Repository { 'user.username AS username', 'user.kakaoId AS kakaoId', 'user.handle AS handle', - 'user.follow_count AS follow_count', + 'user.follower_count AS follower_count', + 'user.following_count AS following_count', 'user.isAdmin AS isAdmin', 'user.description AS description', 'user.profile_image AS profile_image', @@ -45,7 +46,8 @@ export class LikesRepository extends Repository { username: user.username, kakaoId: user.kakaoId, handle: user.handle, - follow_count: user.follow_count, + follower_count: user.follower_count, + following_count: user.following_count, isAdmin: user.isAdmin === 1, description: user.description, profile_image: user.profile_image, diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index eb879c7..04ce75d 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -6,7 +6,8 @@ export const USER_SELECT_OPTION = { handle: true, isAdmin: true, username: true, - follow_count: true, + follower_count: true, + following_count: true, description: true, profile_image: true, background_image: true, diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 2721c18..081d96a 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -28,12 +28,21 @@ export class User { @Column({ default: 0 }) @ApiProperty({ - description: '팔로우 수', + description: '팔로잉 수', type: Number, required: false, default: 0, }) - follow_count: number; + following_count: number; + + @Column({ default: 0 }) + @ApiProperty({ + description: '팔로워 수', + type: Number, + required: false, + default: 0, + }) + follower_count: number; @Column({ unique: true }) @ApiProperty({ description: '유저 이름', type: String }) diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index f6b724b..1e8dcd0 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -28,7 +28,8 @@ export class UsersRepository extends Repository { 'user.handle AS handle', 'user.isAdmin AS isAdmin', 'user.username AS username', - 'user.follow_count AS follow_count', + 'user.following_count AS following_count', + 'user.follower_count AS follower_count', 'user.description AS description', 'user.profile_image AS profile_image', 'user.background_image AS background_image', @@ -54,7 +55,8 @@ export class UsersRepository extends Repository { username: user.username, kakaoId: user.kakaoId, handle: user.handle, - follow_count: user.follow_count, + following_count: user.following_count, + follower_count: user.follower_count, isAdmin: user.isAdmin === 1, description: user.description, profile_image: user.profile_image, From cd2111f76d79b65aac70b8ade6c65c89c4d04be7 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 21:01:17 +0900 Subject: [PATCH 098/236] fix: accuracy of follow list isFollowing --- src/APIs/follows/follows.repository.ts | 85 ++++++++++++++++++-------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index a32d2e6..0f07d2c 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -9,11 +9,38 @@ export class FollowsRepository extends Repository { } async getFollowers({ kakaoId, loggedUser }) { - const queryBuilder = this.getFollowQuery({ kakaoId, loggedUser }); - const followings = await queryBuilder + const followings = await this.createQueryBuilder('follow') .innerJoin('follow.from_user', 'user') + .where('user.date_deleted IS NULL') .andWhere('follow.toUserKakaoId = :kakaoId') + .leftJoinAndSelect( + (subQuery) => { + return subQuery + .select('follow2.toUserKakaoId', 'toUserKakaoId') + .from(Follow, 'follow2') + .where('follow2.fromUserKakaoId = :loggedUser'); + }, + 'follow2', + 'follow2.toUserKakaoId = user.kakaoId', + ) + .select([ + 'user.username AS username', + 'user.kakaoId AS kakaoId', + 'user.handle AS handle', + 'user.follower_count AS follower_count', + 'user.following_count AS following_count', + 'user.isAdmin AS isAdmin', + 'user.username AS username', + 'user.description AS description', + 'user.profile_image AS profile_image', + 'user.background_image AS background_image', + 'user.date_created AS date_created', + 'user.date_deleted AS date_deleted', + 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ]) + .setParameters({ kakaoId, loggedUser }) .getRawMany(); + return followings.map((follower) => ({ username: follower.username, kakaoId: follower.kakaoId, @@ -31,30 +58,20 @@ export class FollowsRepository extends Repository { } async getFollowings({ kakaoId, loggedUser }) { - const queryBuilder = this.getFollowQuery({ kakaoId, loggedUser }); - const followings = await queryBuilder + const followings = await this.createQueryBuilder('follow') .innerJoin('follow.to_user', 'user') - .andWhere('follow.fromUserKakaoId = :kakaoId') - .getRawMany(); - return followings.map((follower) => ({ - username: follower.username, - kakaoId: follower.kakaoId, - handle: follower.handle, - follower_count: follower.follower_count, - following_count: follower.following_count, - isAdmin: follower.isAdmin === 1, - description: follower.description, - profile_image: follower.profile_image, - background_image: follower.background_image, - date_created: follower.date_created, - date_deleted: follower.date_deleted, - isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 - })); - } - - getFollowQuery({ kakaoId, loggedUser }) { - const queryBuilder = this.createQueryBuilder('follow') .where('user.date_deleted IS NULL') + .andWhere('follow.fromUserKakaoId = :kakaoId') + .leftJoinAndSelect( + (subQuery) => { + return subQuery + .select('follow2.toUserKakaoId', 'toUserKakaoId') + .from(Follow, 'follow2') + .where('follow2.fromUserKakaoId = :loggedUser'); + }, + 'follow2', + 'follow2.toUserKakaoId = user.kakaoId', + ) .select([ 'user.username AS username', 'user.kakaoId AS kakaoId', @@ -68,10 +85,24 @@ export class FollowsRepository extends Repository { 'user.background_image AS background_image', 'user.date_created AS date_created', 'user.date_deleted AS date_deleted', - 'CASE WHEN follow.fromUserKakaoId = :loggedUser THEN true ELSE false END AS isFollowing', + 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ kakaoId, loggedUser }); + .setParameters({ kakaoId, loggedUser }) + .getRawMany(); - return queryBuilder; + return followings.map((follower) => ({ + username: follower.username, + kakaoId: follower.kakaoId, + handle: follower.handle, + follower_count: follower.follower_count, + following_count: follower.following_count, + isAdmin: follower.isAdmin === 1, + description: follower.description, + profile_image: follower.profile_image, + background_image: follower.background_image, + date_created: follower.date_created, + date_deleted: follower.date_deleted, + isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 + })); } } From 000ddbb5df1ac563f2c43f7fb44f303f85ecac04 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 19 May 2024 21:34:47 +0900 Subject: [PATCH 099/236] fix: delete targetUser FK on report entity --- src/APIs/reports/dtos/create-report.dto.ts | 1 - src/APIs/reports/dtos/fetch-report.dto.ts | 1 - src/APIs/reports/entities/report.entity.ts | 9 --------- src/APIs/reports/reports.service.ts | 1 - 4 files changed, 12 deletions(-) diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts index d38b2db..875190b 100644 --- a/src/APIs/reports/dtos/create-report.dto.ts +++ b/src/APIs/reports/dtos/create-report.dto.ts @@ -4,7 +4,6 @@ import { Report } from '../entities/report.entity'; export class CreateReportDto extends OmitType(Report, [ 'id', 'user', - 'targetUser', 'post', 'postId', 'comment', diff --git a/src/APIs/reports/dtos/fetch-report.dto.ts b/src/APIs/reports/dtos/fetch-report.dto.ts index b6b28e1..fb05786 100644 --- a/src/APIs/reports/dtos/fetch-report.dto.ts +++ b/src/APIs/reports/dtos/fetch-report.dto.ts @@ -3,7 +3,6 @@ import { Report } from '../entities/report.entity'; export class FetchReportResponse extends OmitType(Report, [ 'user', - 'targetUser', 'post', 'comment', ]) {} diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index eaaed57..0f9a027 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -38,15 +38,6 @@ export class Report { @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; - @ApiProperty({ type: Number, description: '신고당한 유저 id' }) - @Column() - @RelationId((report: Report) => report.targetUser) - targetUserKakaoId; - - @JoinColumn() - @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) - targetUser: User; - @IsEnum(ReportType) @ApiProperty({ type: 'enum', enum: ReportType, description: '신고 유형' }) @Column() diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index aa6b7d5..e2db30d 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -23,7 +23,6 @@ export class ReportsService { ) {} async create(dto: CreateReportDto): Promise { - await this.usersService.existCheck({ kakaoId: dto.targetUserKakaoId }); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); From 2a6767f8becca70ec15b74b2e2596b2a4942bb0b Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 20 May 2024 11:04:33 +0900 Subject: [PATCH 100/236] refactor: update Agreements interface --- src/APIs/agreements/agreements.service.ts | 17 ++++++++++++----- .../interfaces/agreements.service.interface.ts | 13 +++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index 63a56a8..eec77f0 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -6,6 +6,9 @@ import { import { AgreementsRepository } from './agreements.repository'; import { IAgreementsServiceCreate, + IAgreementsServiceFetchContract, + IAgreementsServiceId, + IAgreementsServiceKakaoId, IAgreementsServicePatch, } from './interfaces/agreements.service.interface'; import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; @@ -20,7 +23,7 @@ export class AgreementsService { private readonly usersService: UsersService, ) {} - async adminCheck({ kakaoId }) { + async adminCheck({ kakaoId }: IAgreementsServiceKakaoId): Promise { await this.usersService.adminCheck({ kakaoId }); } @@ -36,7 +39,7 @@ export class AgreementsService { }); } - async fetchContract({ agreementType }) { + async fetchContract({ agreementType }: IAgreementsServiceFetchContract) { const fileName = agreementType + '.txt'; const rootPath = process.cwd(); const filePath = path.join(rootPath, 'src', 'assets', 'terms', fileName); @@ -44,12 +47,16 @@ export class AgreementsService { return data; } - async fetchOne({ id }): Promise { + async fetchOne({ id }: IAgreementsServiceId): Promise { return await this.agreementsRepository.findOne({ where: { id } }); } - async fetchAll({ kakaoId }): Promise { - return await this.agreementsRepository.find({ where: { user: kakaoId } }); + async fetchAll({ + kakaoId, + }: IAgreementsServiceKakaoId): Promise { + return await this.agreementsRepository.find({ + where: { user: { kakaoId } }, + }); } async patch({ diff --git a/src/APIs/agreements/interfaces/agreements.service.interface.ts b/src/APIs/agreements/interfaces/agreements.service.interface.ts index b833c99..6f170e4 100644 --- a/src/APIs/agreements/interfaces/agreements.service.interface.ts +++ b/src/APIs/agreements/interfaces/agreements.service.interface.ts @@ -1,3 +1,4 @@ +import { AgreementType } from 'src/common/enums/agreement-type.enum'; import { Agreement } from '../entities/agreement.entity'; export interface IAgreementsServiceCreate @@ -15,3 +16,15 @@ export interface IAgreementsServiceCreate export interface IAgreementsServicePatch extends Pick {} + +export interface IAgreementsServiceKakaoId { + kakaoId: number; +} + +export interface IAgreementsServiceId { + id: number; +} + +export interface IAgreementsServiceFetchContract { + agreementType: AgreementType; +} From 13925b434e4a2dd8da59d3c8b813e296ac3f190c Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 20 May 2024 11:15:38 +0900 Subject: [PATCH 101/236] refactor: update Announcement interface --- src/APIs/announcements/announcements.controller.ts | 4 ++-- src/APIs/announcements/announcements.service.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index d1fff16..efec8e2 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -61,8 +61,8 @@ export class AnnouncementsController { @Req() req: Request, @Body() body: PatchAnnouncementInput, @Param('id') id: number, - ) { - const kakaoId = req.body.userId; + ): Promise { + const kakaoId = req.user.userId; return await this.announcementsService.patch({ ...body, id, kakaoId }); } diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index b132d71..a25d76d 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -31,13 +31,19 @@ export class AnnouncementsService { return await this.annoucementsRepository.find(); } - async patch({ kakaoId, id, title, content }: IAnnouncementsSercicePatch) { + async patch({ + kakaoId, + id, + title, + content, + }: IAnnouncementsSercicePatch): Promise { await this.usersService.adminCheck({ kakaoId }); const anmt = await this.annoucementsRepository.findOne({ where: { id } }); if (!anmt) throw new NotFoundException('공지를 찾을 수 없습니다.'); if (title) anmt.title = title; if (content) anmt.content = content; - return await this.annoucementsRepository.save(anmt); + await this.annoucementsRepository.save(anmt); + return await this.annoucementsRepository.find({ where: { id: anmt.id } }); } async remove({ From c8a7be8a6fa1626f25ef64a1e73d0158e4709e2c Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 20 May 2024 11:35:57 +0900 Subject: [PATCH 102/236] refactor: update comment interface --- src/APIs/comments/comments.repository.ts | 14 +++-- src/APIs/comments/comments.service.ts | 51 +++++++++++++------ src/APIs/comments/dtos/create-comment.dto.ts | 2 +- src/APIs/comments/entities/comment.entity.ts | 2 +- .../comments.repository.interface.ts | 9 ++++ .../interfaces/comments.service.interface.ts | 25 +++++++++ 6 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 src/APIs/comments/interfaces/comments.repository.interface.ts create mode 100644 src/APIs/comments/interfaces/comments.service.interface.ts diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 6cd0b88..6912009 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -1,14 +1,20 @@ -import { DataSource, Repository } from 'typeorm'; +import { DataSource, InsertResult, Repository } from 'typeorm'; import { Comment } from './entities/comment.entity'; import { Injectable } from '@nestjs/common'; import { FetchCommentsDto } from './dtos/fetch-comments.dto'; +import { + ICommentsRepositoryInsertComment, + ICommentsRepositoryfetchComments, +} from './interfaces/comments.repository.interface'; @Injectable() export class CommentsRepository extends Repository { constructor(private dataSource: DataSource) { super(Comment, dataSource.createEntityManager()); } - async upsertComment({ createCommentDto }) { + async insertComment({ + createCommentDto, + }: ICommentsRepositoryInsertComment): Promise { console.log(createCommentDto); return await this.createQueryBuilder('c') .insert() @@ -17,7 +23,9 @@ export class CommentsRepository extends Repository { .execute(); } - async fetchComments({ postsId }): Promise { + async fetchComments({ + postsId, + }: ICommentsRepositoryfetchComments): Promise { return await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 8c0fde3..0bff446 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -7,7 +7,7 @@ import { import { CreateCommentDto } from './dtos/create-comment.dto'; import { UsersService } from '../users/users.service'; import { CommentsRepository } from './comments.repository'; -import { DataSource } from 'typeorm'; +import { DataSource, EntityManager } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; import { ChildrenComment, @@ -15,6 +15,14 @@ import { FetchCommentsDto, } from './dtos/fetch-comments.dto'; import { USER_PRIMARY_SELECT_OPTION } from '../users/dtos/user-response.dto'; +import { + ICommentsServiceDelete, + ICommentsServiceFetch, + ICommentsServiceId, + ICommentsServicePatch, + ICommentsServicePostsIdValidCheck, +} from './interfaces/comments.service.interface'; +import { Comment } from './entities/comment.entity'; @Injectable() export class CommentsService { @@ -24,7 +32,10 @@ export class CommentsService { private readonly dataSource: DataSource, ) {} - async postsIdValidCheck({ parentId, postsId }) { + async postsIdValidCheck({ + parentId, + postsId, + }: ICommentsServicePostsIdValidCheck): Promise { const parent = await this.existCheck({ id: parentId }); if (parent.postsId != postsId) throw new BadRequestException( @@ -33,7 +44,8 @@ export class CommentsService { if (parent.parentId) throw new BadRequestException('부모 댓글이 루트 댓글이 아닙니다.'); } - async existCheck({ id }) { + + async existCheck({ id }: ICommentsServiceId): Promise { const comment = await this.commentsRepository.findOne({ where: { id } }); if (!comment) { throw new NotFoundException( @@ -42,6 +54,7 @@ export class CommentsService { } return comment; } + async insert(createCommentDto: CreateCommentDto): Promise { if (createCommentDto.parentId) await this.postsIdValidCheck({ @@ -52,7 +65,7 @@ export class CommentsService { comment_count: () => 'comment_count +1', }); - const commentData = await this.commentsRepository.upsertComment({ + const commentData = await this.commentsRepository.insertComment({ createCommentDto, }); const id = commentData.identifiers[0]; @@ -70,7 +83,7 @@ export class CommentsService { postsId, id, content, - }): Promise { + }: ICommentsServicePatch): Promise { const commentData = await this.existCheck({ id }); if (!commentData) throw new NotFoundException('댓글을 찾을 수 없습니다.'); if (commentData.postsId != postsId) @@ -81,25 +94,31 @@ export class CommentsService { return await this.commentsRepository.save(commentData); } - async fetchComments({ postsId }): Promise { + async fetchComments({ + postsId, + }: ICommentsServiceFetch): Promise { return await this.commentsRepository.fetchComments({ postsId }); } - async delete({ id, userKakaoId, postsId }): Promise { - try { + async delete({ + id, + userKakaoId, + postsId, + }: ICommentsServiceDelete): Promise { + await this.dataSource.transaction(async (manager: EntityManager) => { const data = await this.existCheck({ id }); - if (!(data.postsId == postsId)) + if (data.postsId !== postsId) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); - //transaction 거는 것 고려해볼 것 - const deletedComment = await this.commentsRepository.softRemove({ + } + + await manager.softRemove(Comment, { user: { kakaoId: userKakaoId }, id, }); - await this.dataSource.manager.update(Posts, data.postsId, { - comment_count: () => 'comment_count -1', + + await manager.update(Posts, data.postsId, { + comment_count: () => 'comment_count - 1', }); - } catch (e) { - throw e; - } + }); } } diff --git a/src/APIs/comments/dtos/create-comment.dto.ts b/src/APIs/comments/dtos/create-comment.dto.ts index d7b7f54..4dcbb27 100644 --- a/src/APIs/comments/dtos/create-comment.dto.ts +++ b/src/APIs/comments/dtos/create-comment.dto.ts @@ -20,7 +20,7 @@ export class CreateCommentDto { }) parentId?: number; - userKakaoId: string; + userKakaoId: number; } export class CreateCommentInput extends OmitType(CreateCommentDto, [ diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 9384d3e..cc799c1 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -65,7 +65,7 @@ export class Comment { @ApiProperty({ type: Number, description: '루트 댓글 아이디' }) @Column({ nullable: true }) @RelationId((comment: Comment) => comment.parent) - parentId: Comment; + parentId: number; @ApiProperty({ type: [Comment], description: '자식 댓글 정보' }) @OneToMany(() => Comment, (comment) => comment.parent) diff --git a/src/APIs/comments/interfaces/comments.repository.interface.ts b/src/APIs/comments/interfaces/comments.repository.interface.ts new file mode 100644 index 0000000..708e1a6 --- /dev/null +++ b/src/APIs/comments/interfaces/comments.repository.interface.ts @@ -0,0 +1,9 @@ +import { CreateCommentDto } from '../dtos/create-comment.dto'; + +export interface ICommentsRepositoryInsertComment { + createCommentDto: CreateCommentDto; +} + +export interface ICommentsRepositoryfetchComments { + postsId: number; +} diff --git a/src/APIs/comments/interfaces/comments.service.interface.ts b/src/APIs/comments/interfaces/comments.service.interface.ts new file mode 100644 index 0000000..36245e6 --- /dev/null +++ b/src/APIs/comments/interfaces/comments.service.interface.ts @@ -0,0 +1,25 @@ +export interface ICommentsServicePostsIdValidCheck { + parentId: number; + postsId: number; +} + +export interface ICommentsServiceId { + id: number; +} + +export interface ICommentsServicePatch { + kakaoId: number; + postsId: number; + id: number; + content: string; +} + +export interface ICommentsServiceFetch { + postsId: number; +} + +export interface ICommentsServiceDelete { + id: number; + userKakaoId: number; + postsId: number; +} From 015fc403922bfcf717a2fc5c39494604710828ad Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 20 May 2024 11:37:20 +0900 Subject: [PATCH 103/236] refactor: update feedback interface --- src/APIs/feedbacks/feedbacks.service.ts | 9 +++++++-- .../feedbacks/interfaces/feedbacks.service.interface.ts | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts index 1790410..2564209 100644 --- a/src/APIs/feedbacks/feedbacks.service.ts +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; import { FeedbacksRepository } from './feedbacks.repository'; -import { IFeedbacksServiceCreate } from './interfaces/feedbacks.service.interface'; +import { + IFeedbacksServiceCreate, + IFeedbacksServiceKakaoId, +} from './interfaces/feedbacks.service.interface'; import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; import { UsersService } from '../users/users.service'; @@ -21,7 +24,9 @@ export class FeedbacksService { }); } - async fetchAll({ kakaoId }): Promise { + async fetchAll({ + kakaoId, + }: IFeedbacksServiceKakaoId): Promise { await this.usersService.adminCheck({ kakaoId }); return await this.feedbacksRepository.find(); } diff --git a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts index 433804c..15bec31 100644 --- a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts +++ b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts @@ -3,3 +3,7 @@ import { Feedback } from '../entities/feedback.entity'; export interface IFeedbacksServiceCreate extends Pick { kakaoId: number; } + +export interface IFeedbacksServiceKakaoId { + kakaoId: number; +} From 61912917f3f783473be6367275cecb40b77fa918 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 09:48:33 +0900 Subject: [PATCH 104/236] refactor: update follow interface & followUserDto --- src/APIs/follows/dtos/follow-user.dto.ts | 14 ++---- src/APIs/follows/follows.controller.ts | 4 +- src/APIs/follows/follows.service.ts | 45 +++++++++++++------ .../follows.repository.interface.ts | 0 .../interfaces/follows.service.interface.ts | 9 ++++ 5 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 src/APIs/follows/interfaces/follows.repository.interface.ts create mode 100644 src/APIs/follows/interfaces/follows.service.interface.ts diff --git a/src/APIs/follows/dtos/follow-user.dto.ts b/src/APIs/follows/dtos/follow-user.dto.ts index 9dbe7c6..1bac305 100644 --- a/src/APIs/follows/dtos/follow-user.dto.ts +++ b/src/APIs/follows/dtos/follow-user.dto.ts @@ -1,12 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { OmitType } from '@nestjs/swagger'; +import { Follow } from '../entities/follow.entity'; -export class FollowUserDto { - @ApiProperty({ type: String, description: 'PK: uuid' }) - id: string; - - @ApiProperty({ type: Number, description: 'FK: kakaoId' }) - to_user: string; - - @ApiProperty({ type: Number, description: 'FK: kakaoId' }) - from_user: string; -} +export class FollowUserDto extends OmitType(Follow, ['from_user', 'to_user']) {} diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index 52586c2..3aa15ac 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -30,7 +30,7 @@ export class FollowsController { constructor(private readonly followsService: FollowsService) {} @ApiOperation({ - summary: '이웃 추가하기', + summary: '팔로우 추가하기', description: '로그인된 유저가 userId를 팔로우한다.', }) @ApiCookieAuth() @@ -51,7 +51,7 @@ export class FollowsController { } @ApiOperation({ - summary: '이웃 삭제하기', + summary: '팔로우 삭제하기', description: '로그인된 유저가 userId를 언팔로우 한다.', }) @ApiCookieAuth() diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index d8ef49d..d433412 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -5,6 +5,10 @@ import { OpenScope } from 'src/common/enums/open-scope.enum'; import { FollowsRepository } from './follows.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { User } from '../users/entities/user.entity'; +import { + IFollowsServiceGetList, + IFollowsServiceUsers, +} from './interfaces/follows.service.interface'; @Injectable() export class FollowsService { @@ -13,19 +17,25 @@ export class FollowsService { private readonly dataSource: DataSource, ) {} - isSame({ from_user, to_user }): boolean { + isSame({ from_user, to_user }: IFollowsServiceUsers): boolean { if (from_user == to_user) { return true; } return false; } - async getScope({ from_user, to_user }) { + async getScope({ + from_user, + to_user, + }: IFollowsServiceUsers): Promise { if (from_user === to_user) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; if (from_user !== null && to_user !== null) { const follow = await this.followsRepository.findOne({ - where: { from_user, to_user }, + where: { + from_user: { kakaoId: from_user }, + to_user: { kakaoId: to_user }, + }, }); if (follow) { return [OpenScope.PUBLIC, OpenScope.PROTECTED]; @@ -35,8 +45,10 @@ export class FollowsService { return [OpenScope.PUBLIC]; } - async isExist({ from_user, to_user }): Promise { - console.log(from_user, to_user); + async isExist({ + from_user, + to_user, + }: IFollowsServiceUsers): Promise { const follow = await this.followsRepository.findOne({ where: { from_user: { kakaoId: from_user }, @@ -50,7 +62,10 @@ export class FollowsService { return true; } - async followUser({ from_user, to_user }): Promise { + async followUser({ + from_user, + to_user, + }: IFollowsServiceUsers): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -70,8 +85,8 @@ export class FollowsService { throw new ConflictException('you cannot follow yourself!'); } const follow = await this.followsRepository.save({ - from_user, - to_user, + from_user: { kakaoId: from_user }, + to_user: { kakaoId: to_user }, }); await queryRunner.manager.update(User, fromUserData.kakaoId, { following_count: () => 'following_count +1', @@ -80,7 +95,7 @@ export class FollowsService { follower_count: () => 'follower_count +1', }); await queryRunner.commitTransaction(); - return follow; + return await this.followsRepository.findOne({ where: { id: follow.id } }); } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -89,7 +104,7 @@ export class FollowsService { } } - async unfollowUser({ from_user, to_user }) { + async unfollowUser({ from_user, to_user }: IFollowsServiceUsers) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -117,8 +132,12 @@ export class FollowsService { await queryRunner.manager.update(User, toUserData.kakaoId, { follower_count: () => 'follower_count -1', }); + await this.followsRepository.delete({ + from_user: { kakaoId: from_user }, + to_user: { kakaoId: to_user }, + }); await queryRunner.commitTransaction(); - return this.followsRepository.delete({ from_user, to_user }); + return; } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -130,7 +149,7 @@ export class FollowsService { async getFollows({ kakaoId, loggedUser, - }): Promise { + }: IFollowsServiceGetList): Promise { const follows = await this.followsRepository.getFollowings({ kakaoId, loggedUser, @@ -141,7 +160,7 @@ export class FollowsService { async getFollowers({ kakaoId, loggedUser, - }): Promise { + }: IFollowsServiceGetList): Promise { const follows = await this.followsRepository.getFollowers({ kakaoId, loggedUser, diff --git a/src/APIs/follows/interfaces/follows.repository.interface.ts b/src/APIs/follows/interfaces/follows.repository.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/follows/interfaces/follows.service.interface.ts b/src/APIs/follows/interfaces/follows.service.interface.ts new file mode 100644 index 0000000..b196d67 --- /dev/null +++ b/src/APIs/follows/interfaces/follows.service.interface.ts @@ -0,0 +1,9 @@ +export interface IFollowsServiceUsers { + from_user: number; + to_user: number; +} + +export interface IFollowsServiceGetList { + kakaoId: number; + loggedUser: number; +} From c44a0dcd81421e57832711f0525bd48566286554 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 09:54:27 +0900 Subject: [PATCH 105/236] refactor: update followRepos interface & response type --- src/APIs/follows/follows.controller.ts | 5 ++++- src/APIs/follows/follows.repository.ts | 12 ++++++++++-- src/APIs/follows/follows.service.ts | 5 ++++- .../interfaces/follows.repository.interface.ts | 4 ++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index 3aa15ac..ad2fc33 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -60,7 +60,10 @@ export class FollowsController { @UseGuards(AuthGuardV2) @Delete(':userId/follow') @HttpCode(204) - unfollowUser(@Req() req: Request, @Param('userId') to_user: number) { + unfollowUser( + @Req() req: Request, + @Param('userId') to_user: number, + ): Promise { const kakaoId = parseInt(req.user.userId); return this.followsService.unfollowUser({ from_user: kakaoId, diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 0f07d2c..605cb1e 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -1,6 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; +import { IFollowsRepositoryGetList } from './interfaces/follows.repository.interface'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @Injectable() export class FollowsRepository extends Repository { @@ -8,7 +10,10 @@ export class FollowsRepository extends Repository { super(Follow, dataSource.createEntityManager()); } - async getFollowers({ kakaoId, loggedUser }) { + async getFollowers({ + kakaoId, + loggedUser, + }: IFollowsRepositoryGetList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.from_user', 'user') .where('user.date_deleted IS NULL') @@ -57,7 +62,10 @@ export class FollowsRepository extends Repository { })); } - async getFollowings({ kakaoId, loggedUser }) { + async getFollowings({ + kakaoId, + loggedUser, + }: IFollowsRepositoryGetList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.to_user', 'user') .where('user.date_deleted IS NULL') diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index d433412..aded94b 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -104,7 +104,10 @@ export class FollowsService { } } - async unfollowUser({ from_user, to_user }: IFollowsServiceUsers) { + async unfollowUser({ + from_user, + to_user, + }: IFollowsServiceUsers): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); diff --git a/src/APIs/follows/interfaces/follows.repository.interface.ts b/src/APIs/follows/interfaces/follows.repository.interface.ts index e69de29..dc52108 100644 --- a/src/APIs/follows/interfaces/follows.repository.interface.ts +++ b/src/APIs/follows/interfaces/follows.repository.interface.ts @@ -0,0 +1,4 @@ +export interface IFollowsRepositoryGetList { + kakaoId: number; + loggedUser: number; +} From 98739518d20982d978763ff697a0d3aa9c417bb6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:07:28 +0900 Subject: [PATCH 106/236] refactor: update likes interface & fetchLikeResponseDto rename FetchLikeResponseDto.user => userKakaoId --- src/APIs/likes/dtos/fetch-likes.dto.ts | 2 +- src/APIs/likes/entities/like.entity.ts | 4 ++ .../interfaces/likes.repository.interface.ts | 4 ++ .../interfaces/likes.service.interface.ts | 4 ++ src/APIs/likes/likes.controller.ts | 4 +- src/APIs/likes/likes.repository.ts | 11 +++- src/APIs/likes/likes.service.ts | 65 ++++--------------- 7 files changed, 34 insertions(+), 60 deletions(-) create mode 100644 src/APIs/likes/interfaces/likes.repository.interface.ts create mode 100644 src/APIs/likes/interfaces/likes.service.interface.ts diff --git a/src/APIs/likes/dtos/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts index 4d04e7a..4bab4b4 100644 --- a/src/APIs/likes/dtos/fetch-likes.dto.ts +++ b/src/APIs/likes/dtos/fetch-likes.dto.ts @@ -14,5 +14,5 @@ export class FetchLikeResponseDto extends PickType(Likes, ['id', 'postsId']) { posts: Omit; @ApiProperty({ type: Number }) - user: number; + userKakaoId: number; } diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index 4fd959f..2f0138e 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { + Column, Entity, JoinColumn, ManyToOne, @@ -21,6 +22,8 @@ export class Likes { @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; + @ApiProperty({ description: '유저 아이디', type: Number }) + @Column() @RelationId((like: Likes) => like.user) userKakaoId: number; @@ -40,6 +43,7 @@ export class Likes { type: Number, description: '게시글 아이디', }) + @Column() @RelationId((like: Likes) => like.posts) postsId: number; } diff --git a/src/APIs/likes/interfaces/likes.repository.interface.ts b/src/APIs/likes/interfaces/likes.repository.interface.ts new file mode 100644 index 0000000..fb57c90 --- /dev/null +++ b/src/APIs/likes/interfaces/likes.repository.interface.ts @@ -0,0 +1,4 @@ +export interface ILikesRepositoryIds { + id: number; + kakaoId: number; +} diff --git a/src/APIs/likes/interfaces/likes.service.interface.ts b/src/APIs/likes/interfaces/likes.service.interface.ts new file mode 100644 index 0000000..916c1b1 --- /dev/null +++ b/src/APIs/likes/interfaces/likes.service.interface.ts @@ -0,0 +1,4 @@ +export interface ILikesServiceIds { + id: number; + kakaoId: number; +} diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index 491b2b7..c4d19c7 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -97,10 +97,10 @@ export class LikesController { @HttpCode(200) @Get('like-users') async fetchLikes( - @Param('postId') postsId: number, + @Param('postId') id: number, @Req() req: Request, ): Promise { const kakaoId = req.user.userId; - return await this.likesService.fetchLikes({ postsId, kakaoId }); + return await this.likesService.fetchLikes({ id, kakaoId }); } } diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index fd26b38..77f7f1b 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -2,13 +2,18 @@ import { DataSource, Repository } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Follow } from '../follows/entities/follow.entity'; import { Injectable } from '@nestjs/common'; +import { ILikesRepositoryIds } from './interfaces/likes.repository.interface'; +import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; @Injectable() export class LikesRepository extends Repository { constructor(private dataSource: DataSource) { super(Likes, dataSource.createEntityManager()); } - async getLikes({ kakaoId, postsId }) { + async getLikes({ + kakaoId, + id, + }: ILikesRepositoryIds): Promise { const users = await this.createQueryBuilder('likes') .innerJoin('likes.posts', 'posts') .leftJoin('likes.user', 'user') @@ -24,7 +29,7 @@ export class LikesRepository extends Repository { ) .where('user.date_deleted IS NULL') .andWhere('posts.date_deleted IS NULL') - .andWhere('likes.postsId = :postsId') + .andWhere('likes.postsId = :id') .select([ 'user.username AS username', 'user.kakaoId AS kakaoId', @@ -39,7 +44,7 @@ export class LikesRepository extends Repository { 'user.date_deleted AS date_deleted', 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ postsId, kakaoId }) + .setParameters({ id, kakaoId }) .getRawMany(); return users.map((user) => ({ diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index d374eee..d7f7572 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -1,15 +1,11 @@ -import { - ConflictException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { ConflictException, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Likes } from './entities/like.entity'; import { Posts } from '../posts/entities/posts.entity'; -import { ToggleLikeResponseDto } from './dtos/toggle-like-response.dto'; import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; import { LikesRepository } from './likes.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { ILikesServiceIds } from './interfaces/likes.service.interface'; @Injectable() export class LikesService { @@ -18,7 +14,7 @@ export class LikesService { private readonly dataSource: DataSource, ) {} - async fetchIfLiked({ kakaoId, id }): Promise { + async fetchIfLiked({ kakaoId, id }: ILikesServiceIds): Promise { const alreadyLiked = await this.likesRepository.findOne({ where: { posts: { id }, user: { kakaoId } }, }); @@ -26,7 +22,7 @@ export class LikesService { return false; } - async like({ id, kakaoId }): Promise { + async like({ id, kakaoId }: ILikesServiceIds): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -41,7 +37,7 @@ export class LikesService { throw new ConflictException('이미 좋아요 한 게시글입니다.'); } else { const likeData = await queryRunner.manager.save(Likes, { - user: kakaoId, + userKakaoId: kakaoId, posts: postData, }); await queryRunner.manager.update(Posts, postData.id, { @@ -58,7 +54,7 @@ export class LikesService { } } - async cancel_like({ id, kakaoId }) { + async cancel_like({ id, kakaoId }: ILikesServiceIds): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -72,54 +68,15 @@ export class LikesService { if (!alreadyLiked) { throw new ConflictException('좋아요 내역을 찾을 수 없습니다.'); } else { - const likeData = await queryRunner.manager.delete(Likes, { + await queryRunner.manager.delete(Likes, { id: alreadyLiked.id, }); await queryRunner.manager.update(Posts, postData.id, { like_count: () => 'like_count -1', }); await queryRunner.commitTransaction(); - return likeData; - } - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - } - - async toggleLike({ id, kakaoId }): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - const postData = await queryRunner.manager.findOne(Posts, { - where: { id }, - }); - if (!postData) throw new NotFoundException('게시글이 존재하지 않습니다.'); - // 좋아요 눌렀는지 확인하기 - const alreadyLiked = await this.likesRepository.findOne({ - where: { posts: { id }, user: { kakaoId } }, - }); - if (alreadyLiked) { - await queryRunner.manager.delete(Likes, { id: alreadyLiked.id }); - await queryRunner.manager.update(Posts, postData.id, { - like_count: () => 'like_count -1', - }); - postData.like_count -= 1; - } else { - await queryRunner.manager.save(Likes, { - user: kakaoId, - posts: postData, - }); - await queryRunner.manager.update(Posts, postData.id, { - like_count: () => 'like_count +1', - }); - postData.like_count += 1; + return; } - await queryRunner.commitTransaction(); - return postData; } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -129,9 +86,9 @@ export class LikesService { } async fetchLikes({ - postsId, + id, kakaoId, - }): Promise { - return await this.likesRepository.getLikes({ postsId, kakaoId }); + }: ILikesServiceIds): Promise { + return await this.likesRepository.getLikes({ id, kakaoId }); } } From 370c5c1970bbbee3ab23cf2d4f8a286c96bb0c0e Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:11:12 +0900 Subject: [PATCH 107/236] feat: delete sendNoti API --- .../notifications/notifications.controller.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index f42aee6..5376ad2 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -91,14 +91,14 @@ export class NotificationsController { return await this.notificationsService.toggle({ id, targetUserKakaoId }); } - @ApiOperation({ - summary: 'userId에게 알림 생성', - description: - 'userId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', - }) - @Post('send/:userId') - async sendNoti(@Req() req: Request, @Body() body: EmitNotiInput) { - const userKakaoId = req.user.userId; - return await this.notificationsService.emitAlarm({ userKakaoId, ...body }); - } + // @ApiOperation({ + // summary: 'userId에게 알림 생성', + // description: + // 'userId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', + // }) + // @Post('send/:userId') + // async sendNoti(@Req() req: Request, @Body() body: EmitNotiInput) { + // const userKakaoId = req.user.userId; + // return await this.notificationsService.emitAlarm({ userKakaoId, ...body }); + // } } From 16704ebb4fa3b922fc38a2bd8ac2a87183e5c6b0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:40:09 +0900 Subject: [PATCH 108/236] refactor: update notifications interface --- .../notifications/notifications.controller.ts | 28 +++++-------------- .../notifications/notifications.service.ts | 25 +++++++++++++---- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index 5376ad2..ab5961c 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -1,5 +1,4 @@ import { - Body, Controller, Get, HttpCode, @@ -19,7 +18,6 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { EmitNotiInput } from './dtos/emit-noti.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @@ -45,12 +43,11 @@ export class NotificationsController { @ApiProduces('text/event-stream') @UseGuards(AuthGuardV2) @Sse('subscribe') - sendClientAlarm( - @Req() req: Request, - // @Param('kakaoId') userKakaoId, - ) { - const userKakaoId = req.user.userId; - const sseStream = this.notificationsService.connectUser(userKakaoId); + connectUser(@Req() req: Request) { + const targetUserKakaoId = req.user.userId; + const sseStream = this.notificationsService.connectUser({ + targetUserKakaoId, + }); return sseStream; } @@ -83,22 +80,11 @@ export class NotificationsController { @ApiOkResponse({ type: FetchNotiResponse }) @HttpCode(200) @Post(':id/read') - async toggleNoti( + async readNoti( @Req() req: Request, @Param('id') id: number, ): Promise { const targetUserKakaoId = req.user.userId; - return await this.notificationsService.toggle({ id, targetUserKakaoId }); + return await this.notificationsService.read({ id, targetUserKakaoId }); } - - // @ApiOperation({ - // summary: 'userId에게 알림 생성', - // description: - // 'userId에게 알림을 보낸다. sse로 연결되어 있을 경우 실시간으로 fetch된다.', - // }) - // @Post('send/:userId') - // async sendNoti(@Req() req: Request, @Body() body: EmitNotiInput) { - // const userKakaoId = req.user.userId; - // return await this.notificationsService.emitAlarm({ userKakaoId, ...body }); - // } } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 4f257e3..093c642 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -1,10 +1,14 @@ import { BadRequestException, Injectable, MessageEvent } from '@nestjs/common'; import { NotificationsRepository } from './notifications.repository'; -import { Subject, filter, map } from 'rxjs'; +import { Observable, Subject, filter, map } from 'rxjs'; import { Notification } from './entities/notification.entity'; import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; import { DateOption } from 'src/common/enums/date-option'; +import { + INotificationsServiceConnectUser, + INotificationsServiceRead, +} from './interfaces/notifications.service.interface'; @Injectable() export class NotificationsService { @@ -14,7 +18,9 @@ export class NotificationsService { private notis$: Subject = new Subject(); private observer = this.notis$.asObservable(); - async connectUser(targetUserKakaoId) { + connectUser({ + targetUserKakaoId, + }: INotificationsServiceConnectUser): Observable { console.log('connected: ' + targetUserKakaoId); const pipe = this.observer.pipe( filter((noti) => noti.targetUserKakaoId == targetUserKakaoId), @@ -29,7 +35,7 @@ export class NotificationsService { return pipe; } - async emitAlarm(emitNotiDto: EmitNotiDto) { + async emitAlarm(emitNotiDto: EmitNotiDto): Promise { try { const executeResult = await this.notificationsRepository.createOne(emitNotiDto); @@ -45,7 +51,11 @@ export class NotificationsService { } } - async fetch({ is_checked, kakaoId, date_created }: FetchNotiDto) { + async fetch({ + is_checked, + kakaoId, + date_created, + }: FetchNotiDto): Promise { let currentDate = new Date(); switch (date_created) { @@ -68,11 +78,14 @@ export class NotificationsService { }); } - async toggle({ id, targetUserKakaoId }): Promise { + async read({ + id, + targetUserKakaoId, + }: INotificationsServiceRead): Promise { const updateResult = await this.notificationsRepository.update( { id, targetUserKakaoId }, { - is_checked: () => '!is_checked', + is_checked: true, }, ); if (updateResult.affected < 1) { From 28d0df11540fa985825c06744778470e257a0cbd Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:40:29 +0900 Subject: [PATCH 109/236] refactor: update notifications interface --- .../interfaces/notifications.service.interface.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/APIs/notifications/interfaces/notifications.service.interface.ts diff --git a/src/APIs/notifications/interfaces/notifications.service.interface.ts b/src/APIs/notifications/interfaces/notifications.service.interface.ts new file mode 100644 index 0000000..c546a0b --- /dev/null +++ b/src/APIs/notifications/interfaces/notifications.service.interface.ts @@ -0,0 +1,8 @@ +export interface INotificationsServiceConnectUser { + targetUserKakaoId: number; +} + +export interface INotificationsServiceRead { + id: number; + targetUserKakaoId: number; +} From 05ddc2e12b3cdbfe4a29f5973bd843285470a7ae Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:44:12 +0900 Subject: [PATCH 110/236] feat: add caching dependencies (cache-manager, cache-manager-redis-store, @nestjs/cache-manager) --- package-lock.json | 152 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 2 files changed, 155 insertions(+) diff --git a/package-lock.json b/package-lock.json index be1663f..7e2b900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-s3": "^3.534.0", "@imgly/background-removal-node": "^1.4.5", "@nestjs/axios": "^3.0.2", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", @@ -24,6 +25,8 @@ "@types/multer": "^1.4.11", "axios": "^1.6.7", "bcrypt": "^5.1.1", + "cache-manager": "^5.5.3", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", @@ -2593,6 +2596,17 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", + "integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -3049,6 +3063,64 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.16", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.16.tgz", + "integrity": "sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", + "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -5094,6 +5166,36 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.5.3.tgz", + "integrity": "sha512-Td6F8lZE/YCciSi8xmdBjur6zlKIcoLMZp9jTmJNc44DrXIIcEhr9gra5j1T7RRbtWHaG5tJMEBKS+0S1+T2NQ==", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + } + }, + "node_modules/cache-manager-redis-store": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz", + "integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==", + "dependencies": { + "redis": "^4.3.1" + }, + "engines": { + "node": ">= 16.18.0" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5414,6 +5516,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6304,6 +6414,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6963,6 +7078,14 @@ "is-property": "^1.0.2" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8523,6 +8646,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9674,6 +9802,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "engines": { + "node": ">=16" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9891,6 +10027,22 @@ "node": ">= 0.10" } }, + "node_modules/redis": { + "version": "4.6.14", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.14.tgz", + "integrity": "sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.16", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.6", + "@redis/search": "1.1.6", + "@redis/time-series": "1.0.5" + } + }, "node_modules/reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", diff --git a/package.json b/package.json index c47c337..8b8118c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@aws-sdk/client-s3": "^3.534.0", "@imgly/background-removal-node": "^1.4.5", "@nestjs/axios": "^3.0.2", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", @@ -35,6 +36,8 @@ "@types/multer": "^1.4.11", "axios": "^1.6.7", "bcrypt": "^5.1.1", + "cache-manager": "^5.5.3", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", From 17c86af2527883573c202ab0b6804b03bbbd3a44 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 10:48:51 +0900 Subject: [PATCH 111/236] feat: add caching dependency (redis) --- package-lock.json | 1 + package.json | 1 + src/app.module.ts | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7e2b900..fdec9cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", "passport-kakao": "^1.0.1", + "redis": "^4.6.14", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.32.6", diff --git a/package.json b/package.json index 8b8118c..94d8b33 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", "passport-kakao": "^1.0.1", + "redis": "^4.6.14", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.32.6", diff --git a/src/app.module.ts b/src/app.module.ts index cd922a1..08219f7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,8 @@ import { JwtModule } from '@nestjs/jwt'; import { AgreementsModule } from './APIs/agreements/agreements.module'; import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; import { parseBoolean } from './common/validators/isBoolean'; +import { CacheModule } from '@nestjs/cache-manager'; +import { redisStore } from 'cache-manager-redis-store'; @Module({ imports: [ @@ -41,6 +43,12 @@ import { parseBoolean } from './common/validators/isBoolean'; NotificationsModule, PostBackgroundsModule, ReportsModule, + CacheModule.register({ + store: redisStore, + host: 'localhost', + port: 6379, + ttl: 600, + }), JwtModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ From 31daef8bd01a0b44cf38c362d1ab6055449d55c8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 11:03:10 +0900 Subject: [PATCH 112/236] feat: add caching dependency (cache-manager-redis-yet) --- package-lock.json | 19 +++++++++++++++++++ package.json | 1 + src/app.module.ts | 21 +++++++++++++++------ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdec9cf..a1123be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "bcrypt": "^5.1.1", "cache-manager": "^5.5.3", "cache-manager-redis-store": "^3.0.1", + "cache-manager-redis-yet": "^5.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", @@ -5189,6 +5190,24 @@ "node": ">= 16.18.0" } }, + "node_modules/cache-manager-redis-yet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cache-manager-redis-yet/-/cache-manager-redis-yet-5.0.0.tgz", + "integrity": "sha512-ooBCA71CVUFPoLBx54vig0zhEe5yLcGDQmy9FGS1lBm0wc/efccC+KHdW9mURbuAtL7bi4mRiDA0bHg1a3Vbhw==", + "dependencies": { + "@redis/bloom": "^1.2.0", + "@redis/client": "^1.5.14", + "@redis/graph": "^1.1.1", + "@redis/json": "^1.0.6", + "@redis/search": "^1.1.6", + "@redis/time-series": "^1.0.5", + "cache-manager": "^5.4.0", + "redis": "^4.6.13" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/cache-manager/node_modules/lru-cache": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", diff --git a/package.json b/package.json index 94d8b33..f5132f6 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "bcrypt": "^5.1.1", "cache-manager": "^5.5.3", "cache-manager-redis-store": "^3.0.1", + "cache-manager-redis-yet": "^5.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", diff --git a/src/app.module.ts b/src/app.module.ts index 08219f7..a4210d8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,8 +22,9 @@ import { JwtModule } from '@nestjs/jwt'; import { AgreementsModule } from './APIs/agreements/agreements.module'; import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; import { parseBoolean } from './common/validators/isBoolean'; +import { RedisClientOptions } from 'redis'; import { CacheModule } from '@nestjs/cache-manager'; -import { redisStore } from 'cache-manager-redis-store'; +import { redisStore } from 'cache-manager-redis-yet'; @Module({ imports: [ @@ -43,11 +44,19 @@ import { redisStore } from 'cache-manager-redis-store'; NotificationsModule, PostBackgroundsModule, ReportsModule, - CacheModule.register({ - store: redisStore, - host: 'localhost', - port: 6379, - ttl: 600, + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + store: await redisStore({ + ttl: parseInt(configService.get('REDIS_TTL')), + socket: { + host: configService.get('REDIS_HOST'), + port: parseInt(configService.get('REDIS_PORT')), + }, + }), + }), + isGlobal: true, }), JwtModule.registerAsync({ imports: [ConfigModule], From f858a39807bc96955ccec65c8a38ab079f718e7b Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 11:18:01 +0900 Subject: [PATCH 113/236] feat: add queue dependencies (@nestjs/bull bull) --- package-lock.json | 328 ++++++++++++++++++++++++++++++++++++++++------ package.json | 4 +- 2 files changed, 290 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1123be..f7358f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-s3": "^3.534.0", - "@imgly/background-removal-node": "^1.4.5", + "@imgly/background-removal-node": "^1.4.3", "@nestjs/axios": "^3.0.2", + "@nestjs/bull": "^10.1.1", "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", @@ -25,6 +26,7 @@ "@types/multer": "^1.4.11", "axios": "^1.6.7", "bcrypt": "^5.1.1", + "bull": "^4.12.8", "cache-manager": "^5.5.3", "cache-manager-redis-store": "^3.0.1", "cache-manager-redis-yet": "^5.0.0", @@ -1897,24 +1899,23 @@ "dev": true }, "node_modules/@imgly/background-removal-node": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@imgly/background-removal-node/-/background-removal-node-1.4.5.tgz", - "integrity": "sha512-/s9K88qhKy1jPhrSkBxurUqCVqJ8KHWCc+5yWdppdC4fuSrGC8mK8WQtmULs2ASEr8naY1qpvZu0EL5jr2Hqtg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@imgly/background-removal-node/-/background-removal-node-1.4.3.tgz", + "integrity": "sha512-oLt4HlNDcpL4M7v7Z+aLeNnK/g0vPmyGyZmkRgzomPvr/jEo56xNS5o+WArapNZliEaZv2LtF34KlghdLgNtuQ==", "dependencies": { - "@types/lodash": "~4.14.195", - "@types/ndarray": "~1.0.14", - "@types/node": "~20.3.1", - "lodash": "~4.17.21", - "ndarray": "~1.0.19", - "onnxruntime-node": "~1.17.0", - "sharp": "~0.32.4", - "zod": "~3.21.4" + "@types/lodash": "^4.14.195", + "@types/node": "^20.3.1", + "lodash": "^4.17.21", + "ndarray": "^1.0.19", + "onnxruntime-node": "^1.15.1", + "sharp": "^0.32.4", + "zod": "^3.21.4" } }, - "node_modules/@imgly/background-removal-node/node_modules/@types/node": { - "version": "20.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", - "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==" + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2588,6 +2589,78 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@nestjs/axios": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", @@ -2598,6 +2671,32 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/bull": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-10.1.1.tgz", + "integrity": "sha512-fbhgXNk1DpV27M8hdGH94iNJ7YonZRYyusmHb2qvvYMRBxkuzN0Hjf02jYCNUiPvCpn+WZBeEiYlYK30x3HkAQ==", + "dependencies": { + "@nestjs/bull-shared": "^10.1.1", + "tslib": "2.6.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "bull": "^3.3 || ^4.0.0" + } + }, + "node_modules/@nestjs/bull-shared": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", + "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", + "dependencies": { + "tslib": "2.6.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/cache-manager": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", @@ -2860,13 +2959,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.4.tgz", - "integrity": "sha512-rzUUUZCGYNs/viT9I6W5izJ1+oYCG0ym/dAn31NmYJW9UchxJdX5PCJqWF8iIbys6JgfbdcapMR5t+L7OZsasQ==", + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.8.tgz", + "integrity": "sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.3", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" }, @@ -4020,11 +4119,6 @@ "@types/express": "*" } }, - "node_modules/@types/ndarray": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz", - "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==" - }, "node_modules/@types/node": { "version": "20.11.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.29.tgz", @@ -5149,6 +5243,31 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bull": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/bull/-/bull-4.12.8.tgz", + "integrity": "sha512-JgZVAR3ChuesMWSJfQjgTyXb6kdT6qos3ft9pt0AASRMiRZIjDxHMtxfcOs2Zf8O9uSv0Y5ycYwv5Te3liGJ3A==", + "dependencies": { + "cron-parser": "^4.2.1", + "get-port": "^5.1.1", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.10.1", + "semver": "^7.5.2", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/bull/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -5805,6 +5924,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6511,16 +6641,16 @@ } }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -6560,9 +6690,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -7150,6 +7280,17 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7526,6 +7667,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", @@ -8671,11 +8835,21 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -8748,6 +8922,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -8999,6 +9181,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msgpackr": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.2.tgz", + "integrity": "sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, "node_modules/multer": { "version": "1.4.5-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", @@ -9023,9 +9234,9 @@ "dev": true }, "node_modules/mysql2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.2.tgz", - "integrity": "sha512-3Cwg/UuRkAv/wm6RhtPE5L7JlPB877vwSF6gfLAS68H+zhH+u5oa3AieqEd0D0/kC3W7qIhYbH419f7O9i/5nw==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", + "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -9172,6 +9383,17 @@ } } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10063,6 +10285,25 @@ "@redis/time-series": "1.0.5" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", @@ -10756,6 +10997,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -11004,9 +11250,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -12096,9 +12342,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f5132f6..517c427 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,9 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.534.0", - "@imgly/background-removal-node": "^1.4.5", + "@imgly/background-removal-node": "^1.4.3", "@nestjs/axios": "^3.0.2", + "@nestjs/bull": "^10.1.1", "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", @@ -36,6 +37,7 @@ "@types/multer": "^1.4.11", "axios": "^1.6.7", "bcrypt": "^5.1.1", + "bull": "^4.12.8", "cache-manager": "^5.5.3", "cache-manager-redis-store": "^3.0.1", "cache-manager-redis-yet": "^5.0.0", From 1c25e7369b874227a53fa9b19e0ce1c4e181b05f Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 11:27:04 +0900 Subject: [PATCH 114/236] feat:change whole module imports process using configModule --- src/APIs/notifications/dtos/emit-noti.dto.ts | 14 +++--- .../notifications/notifications.module.ts | 8 ++- .../notifications/notifications.service.ts | 38 +++++++++++--- src/app.module.ts | 50 +++++++++++++------ 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/APIs/notifications/dtos/emit-noti.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts index 88b53cd..046a1fd 100644 --- a/src/APIs/notifications/dtos/emit-noti.dto.ts +++ b/src/APIs/notifications/dtos/emit-noti.dto.ts @@ -1,11 +1,11 @@ -import { OmitType } from '@nestjs/swagger'; +import { OmitType, PickType } from '@nestjs/swagger'; import { Notification } from '../entities/notification.entity'; -export class EmitNotiDto extends OmitType(Notification, [ - 'user', - 'id', - 'targetUser', - 'date_created', - 'date_deleted', +export class EmitNotiDto extends PickType(Notification, [ + 'userKakaoId', + 'targetUserKakaoId', + 'type', + 'url', + 'message', ]) {} export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/notifications.module.ts b/src/APIs/notifications/notifications.module.ts index f1cae02..14906aa 100644 --- a/src/APIs/notifications/notifications.module.ts +++ b/src/APIs/notifications/notifications.module.ts @@ -4,9 +4,15 @@ import { NotificationsService } from './notifications.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Notification } from './entities/notification.entity'; import { NotificationsRepository } from './notifications.repository'; +import { BullModule } from '@nestjs/bull'; @Module({ - imports: [TypeOrmModule.forFeature([Notification])], + imports: [ + TypeOrmModule.forFeature([Notification]), + BullModule.registerQueue({ + name: 'audio', + }), + ], controllers: [NotificationsController], providers: [NotificationsService, NotificationsRepository], exports: [], diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 093c642..daf34b7 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -9,14 +9,29 @@ import { INotificationsServiceConnectUser, INotificationsServiceRead, } from './interfaces/notifications.service.interface'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class NotificationsService { constructor( + @InjectQueue('audio') private redisQueue: Queue, private readonly notificationsRepository: NotificationsRepository, ) {} private notis$: Subject = new Subject(); private observer = this.notis$.asObservable(); + private readonly queueName = 'audio'; + + onModuleInit() { + this.redisQueue.process(this.queueName, async (job) => { + const data = job.data; + this.notis$.next(data); + }); + } + + onModuleDestroy() { + this.redisQueue.close(); + } connectUser({ targetUserKakaoId, @@ -30,21 +45,30 @@ export class NotificationsService { } as MessageEvent; }), ); - // const data = { id: 1, targetUserKakaoId: 3388766789, }; - // this.users$.next(data); return pipe; } - async emitAlarm(emitNotiDto: EmitNotiDto): Promise { + async emitAlarm({ + userKakaoId, + targetUserKakaoId, + url, + type, + message, + }: EmitNotiDto): Promise { try { - const executeResult = - await this.notificationsRepository.createOne(emitNotiDto); + const executeResult = await this.notificationsRepository.createOne({ + userKakaoId, + targetUserKakaoId, + url, + type, + message, + }); const id = executeResult.identifiers[0].id; const data = await this.notificationsRepository.findOne({ where: { id }, }); - // next를 통해 이벤트를 생성 - this.notis$.next(data); + // Redis 큐에 이벤트를 전송 + await this.redisQueue.add(this.queueName, data); return data; } catch (e) { throw new BadRequestException('대상을 찾을 수 없습니다.'); diff --git a/src/app.module.ts b/src/app.module.ts index a4210d8..e60f7aa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,6 +25,7 @@ import { parseBoolean } from './common/validators/isBoolean'; import { RedisClientOptions } from 'redis'; import { CacheModule } from '@nestjs/cache-manager'; import { redisStore } from 'cache-manager-redis-yet'; +import { BullModule } from '@nestjs/bull'; @Module({ imports: [ @@ -44,11 +45,15 @@ import { redisStore } from 'cache-manager-redis-yet'; NotificationsModule, PostBackgroundsModule, ReportsModule, + ConfigModule.forRoot({ + isGlobal: true, + }), CacheModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService) => ({ store: await redisStore({ + database: parseInt(configService.get('REDIS_CACHE_DB')), ttl: parseInt(configService.get('REDIS_TTL')), socket: { host: configService.get('REDIS_HOST'), @@ -58,28 +63,43 @@ import { redisStore } from 'cache-manager-redis-yet'; }), isGlobal: true, }), + BullModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + redis: { + host: configService.get('REDIS_HOST'), + port: parseInt(configService.get('REDIS_PORT')), + db: parseInt(configService.get('REDIS_QUEUE_DB')), + }, + isGlobal: true, + }), + }), JwtModule.registerAsync({ imports: [ConfigModule], + inject: [ConfigService], useFactory: async (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET'), signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') }, }), - inject: [ConfigService], - }), - ConfigModule.forRoot({ - isGlobal: true, }), - TypeOrmModule.forRoot({ - bigNumberStrings: false, - type: process.env.DATABASE_TYPE as 'mysql', - host: process.env.DATABASE_HOST, - port: Number(process.env.DATABASE_PORT), - username: process.env.DATABASE_USERNAME, - password: process.env.DATABASE_PASSWORD, - database: process.env.DATABASE_DATABASE, - entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: parseBoolean(process.env.DATABASE_SYNCHRO), - logging: true, + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + bigNumberStrings: false, + type: configService.get('DATABASE_TYPE') as 'mysql', + host: configService.get('DATABASE_HOST'), + port: parseInt(configService.get('DATABASE_PORT')), + username: configService.get('DATABASE_USERNAME'), + password: configService.get('DATABASE_PASSWORD'), + database: configService.get('DATABASE_DATABASE'), + entities: [__dirname + '/APIs/**/*.entity.*'], + synchronize: parseBoolean( + configService.get('DATABASE_SYNCHRO'), + ), + logging: true, + }), }), ], controllers: [AppController], From 57190eb75dcefa4f1b8b1a685a6d2747bef52827 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 23 May 2024 12:23:53 +0900 Subject: [PATCH 115/236] fix: change NotType Enum --- src/common/enums/not-type.enum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/enums/not-type.enum.ts b/src/common/enums/not-type.enum.ts index 4d6ace6..de8de13 100644 --- a/src/common/enums/not-type.enum.ts +++ b/src/common/enums/not-type.enum.ts @@ -1,7 +1,7 @@ export enum NotType { COMMENT = 'COMMENT', REPLY = 'REPLY', - NEIGHBOR_POST = 'NEIGHBOR_POST', + FOLLOW_POST = 'FOLLOW_POST', ANNOUNCEMENT = 'ANNOUNCEMENT', REPORT = 'REPORT', } From 518f12e89e3ccd77b3bde8508dae2458e636003e Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 25 May 2024 03:17:04 +0900 Subject: [PATCH 116/236] fix: delete 'message' column on not entity --- src/APIs/follows/follows.module.ts | 7 ++++++- src/APIs/follows/follows.service.ts | 2 ++ src/APIs/notifications/dtos/emit-noti.dto.ts | 1 - src/APIs/notifications/entities/notification.entity.ts | 4 ---- src/APIs/notifications/notifications.module.ts | 2 +- src/APIs/notifications/notifications.service.ts | 2 -- src/common/enums/not-type.enum.ts | 2 ++ 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/APIs/follows/follows.module.ts b/src/APIs/follows/follows.module.ts index b9c5423..c57a71f 100644 --- a/src/APIs/follows/follows.module.ts +++ b/src/APIs/follows/follows.module.ts @@ -6,9 +6,14 @@ import { FollowsService } from './follows.service'; import { FollowsController } from './follows.controller'; import { Follow } from './entities/follow.entity'; import { FollowsRepository } from './follows.repository'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [UsersModule, TypeOrmModule.forFeature([Follow, User])], + imports: [ + UsersModule, + NotificationsModule, + TypeOrmModule.forFeature([Follow, User]), + ], providers: [FollowsService, FollowsRepository], controllers: [FollowsController], exports: [FollowsService], diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index aded94b..ca07707 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -9,11 +9,13 @@ import { IFollowsServiceGetList, IFollowsServiceUsers, } from './interfaces/follows.service.interface'; +import { NotificationsService } from '../notifications/notifications.service'; @Injectable() export class FollowsService { constructor( private readonly followsRepository: FollowsRepository, + private readonly notificationsService: NotificationsService, private readonly dataSource: DataSource, ) {} diff --git a/src/APIs/notifications/dtos/emit-noti.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts index 046a1fd..e764d86 100644 --- a/src/APIs/notifications/dtos/emit-noti.dto.ts +++ b/src/APIs/notifications/dtos/emit-noti.dto.ts @@ -5,7 +5,6 @@ export class EmitNotiDto extends PickType(Notification, [ 'targetUserKakaoId', 'type', 'url', - 'message', ]) {} export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index daaacae..82fedfb 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -50,10 +50,6 @@ export class Notification { @Column() url: string; - @ApiProperty({ description: '알림 메시지' }) - @Column() - message: string; - @ApiProperty({ description: '생성된 날짜', type: Date }) @CreateDateColumn() date_created: Date; diff --git a/src/APIs/notifications/notifications.module.ts b/src/APIs/notifications/notifications.module.ts index 14906aa..13b9245 100644 --- a/src/APIs/notifications/notifications.module.ts +++ b/src/APIs/notifications/notifications.module.ts @@ -15,6 +15,6 @@ import { BullModule } from '@nestjs/bull'; ], controllers: [NotificationsController], providers: [NotificationsService, NotificationsRepository], - exports: [], + exports: [NotificationsService], }) export class NotificationsModule {} diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index daf34b7..1e93ec3 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -53,7 +53,6 @@ export class NotificationsService { targetUserKakaoId, url, type, - message, }: EmitNotiDto): Promise { try { const executeResult = await this.notificationsRepository.createOne({ @@ -61,7 +60,6 @@ export class NotificationsService { targetUserKakaoId, url, type, - message, }); const id = executeResult.identifiers[0].id; const data = await this.notificationsRepository.findOne({ diff --git a/src/common/enums/not-type.enum.ts b/src/common/enums/not-type.enum.ts index de8de13..0ce7196 100644 --- a/src/common/enums/not-type.enum.ts +++ b/src/common/enums/not-type.enum.ts @@ -1,6 +1,8 @@ export enum NotType { COMMENT = 'COMMENT', REPLY = 'REPLY', + LIKE = 'LIKE', + FOLLOW = 'FOLLOW', FOLLOW_POST = 'FOLLOW_POST', ANNOUNCEMENT = 'ANNOUNCEMENT', REPORT = 'REPORT', From 281df92e52ef4fe61c51e7a25988bd5b75df2af9 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 11:28:02 +0900 Subject: [PATCH 117/236] feat: add cache on fetchPost --- src/APIs/posts/posts.repository.ts | 4 ++-- src/APIs/posts/posts.service.ts | 34 ++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 01b2137..58a8fec 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -101,7 +101,7 @@ export class PostsRepository extends Repository { ]) .where(`p.userKakaoId = any(${subQuery})`) .andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], + scopes: [OpenScope.PUBLIC], }) //sql injection 방지를 위해 만드시 enum 거칠 것 .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { search: `%${page.search}%`, @@ -198,7 +198,7 @@ export class PostsRepository extends Repository { queryBuilder .andWhere(`p.userKakaoId = any(${subQuery})`) .andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED], + scopes: [OpenScope.PUBLIC], }); //sql injection 방지를 위해 만드시 enum 거칠 것 if (date_filter) { diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 48f640f..c629490 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + Inject, Injectable, NotFoundException, UnauthorizedException, @@ -18,7 +19,6 @@ import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; -import { CommentsService } from '../comments/comments.service'; import { PostResponseDto } from './dtos/post-response.dto'; import { FetchPostForUpdateDto, @@ -26,7 +26,10 @@ import { } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { PostsOrderOption } from 'src/common/enums/posts-order-option'; +import { + PostsOrderOption, + PostsOrderOptionWrap, +} from 'src/common/enums/posts-order-option'; import { FollowsService } from '../follows/follows.service'; import { DateOption } from 'src/common/enums/date-option'; import { Follow } from '../follows/entities/follow.entity'; @@ -40,6 +43,9 @@ import { IPostsServicePostId, IPostsServicePostUserIdPair, } from './interfaces/posts.service.interface'; +import { SortOption } from 'src/common/enums/sort-option'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; @Injectable() export class PostsService { @@ -48,9 +54,9 @@ export class PostsService { private readonly utilsService: UtilsService, private readonly dataSource: DataSource, private readonly stickerBlocksService: StickerBlocksService, - private readonly commentsService: CommentsService, private readonly postsRepository: PostsRepository, private readonly followsService: FollowsService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} async saveImage(file: Express.Multer.File) { return await this.imageUpload(file); @@ -246,6 +252,23 @@ export class PostsService { }: IPostsServiceFetchPostsCursor): Promise< CustomCursorPageDto > { + const useCache = + cursorOption.order === PostsOrderOptionWrap.VIEW && + cursorOption.take === 10 && + cursorOption.date_created === DateOption.WEEK && + cursorOption.sort === SortOption.DESC; + const cacheKey = `fetchPostsCursor_${JSON.stringify(cursorOption)}`; + + if (useCache) { + const cachedPosts = + await this.cacheManager.get>( + cacheKey, + ); + if (cachedPosts) { + return cachedPosts; + } + } + let date_filter: Date; if (cursorOption.date_created) date_filter = this.getDate(cursorOption.date_created); @@ -253,7 +276,10 @@ export class PostsService { cursorOption, date_filter, }); - return await this.createCursorResponse({ posts, cursorOption }); + const result = await this.createCursorResponse({ posts, cursorOption }); + if (useCache) { + await this.cacheManager.set(cacheKey, result, 1800); + } } async fetchFriendsPostsCursor({ From 40a8027eb9375b5f88a694d606acc125bdb79ce9 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 11:30:24 +0900 Subject: [PATCH 118/236] fix: change follow scope condition --- src/APIs/follows/follows.service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index ca07707..dcfa00c 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -33,15 +33,23 @@ export class FollowsService { if (from_user === to_user) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; if (from_user !== null && to_user !== null) { - const follow = await this.followsRepository.findOne({ + const following = await this.followsRepository.findOne({ where: { from_user: { kakaoId: from_user }, to_user: { kakaoId: to_user }, }, }); - if (follow) { + const follower = await this.followsRepository.findOne({ + where: { + from_user: { kakaoId: to_user }, + to_user: { kakaoId: from_user }, + }, + }); + if (following && follower) { return [OpenScope.PUBLIC, OpenScope.PROTECTED]; } + if (following) return [OpenScope.PUBLIC]; + if (follower) return [OpenScope.PUBLIC]; } return [OpenScope.PUBLIC]; From 7334803d55879e1af819f23426d280dd8bc1b5d8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 11:35:52 +0900 Subject: [PATCH 119/236] fix: change stickerBlock Entitiy schema --- src/APIs/posts/posts.repository.ts | 2 +- .../entities/stickerblock.entity.ts | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 58a8fec..4acfbed 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -196,7 +196,7 @@ export class PostsRepository extends Repository { const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); queryBuilder - .andWhere(`p.userKakaoId = any(${subQuery})`) + .andWhere(`p.userKakaoId = any(${subQuery})`) // 만약 서로이웃으로 scope하려면, 정반대 옵션으로 subQuery2를 만들고 andWhere()하나 추가하면 될듯 .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC], }); //sql injection 방지를 위해 만드시 enum 거칠 것 diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 5d62064..0a2a444 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -42,27 +42,27 @@ export class StickerBlock { }) posts: Posts; - @ApiProperty({ description: '스티커의 depth', type: Number }) - @Column() - depth: number; + @ApiProperty({ description: '스티커의 width', type: Number }) + @Column({ type: 'float' }) + width: number; - @ApiProperty({ description: '스티커의 fill', type: String }) - @Column() - fill: string; + @ApiProperty({ description: '스티커의 top', type: Number }) + @Column({ type: 'float' }) + top: number; - @ApiProperty({ description: '스티커의 x좌표', type: String }) - @Column() - x: string; + @ApiProperty({ description: '스티커의 left', type: Number }) + @Column({ type: 'float' }) + left: number; - @ApiProperty({ description: '스티커의 y좌표', type: String }) - @Column() - y: string; + @ApiProperty({ description: '스티커의 rotate', type: Number }) + @Column({ type: 'float' }) + rotate: number; - @ApiProperty({ description: '스티커의 가로 폭', type: String }) - @Column() - width: string; + @ApiProperty({ description: '스티커의 scale', type: Number }) + @Column({ type: 'float' }) + scale: number; - @ApiProperty({ description: '스티커의 세로 폭', type: String }) - @Column() - height: string; + @ApiProperty({ description: '스티커의 zindex', type: Number }) + @Column({ type: 'float' }) + zindex: number; } From 6aa7f09c9a7830028c452e2bf20c9fd91f2e7435 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 22:45:34 +0900 Subject: [PATCH 120/236] feat: send noti when writing comment & reply --- src/APIs/comments/comments.module.ts | 4 +-- src/APIs/comments/comments.repository.ts | 10 ++++++- src/APIs/comments/comments.service.ts | 30 ++++++++++++------- src/APIs/notifications/dtos/emit-noti.dto.ts | 1 - .../entities/notification.entity.ts | 4 --- .../notifications/notifications.repository.ts | 9 ++++-- .../notifications/notifications.service.ts | 2 -- src/APIs/posts/posts.module.ts | 2 -- src/common/enums/not-type.enum.ts | 1 - 9 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/APIs/comments/comments.module.ts b/src/APIs/comments/comments.module.ts index 0481fe4..efb288d 100644 --- a/src/APIs/comments/comments.module.ts +++ b/src/APIs/comments/comments.module.ts @@ -5,10 +5,10 @@ import { Comment } from './entities/comment.entity'; import { CommentsService } from './comments.service'; import { CommentsController } from './comments.controller'; import { CommentsRepository } from './comments.repository'; -import { UsersModule } from '../users/users.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Comment]), UsersModule], + imports: [TypeOrmModule.forFeature([Comment]), NotificationsModule], providers: [CommentsService, CommentsRepository], controllers: [CommentsController], exports: [CommentsService], diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 6912009..4402f21 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -15,7 +15,6 @@ export class CommentsRepository extends Repository { async insertComment({ createCommentDto, }: ICommentsRepositoryInsertComment): Promise { - console.log(createCommentDto); return await this.createQueryBuilder('c') .insert() .into(Comment, Object.keys(createCommentDto)) @@ -23,6 +22,15 @@ export class CommentsRepository extends Repository { .execute(); } + async fetchCommentWithNotiInfo({ id }) { + return await this.createQueryBuilder('c') + .leftJoinAndSelect('c.user', 'user') + .leftJoinAndSelect('c.posts', 'posts') + .leftJoinAndSelect('c.parent', 'parent') + .where('c.id = :id', { id }) + .getOne(); + } + async fetchComments({ postsId, }: ICommentsRepositoryfetchComments): Promise { diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 0bff446..b3ed767 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -5,7 +5,6 @@ import { NotFoundException, } from '@nestjs/common'; import { CreateCommentDto } from './dtos/create-comment.dto'; -import { UsersService } from '../users/users.service'; import { CommentsRepository } from './comments.repository'; import { DataSource, EntityManager } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; @@ -14,7 +13,6 @@ import { FetchCommentDto, FetchCommentsDto, } from './dtos/fetch-comments.dto'; -import { USER_PRIMARY_SELECT_OPTION } from '../users/dtos/user-response.dto'; import { ICommentsServiceDelete, ICommentsServiceFetch, @@ -23,13 +21,15 @@ import { ICommentsServicePostsIdValidCheck, } from './interfaces/comments.service.interface'; import { Comment } from './entities/comment.entity'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotType } from 'src/common/enums/not-type.enum'; @Injectable() export class CommentsService { constructor( private readonly commentsRepository: CommentsRepository, - private readonly usersService: UsersService, private readonly dataSource: DataSource, + private readonly notificationsService: NotificationsService, ) {} async postsIdValidCheck({ @@ -68,14 +68,24 @@ export class CommentsService { const commentData = await this.commentsRepository.insertComment({ createCommentDto, }); - const id = commentData.identifiers[0]; - return await this.commentsRepository.findOne({ - select: { - user: USER_PRIMARY_SELECT_OPTION, - }, - relations: { user: true }, - where: { ...id }, + const { id } = commentData.identifiers[0]; + console.log(id); + const { posts, parent, ...result } = + await this.commentsRepository.fetchCommentWithNotiInfo({ id }); + + await this.notificationsService.emitAlarm({ + userKakaoId: result.userKakaoId, + targetUserKakaoId: posts.userKakaoId, + type: NotType.COMMENT, }); + if (result.parentId) { + await this.notificationsService.emitAlarm({ + userKakaoId: result.userKakaoId, + targetUserKakaoId: parent.userKakaoId, + type: NotType.REPLY, + }); + } + return result; } async patchComment({ diff --git a/src/APIs/notifications/dtos/emit-noti.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts index e764d86..3d8809e 100644 --- a/src/APIs/notifications/dtos/emit-noti.dto.ts +++ b/src/APIs/notifications/dtos/emit-noti.dto.ts @@ -4,7 +4,6 @@ export class EmitNotiDto extends PickType(Notification, [ 'userKakaoId', 'targetUserKakaoId', 'type', - 'url', ]) {} export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 82fedfb..33cd522 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -46,10 +46,6 @@ export class Notification { @Column({ default: false }) is_checked: boolean; - @ApiProperty({ description: '리다이렉션 url', type: String }) - @Column() - url: string; - @ApiProperty({ description: '생성된 날짜', type: Date }) @CreateDateColumn() date_created: Date; diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index 0aef99e..2d217d5 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -23,9 +23,12 @@ export class NotificationsRepository extends Repository { date_created, is_checked, }): Promise { - const query = this.createQueryBuilder('').where('userKakaoId = :kakaoId', { - kakaoId, - }); + const query = this.createQueryBuilder('').where( + 'targetUserKakaoId = :kakaoId', + { + kakaoId, + }, + ); if (!is_checked) { query.andWhere('is_checked = true'); } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 1e93ec3..6cf9c9c 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -51,14 +51,12 @@ export class NotificationsService { async emitAlarm({ userKakaoId, targetUserKakaoId, - url, type, }: EmitNotiDto): Promise { try { const executeResult = await this.notificationsRepository.createOne({ userKakaoId, targetUserKakaoId, - url, type, }); const id = executeResult.identifiers[0].id; diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index e7b3724..1728cc4 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -10,7 +10,6 @@ import { PostBackground } from '../postBackgrounds/entities/postBackground.entit import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; -import { CommentsModule } from '../comments/comments.module'; import { FollowsModule } from '../follows/follows.module'; @Module({ @@ -20,7 +19,6 @@ import { FollowsModule } from '../follows/follows.module'; AwsModule, FollowsModule, StickerBlocksModule, - CommentsModule, ], providers: [PostsService, PostsRepository], controllers: [PostsController], diff --git a/src/common/enums/not-type.enum.ts b/src/common/enums/not-type.enum.ts index 0ce7196..04a80bf 100644 --- a/src/common/enums/not-type.enum.ts +++ b/src/common/enums/not-type.enum.ts @@ -3,7 +3,6 @@ export enum NotType { REPLY = 'REPLY', LIKE = 'LIKE', FOLLOW = 'FOLLOW', - FOLLOW_POST = 'FOLLOW_POST', ANNOUNCEMENT = 'ANNOUNCEMENT', REPORT = 'REPORT', } From 434b9f1c09693bbf0bc5bb10bc06b10dab1797b4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 22:48:28 +0900 Subject: [PATCH 121/236] feat: add Interface & response type on fetchCommentWithNotiInfo --- src/APIs/comments/comments.repository.ts | 5 ++++- .../comments/interfaces/comments.repository.interface.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 4402f21..37546a1 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -3,6 +3,7 @@ import { Comment } from './entities/comment.entity'; import { Injectable } from '@nestjs/common'; import { FetchCommentsDto } from './dtos/fetch-comments.dto'; import { + ICommentsRepositoryId, ICommentsRepositoryInsertComment, ICommentsRepositoryfetchComments, } from './interfaces/comments.repository.interface'; @@ -22,7 +23,9 @@ export class CommentsRepository extends Repository { .execute(); } - async fetchCommentWithNotiInfo({ id }) { + async fetchCommentWithNotiInfo({ + id, + }: ICommentsRepositoryId): Promise { return await this.createQueryBuilder('c') .leftJoinAndSelect('c.user', 'user') .leftJoinAndSelect('c.posts', 'posts') diff --git a/src/APIs/comments/interfaces/comments.repository.interface.ts b/src/APIs/comments/interfaces/comments.repository.interface.ts index 708e1a6..53a7451 100644 --- a/src/APIs/comments/interfaces/comments.repository.interface.ts +++ b/src/APIs/comments/interfaces/comments.repository.interface.ts @@ -7,3 +7,7 @@ export interface ICommentsRepositoryInsertComment { export interface ICommentsRepositoryfetchComments { postsId: number; } + +export interface ICommentsRepositoryId { + id: number; +} From a5383188ce13da93b23809b2e48ae63b0ce62026 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 22:51:32 +0900 Subject: [PATCH 122/236] feat: send noti on following process --- src/APIs/comments/comments.service.ts | 1 - src/APIs/follows/follows.service.ts | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index b3ed767..802b24a 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -69,7 +69,6 @@ export class CommentsService { createCommentDto, }); const { id } = commentData.identifiers[0]; - console.log(id); const { posts, parent, ...result } = await this.commentsRepository.fetchCommentWithNotiInfo({ id }); diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index dcfa00c..8a9a358 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -10,6 +10,7 @@ import { IFollowsServiceUsers, } from './interfaces/follows.service.interface'; import { NotificationsService } from '../notifications/notifications.service'; +import { NotType } from 'src/common/enums/not-type.enum'; @Injectable() export class FollowsService { @@ -104,6 +105,11 @@ export class FollowsService { await queryRunner.manager.update(User, toUserData.kakaoId, { follower_count: () => 'follower_count +1', }); + await this.notificationsService.emitAlarm({ + userKakaoId: from_user, + targetUserKakaoId: to_user, + type: NotType.FOLLOW, + }); await queryRunner.commitTransaction(); return await this.followsRepository.findOne({ where: { id: follow.id } }); } catch (e) { From 9b47eb7febdf048922103b4b98e5af33b558a249 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 27 May 2024 22:53:51 +0900 Subject: [PATCH 123/236] feat: send noti when liking post --- src/APIs/likes/likes.module.ts | 3 ++- src/APIs/likes/likes.service.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/APIs/likes/likes.module.ts b/src/APIs/likes/likes.module.ts index 512943a..dcb09be 100644 --- a/src/APIs/likes/likes.module.ts +++ b/src/APIs/likes/likes.module.ts @@ -5,9 +5,10 @@ import { LikesController } from './likes.controller'; import { LikesService } from './likes.service'; import { Likes } from './entities/like.entity'; import { LikesRepository } from './likes.repository'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Posts, Likes])], + imports: [TypeOrmModule.forFeature([Posts, Likes]), NotificationsModule], providers: [LikesService, LikesRepository], controllers: [LikesController], }) diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index d7f7572..e5178ff 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -6,12 +6,15 @@ import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; import { LikesRepository } from './likes.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { ILikesServiceIds } from './interfaces/likes.service.interface'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotType } from 'src/common/enums/not-type.enum'; @Injectable() export class LikesService { constructor( private readonly likesRepository: LikesRepository, private readonly dataSource: DataSource, + private readonly notificationsService: NotificationsService, ) {} async fetchIfLiked({ kakaoId, id }: ILikesServiceIds): Promise { @@ -43,7 +46,12 @@ export class LikesService { await queryRunner.manager.update(Posts, postData.id, { like_count: () => 'like_count +1', }); - await queryRunner.commitTransaction(); + await await queryRunner.commitTransaction(); + await this.notificationsService.emitAlarm({ + userKakaoId: kakaoId, + targetUserKakaoId: postData.userKakaoId, + type: NotType.LIKE, + }); return likeData; } } catch (e) { From 504ce0663218a9986068900b31a723c4bffa4162 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 28 May 2024 00:18:04 +0900 Subject: [PATCH 124/236] fix: dockerignore --- .dockerignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1a8a80b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +dist +node_modules +.dockerignore +Dockerfile \ No newline at end of file From 67dfaf9cdc20bb07e6c9819d713aedc8cd2464ae Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 28 May 2024 05:18:19 +0900 Subject: [PATCH 125/236] feat: blue green zero dt deployment --- .dockerignore | 5 ++-- deploy/deploy.sh | 69 +++++++++++++++++++++++++++++++++++++++++++++++ deploy/nginx.conf | 50 ++++++++++++++++++++++++++++++++++ package-lock.json | 60 ----------------------------------------- package.json | 1 + 5 files changed, 123 insertions(+), 62 deletions(-) create mode 100755 deploy/deploy.sh create mode 100644 deploy/nginx.conf diff --git a/.dockerignore b/.dockerignore index 1a8a80b..a0cd1fb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .git -dist +/dist node_modules .dockerignore -Dockerfile \ No newline at end of file +Dockerfile +/deploy \ No newline at end of file diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..e6ca5c0 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# 스크립트의 실제 위치를 기준으로 경로 설정 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PEM_PATH="$SCRIPT_DIR/../../../keys/blccu-dev-rsa.pem" + +# PEM 파일 경로가 올바른지 확인 +if [[ ! -f "$PEM_PATH" ]]; then + echo "PEM 파일이 존재하지 않습니다: $PEM_PATH" + exit 1 +fi + +# PEM 파일 권한 확인 및 수정 +chmod 400 "$PEM_PATH" + +HOST="api.blccu.com" +ACCOUNT=ubuntu +SERVICE_NAME=blccu-ecr +DOCKER_TAG=latest +ECR_URL="637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" +SERVER=$ACCOUNT@$HOST + +NGINX_CONFIG=/etc/nginx/nginx.conf +BLUE_PORT="3000" +GREEN_PORT="3001" + +# docker push (aws ecr) +echo -e "\n## Docker build & push ##\n" +npm run build +docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load +docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG +docker push $ECR_URL/$SERVICE_NAME:$DOCKER_TAG + +# 현재 설정에서 활성 포트 찾기 +CURRENT_PORT=$(ssh -i "$PEM_PATH" -o StrictHostKeyChecking=no $SERVER "grep 'server localhost:' $NGINX_CONFIG | awk '{print \$2}' | cut -d ':' -f 2 | sed 's/;//'") +echo -e "\nOld = $CURRENT_PORT\n" + +# 포트 변경 +if [ "$CURRENT_PORT" = "$BLUE_PORT" ]; then + NEW_PORT=$GREEN_PORT +elif [ "$CURRENT_PORT" = "$GREEN_PORT" ]; then + NEW_PORT=$BLUE_PORT +else + echo -e "\n 서버의 blue green 포트 확인 실패 \n" + exit 1; +fi + +echo -e "\nNew $NEW_PORT\n" + +NEW_SERVICE_NAME=$SERVICE_NAME-$NEW_PORT +OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT + +# docker pull & run +echo -e "\n## new docker pull & run ##\n" +ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" +ssh -i $PEM_PATH $SERVER "docker run -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" +# memory랑 cpu 사용량 조절 +# NGINX 설정 파일 수정 +echo -e "\n## Nginx 설정 수정 & restart ##\n" +ssh -i $PEM_PATH $SERVER "sudo sed -i 's/server localhost:$CURRENT_PORT;/server localhost:$NEW_PORT;/g' $NGINX_CONFIG" +ssh -i $PEM_PATH $SERVER "sudo systemctl restart nginx" + +# old docker 제거 +echo -e "\n## old docker 제거 ##\n" +ssh -i $PEM_PATH $SERVER "sudo docker stop $OLD_SERVICE_NAME" +ssh -i $PEM_PATH $SERVER "sudo docker rm $OLD_SERVICE_NAME" +ssh -i $PEM_PATH $SERVER "sudo docker rmi $OLD_SERVICE_NAME:$DOCKER_TAG" +# yes | ssh -i $PEM_PATH $SERVER "sudo docker system prune -a" + +echo -e "\n## 배포 완료. $NEW_SERVICE_NAME ##\n" \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..9f8ce0c --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,50 @@ + +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + client_max_body_size 5M; + upstream blccu-backend { + server localhost:3000; + } + + server { + server_name api.blccu.com; + location / { + proxy_pass http://blccu-backend; + root html; + index index.html index.htm; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/api.blccu.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/api.blccu.com/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + } + + server { + if ($host = api.blccu.com) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80 default_server; + listen [::]:80 default_server; + server_name api.blccu.com; + return 404; # managed by Certbot + + + }} + ~ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f7358f1..6e2bcbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2601,66 +2601,6 @@ "darwin" ] }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", - "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", - "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", - "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", - "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", - "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@nestjs/axios": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", diff --git a/package.json b/package.json index 517c427..7bffb7a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "deploy": "./deploy/deploy.sh", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", From 8419565a2246ba8f133f65ea3b2d8b63accef680 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 28 May 2024 05:21:03 +0900 Subject: [PATCH 126/236] feat: delete imgly --- package-lock.json | 65 --------------------------- package.json | 1 - src/APIs/stickers/stickers.service.ts | 16 ------- 3 files changed, 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e2bcbe..edd6820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-s3": "^3.534.0", - "@imgly/background-removal-node": "^1.4.3", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.1.1", "@nestjs/cache-manager": "^2.2.2", @@ -1898,20 +1897,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/@imgly/background-removal-node": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@imgly/background-removal-node/-/background-removal-node-1.4.3.tgz", - "integrity": "sha512-oLt4HlNDcpL4M7v7Z+aLeNnK/g0vPmyGyZmkRgzomPvr/jEo56xNS5o+WArapNZliEaZv2LtF34KlghdLgNtuQ==", - "dependencies": { - "@types/lodash": "^4.14.195", - "@types/node": "^20.3.1", - "lodash": "^4.17.21", - "ndarray": "^1.0.19", - "onnxruntime-node": "^1.15.1", - "sharp": "^0.32.4", - "zod": "^3.21.4" - } - }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4035,11 +4020,6 @@ "@types/node": "*" } }, - "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" - }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -7630,11 +7610,6 @@ "url": "https://opencollective.com/ioredis" } }, - "node_modules/iota-array": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", - "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7661,11 +7636,6 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -9250,15 +9220,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/ndarray": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", - "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", - "dependencies": { - "iota-array": "^1.0.0", - "is-buffer": "^1.0.2" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9447,24 +9408,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/onnxruntime-common": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz", - "integrity": "sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw==" - }, - "node_modules/onnxruntime-node": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz", - "integrity": "sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg==", - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "onnxruntime-common": "1.17.0" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -12280,14 +12223,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 7bffb7a..9396ce7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.534.0", - "@imgly/background-removal-node": "^1.4.3", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.1.1", "@nestjs/cache-manager": "^2.2.2", diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index f19b18b..7ea3313 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -7,7 +7,6 @@ import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; -import { removeBackground } from '@imgly/background-removal-node'; import { FindStickerDto } from './dtos/find-sticker.dto'; import { UpdateStickerDto } from './dtos/update-sticker.dto'; @@ -95,21 +94,6 @@ export class StickersService { }); } - async removeBg({ url }): Promise { - const blobData = await removeBackground(url); - const arrayBuffer = await blobData.arrayBuffer(); - const bufferData = Buffer.from(arrayBuffer); - - const imageName = this.utilsService.getUUID(); - - const image_url = await this.awsService.imageUploadToS3Buffer( - imageName, - bufferData, - 'png', - ); - return { image_url }; - } - async updateSticker({ image_url, isReusable, From e11da0e9d29f790ded42fa5531d5a114fe294792 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 28 May 2024 08:32:34 +0900 Subject: [PATCH 127/236] feat: add health check process on blue green deployment --- deploy/deploy.sh | 26 +++++- docker-compose.yaml | 4 +- package-lock.json | 157 +++++++++++++++++++++++++++++++- package.json | 1 + src/APIs/posts/posts.service.ts | 39 ++++---- src/app.controller.ts | 15 ++- src/app.module.ts | 61 +++++++------ 7 files changed, 250 insertions(+), 53 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index e6ca5c0..73ee565 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -25,6 +25,8 @@ GREEN_PORT="3001" # docker push (aws ecr) echo -e "\n## Docker build & push ##\n" + + npm run build docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG @@ -52,8 +54,27 @@ OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT # docker pull & run echo -e "\n## new docker pull & run ##\n" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" -ssh -i $PEM_PATH $SERVER "docker run -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" +ssh -i $PEM_PATH $SERVER "docker run --env-file .env.prod -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # memory랑 cpu 사용량 조절 + +# 헬스체크 수행 +echo -e "\n## 헬스체크 수행 ##\n" +for i in {1..20}; do + HEALTH_CHECK=$(ssh -i $PEM_PATH $SERVER "curl -s -o /dev/null -w '%{http_code}' http://localhost:$NEW_PORT/health") + if [ "$HEALTH_CHECK" -eq 200 ]; then + echo -e "\n 헬스체크 성공 \n" + break + fi + echo -e "\n 헬스체크 시도 $i/20 실패. 5초 후 재시도... \n" + sleep 5 +done + +if [ "$HEALTH_CHECK" -ne 200 ]; then + echo -e "\n 헬스체크 실패. 배포 중단 \n" + ssh -i $PEM_PATH $SERVER "docker stop $NEW_SERVICE_NAME && docker rm $NEW_SERVICE_NAME" + exit 1 +fi + # NGINX 설정 파일 수정 echo -e "\n## Nginx 설정 수정 & restart ##\n" ssh -i $PEM_PATH $SERVER "sudo sed -i 's/server localhost:$CURRENT_PORT;/server localhost:$NEW_PORT;/g' $NGINX_CONFIG" @@ -63,7 +84,8 @@ ssh -i $PEM_PATH $SERVER "sudo systemctl restart nginx" echo -e "\n## old docker 제거 ##\n" ssh -i $PEM_PATH $SERVER "sudo docker stop $OLD_SERVICE_NAME" ssh -i $PEM_PATH $SERVER "sudo docker rm $OLD_SERVICE_NAME" -ssh -i $PEM_PATH $SERVER "sudo docker rmi $OLD_SERVICE_NAME:$DOCKER_TAG" +echo -e "$ECR_URL/$SERVICE_NAME:$DOCKER_TAG" +ssh -i $PEM_PATH $SERVER "docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi" # yes | ssh -i $PEM_PATH $SERVER "sudo docker system prune -a" echo -e "\n## 배포 완료. $NEW_SERVICE_NAME ##\n" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 18283d2..917b4bc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,12 @@ version: "3.7" services: - my-backend: + blccu-ecr: build: context: . dockerfile: Dockerfile + args: + PLATFORM: linux/amd64 ports: - 3000:3000 env_file: diff --git a/package-lock.json b/package-lock.json index edd6820..d88fbb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", + "@nestjs/terminus": "^10.2.3", "@nestjs/typeorm": "^10.0.2", "@types/multer": "^1.4.11", "axios": "^1.6.7", @@ -2974,6 +2975,75 @@ } } }, + "node_modules/@nestjs/terminus": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-10.2.3.tgz", + "integrity": "sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==", + "dependencies": { + "boxen": "5.1.2", + "check-disk-space": "3.4.0" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@grpc/proto-loader": "*", + "@mikro-orm/core": "*", + "@mikro-orm/nestjs": "*", + "@nestjs/axios": "^1.0.0 || ^2.0.0 || ^3.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "@nestjs/microservices": "^9.0.0 || ^10.0.0", + "@nestjs/mongoose": "^9.0.0 || ^10.0.0", + "@nestjs/sequelize": "^9.0.0 || ^10.0.0", + "@nestjs/typeorm": "^9.0.0 || ^10.0.0", + "@prisma/client": "*", + "mongoose": "*", + "reflect-metadata": "0.1.x || 0.2.x", + "rxjs": "7.x", + "sequelize": "*", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@grpc/proto-loader": { + "optional": true + }, + "@mikro-orm/core": { + "optional": true + }, + "@mikro-orm/nestjs": { + "optional": true + }, + "@nestjs/axios": { + "optional": true + }, + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/mongoose": { + "optional": true + }, + "@nestjs/sequelize": { + "optional": true + }, + "@nestjs/typeorm": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "mongoose": { + "optional": true + }, + "sequelize": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.4.tgz", @@ -4583,6 +4653,14 @@ } } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -5057,6 +5135,54 @@ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -5341,6 +5467,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-disk-space": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", + "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", + "engines": { + "node": ">=16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5418,6 +5552,17 @@ "validator": "^13.9.0" } }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -11626,7 +11771,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -12101,6 +12245,17 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 9396ce7..2291156 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", + "@nestjs/terminus": "^10.2.3", "@nestjs/typeorm": "^10.0.2", "@types/multer": "^1.4.11", "axios": "^1.6.7", diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index c629490..b182aee 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -14,7 +14,6 @@ import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; -// import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; @@ -26,10 +25,7 @@ import { } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { - PostsOrderOption, - PostsOrderOptionWrap, -} from 'src/common/enums/posts-order-option'; +import { PostsOrderOption } from 'src/common/enums/posts-order-option'; import { FollowsService } from '../follows/follows.service'; import { DateOption } from 'src/common/enums/date-option'; import { Follow } from '../follows/entities/follow.entity'; @@ -252,22 +248,22 @@ export class PostsService { }: IPostsServiceFetchPostsCursor): Promise< CustomCursorPageDto > { - const useCache = - cursorOption.order === PostsOrderOptionWrap.VIEW && - cursorOption.take === 10 && - cursorOption.date_created === DateOption.WEEK && - cursorOption.sort === SortOption.DESC; + // const useCache = + // cursorOption.order === PostsOrderOptionWrap.VIEW && + // cursorOption.take === 10 && + // cursorOption.date_created === DateOption.WEEK && + // cursorOption.sort === SortOption.DESC; const cacheKey = `fetchPostsCursor_${JSON.stringify(cursorOption)}`; - if (useCache) { - const cachedPosts = - await this.cacheManager.get>( - cacheKey, - ); - if (cachedPosts) { - return cachedPosts; - } + // if (useCache) { + const cachedPosts = + await this.cacheManager.get>( + cacheKey, + ); + if (cachedPosts) { + return cachedPosts; } + // } let date_filter: Date; if (cursorOption.date_created) @@ -277,9 +273,10 @@ export class PostsService { date_filter, }); const result = await this.createCursorResponse({ posts, cursorOption }); - if (useCache) { - await this.cacheManager.set(cacheKey, result, 1800); - } + // if (useCache) { + await this.cacheManager.set(cacheKey, result, 180000); + // } + return result; } async fetchFriendsPostsCursor({ diff --git a/src/app.controller.ts b/src/app.controller.ts index c282842..a4b3486 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -2,11 +2,16 @@ import { Controller, Get, HttpCode, Res } from '@nestjs/common'; import { AppService } from './app.service'; import { Response } from 'express'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { HealthCheckService, HttpHealthIndicator } from '@nestjs/terminus'; @ApiTags('root') @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private health: HealthCheckService, + private http: HttpHealthIndicator, + ) {} @ApiOperation({ summary: 'swagger docs로 redirect' }) @Get('/') @@ -14,4 +19,12 @@ export class AppController { get(@Res() res: Response) { return res.redirect('/api-docs'); } + + @ApiOperation({ summary: 'health check' }) + @Get('/health') + healthCheck() { + return this.health.check([ + () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com'), + ]); + } } diff --git a/src/app.module.ts b/src/app.module.ts index e60f7aa..2b5e0ab 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,8 +24,11 @@ import { FeedbacksModule } from './APIs/feedbacks/feedbacks.module'; import { parseBoolean } from './common/validators/isBoolean'; import { RedisClientOptions } from 'redis'; import { CacheModule } from '@nestjs/cache-manager'; -import { redisStore } from 'cache-manager-redis-yet'; +// import * as redisStore from 'cache-manager-redis-store'; import { BullModule } from '@nestjs/bull'; +import { redisStore } from 'cache-manager-redis-yet'; +import { TerminusModule } from '@nestjs/terminus'; +import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ @@ -45,9 +48,38 @@ import { BullModule } from '@nestjs/bull'; NotificationsModule, PostBackgroundsModule, ReportsModule, + TerminusModule, + HttpModule, ConfigModule.forRoot({ isGlobal: true, }), + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') }, + }), + }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + bigNumberStrings: false, + type: configService.get('DATABASE_TYPE') as 'mysql', + host: configService.get('DATABASE_HOST'), + port: parseInt(configService.get('DATABASE_PORT')), + username: configService.get('DATABASE_USERNAME'), + password: configService.get('DATABASE_PASSWORD'), + database: configService.get('DATABASE_DATABASE'), + entities: [__dirname + '/APIs/**/*.entity.*'], + synchronize: parseBoolean( + configService.get('DATABASE_SYNCHRO'), + ), + logging: true, + }), + }), + CacheModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], @@ -63,6 +95,7 @@ import { BullModule } from '@nestjs/bull'; }), isGlobal: true, }), + BullModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], @@ -75,32 +108,6 @@ import { BullModule } from '@nestjs/bull'; isGlobal: true, }), }), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') }, - }), - }), - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - bigNumberStrings: false, - type: configService.get('DATABASE_TYPE') as 'mysql', - host: configService.get('DATABASE_HOST'), - port: parseInt(configService.get('DATABASE_PORT')), - username: configService.get('DATABASE_USERNAME'), - password: configService.get('DATABASE_PASSWORD'), - database: configService.get('DATABASE_DATABASE'), - entities: [__dirname + '/APIs/**/*.entity.*'], - synchronize: parseBoolean( - configService.get('DATABASE_SYNCHRO'), - ), - logging: true, - }), - }), ], controllers: [AppController], providers: [AppService], From a04f3d8fc57fb54883c9cbeb63ebb4be538e5597 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 29 May 2024 20:29:58 +0900 Subject: [PATCH 128/236] feat: patch post API --- src/APIs/posts/dtos/post-response.dto.ts | 6 +++++ .../interfaces/posts.service.interface.ts | 6 +++++ src/APIs/posts/posts.controller.ts | 19 +++++++++++++- src/APIs/posts/posts.service.ts | 25 ++++++++++++++----- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/APIs/posts/dtos/post-response.dto.ts b/src/APIs/posts/dtos/post-response.dto.ts index 3266acc..57a873a 100644 --- a/src/APIs/posts/dtos/post-response.dto.ts +++ b/src/APIs/posts/dtos/post-response.dto.ts @@ -7,3 +7,9 @@ export class PostResponseDto extends OmitType(Posts, ['user']) { @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) user: UserPrimaryResponseDto; } + +export class PostOnlyResponseDto extends OmitType(Posts, [ + 'user', + 'postBackground', + 'postCategory', +]) {} diff --git a/src/APIs/posts/interfaces/posts.service.interface.ts b/src/APIs/posts/interfaces/posts.service.interface.ts index 39d5ecb..d54bb1b 100644 --- a/src/APIs/posts/interfaces/posts.service.interface.ts +++ b/src/APIs/posts/interfaces/posts.service.interface.ts @@ -1,6 +1,7 @@ import { CreatePostInput } from '../dtos/create-post.input'; import { CursorFetchPosts } from '../dtos/cursor-fetch-posts.dto'; import { FetchUserPostsInput } from '../dtos/fetch-user-posts.input'; +import { PatchPostInput } from '../dtos/patch-post.dto'; import { Posts } from '../entities/posts.entity'; export interface IPostsServicePostId extends Pick {} @@ -40,3 +41,8 @@ export interface IPostsServiceFetchUserPostsCursor { targetKakaoId: number; kakaoId: number; } + +export interface IPostsServicePatchPost extends PatchPostInput { + kakaoId: number; + id: number; +} diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 99994f6..540e931 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -5,6 +5,7 @@ import { Get, HttpCode, Param, + Patch, Post, Query, Req, @@ -33,7 +34,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { PostResponseDto } from './dtos/post-response.dto'; +import { PostOnlyResponseDto, PostResponseDto } from './dtos/post-response.dto'; import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, @@ -42,6 +43,7 @@ import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto import { SortOption } from 'src/common/enums/sort-option'; import { CursorFetchPosts } from './dtos/cursor-fetch-posts.dto'; import { CursorPagePostResponseDto } from './dtos/cursor-page-post-response.dto'; +import { PatchPostInput } from './dtos/patch-post.dto'; @ApiTags('게시글 API') @Controller('posts') @@ -91,6 +93,21 @@ export class PostsController { return await this.postsService.save(dto); } + @ApiOperation({ summary: '게시글 patch' }) + @ApiCookieAuth() + @ApiOkResponse({ type: PostOnlyResponseDto }) + @UseGuards(AuthGuardV2) + @Patch(':postId') + @HttpCode(200) + async patchPost( + @Req() req: Request, + @Body() body: PatchPostInput, + @Param('postId') id: number, + ): Promise { + const kakaoId = req.user.userId; + return await this.postsService.patchPost({ ...body, id, kakaoId }); + } + @ApiOperation({ summary: '임시작성 게시글 조회', description: '로그인된 유저의 임시작성 게시글을 조회한다.', diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index b182aee..67711d2 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Inject, Injectable, NotFoundException, @@ -18,7 +19,7 @@ import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; import { PostsRepository } from './posts.repository'; -import { PostResponseDto } from './dtos/post-response.dto'; +import { PostOnlyResponseDto, PostResponseDto } from './dtos/post-response.dto'; import { FetchPostForUpdateDto, PostResponseDtoExceptCategory, @@ -36,10 +37,10 @@ import { IPostsServiceFetchPostForUpdate, IPostsServiceFetchPostsCursor, IPostsServiceFetchUserPostsCursor, + IPostsServicePatchPost, IPostsServicePostId, IPostsServicePostUserIdPair, } from './interfaces/posts.service.interface'; -import { SortOption } from 'src/common/enums/sort-option'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; @@ -113,7 +114,7 @@ export class PostsService { try { Object.keys(createPostDto).map((el) => { const value = createPostDto[el]; - if (createPostDto[el]) { + if (createPostDto[el] != null) { post[el] = value; } }); @@ -126,9 +127,6 @@ export class PostsService { .insert() .into(Posts, Object.keys(post)) .values(post) - .orUpdate(Object.keys(post), ['id'], { - skipUpdateIfNoValuesChanged: true, - }) .execute(); await queryRunner.commitTransaction(); const result = this.postsRepository.findOne({ @@ -143,6 +141,21 @@ export class PostsService { } } + async patchPost({ + kakaoId, + id, + ...rest + }: IPostsServicePatchPost): Promise { + const postData = await this.existCheck({ id }); + if (postData.userKakaoId != kakaoId) + throw new ForbiddenException('게시글 작성자가 아닙니다.'); + Object.keys(rest).forEach((value) => { + if (rest[value] != null) postData[value] = rest[value]; + }); + await this.fkValidCheck({ posts: postData, passNonEssentail: false }); + return await this.postsRepository.save(postData); + } + async fetchPosts(page: FetchPostsDto): Promise { const postsAndCounts = await this.postsRepository.fetchPosts(page); return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); From aeb23f3285e006904bb4ffde1c0d3c3259289082 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 1 Jun 2024 10:25:32 +0900 Subject: [PATCH 129/236] feat: add node-exporter --- deploy/deploy.sh | 2 ++ deploy/nginx.conf | 11 +++++++++-- src/APIs/posts/dtos/patch-post.dto.ts | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/APIs/posts/dtos/patch-post.dto.ts diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 73ee565..0d27ec6 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -28,6 +28,7 @@ echo -e "\n## Docker build & push ##\n" npm run build +aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG docker push $ECR_URL/$SERVICE_NAME:$DOCKER_TAG @@ -53,6 +54,7 @@ OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT # docker pull & run echo -e "\n## new docker pull & run ##\n" +ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" ssh -i $PEM_PATH $SERVER "docker run --env-file .env.prod -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # memory랑 cpu 사용량 조절 diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 9f8ce0c..4b32f01 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -25,6 +25,14 @@ http { index index.html index.htm; } + location /metrics { + proxy_pass http://localhost:9100/metrics; # Node Exporter의 주소 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/api.blccu.com/fullchain.pem; # managed by Certbot @@ -46,5 +54,4 @@ http { return 404; # managed by Certbot - }} - ~ \ No newline at end of file + }} \ No newline at end of file diff --git a/src/APIs/posts/dtos/patch-post.dto.ts b/src/APIs/posts/dtos/patch-post.dto.ts new file mode 100644 index 0000000..0c5ecba --- /dev/null +++ b/src/APIs/posts/dtos/patch-post.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePostInput } from './create-post.input'; + +export class PatchPostInput extends PartialType(CreatePostInput) {} From fe1b3696c9fc1b506ea3882491d4ed4eecce0e08 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 15:10:53 +0900 Subject: [PATCH 130/236] fix: revise clear-cookie domain --- src/APIs/auth/auth.controller.ts | 37 ++++++++++++++++--- src/APIs/follows/entities/follow.entity.ts | 3 ++ src/APIs/follows/follows.service.ts | 4 +- .../notifications/notifications.service.ts | 7 +--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 08697b5..7614f37 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -85,9 +85,22 @@ export class AuthController { }); return res.send(); } catch (e) { - res.clearCookie('accessToken'); - res.clearCookie('refreshToken'); - res.clearCookie('isLoggedIn'); + const clientDomain = process.env.CLIENT_DOMAIN; + + res.clearCookie('accessToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('refreshToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('isLoggedIn', { httpOnly: false, domain: clientDomain }); + throw new UnauthorizedException(e.message); } } @@ -99,9 +112,21 @@ export class AuthController { @Get('logout') @HttpCode(204) async logout(@Res() res: Response) { - res.clearCookie('accessToken'); - res.clearCookie('refreshToken'); - res.clearCookie('isLoggedIn'); + const clientDomain = process.env.CLIENT_DOMAIN; + + res.clearCookie('accessToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('refreshToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('isLoggedIn', { httpOnly: false, domain: clientDomain }); return res.send(); } } diff --git a/src/APIs/follows/entities/follow.entity.ts b/src/APIs/follows/entities/follow.entity.ts index 07c41d0..ee61911 100644 --- a/src/APIs/follows/entities/follow.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { User } from 'src/APIs/users/entities/user.entity'; import { + Column, Entity, JoinColumn, ManyToOne, @@ -34,10 +35,12 @@ export class Follow { from_user: User; @ApiProperty({ type: Number, description: '이웃 추가를 받은 유저' }) + @Column() @RelationId((follow: Follow) => follow.to_user) toUserKakaoId: number; @ApiProperty({ type: Number, description: '이웃 추가를 한 유저' }) + @Column() @RelationId((follow: Follow) => follow.from_user) fromUserKakaoId: number; } diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index 8a9a358..e0b46a5 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -105,12 +105,14 @@ export class FollowsService { await queryRunner.manager.update(User, toUserData.kakaoId, { follower_count: () => 'follower_count +1', }); + + await queryRunner.commitTransaction(); + console.log('commited'); await this.notificationsService.emitAlarm({ userKakaoId: from_user, targetUserKakaoId: to_user, type: NotType.FOLLOW, }); - await queryRunner.commitTransaction(); return await this.followsRepository.findOne({ where: { id: follow.id } }); } catch (e) { await queryRunner.rollbackTransaction(); diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 6cf9c9c..44783d9 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -54,15 +54,12 @@ export class NotificationsService { type, }: EmitNotiDto): Promise { try { - const executeResult = await this.notificationsRepository.createOne({ + const data = await this.notificationsRepository.save({ userKakaoId, targetUserKakaoId, type, }); - const id = executeResult.identifiers[0].id; - const data = await this.notificationsRepository.findOne({ - where: { id }, - }); + console.log(data); // Redis 큐에 이벤트를 전송 await this.redisQueue.add(this.queueName, data); return data; From 26eb93a55e66bc4b55ec80bec34bedaee039ac5f Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 15:14:19 +0900 Subject: [PATCH 131/236] fix: revise comment select option --- src/APIs/comments/comments.repository.ts | 2 ++ src/APIs/posts/posts.service.ts | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 37546a1..a487a78 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -45,12 +45,14 @@ export class CommentsRepository extends Repository { 'u.username', 'u.description', 'u.profile_image', + 'u.handle', ]) .addSelect([ 'childrenUser.kakaoId', 'childrenUser.username', 'childrenUser.description', 'childrenUser.profile_image', + 'childrenUser.handle', ]) .leftJoinAndSelect('c.children', 'children') .leftJoin('children.user', 'childrenUser') diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 67711d2..7cfd7c6 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -261,14 +261,8 @@ export class PostsService { }: IPostsServiceFetchPostsCursor): Promise< CustomCursorPageDto > { - // const useCache = - // cursorOption.order === PostsOrderOptionWrap.VIEW && - // cursorOption.take === 10 && - // cursorOption.date_created === DateOption.WEEK && - // cursorOption.sort === SortOption.DESC; const cacheKey = `fetchPostsCursor_${JSON.stringify(cursorOption)}`; - // if (useCache) { const cachedPosts = await this.cacheManager.get>( cacheKey, @@ -276,7 +270,6 @@ export class PostsService { if (cachedPosts) { return cachedPosts; } - // } let date_filter: Date; if (cursorOption.date_created) @@ -286,9 +279,7 @@ export class PostsService { date_filter, }); const result = await this.createCursorResponse({ posts, cursorOption }); - // if (useCache) { await this.cacheManager.set(cacheKey, result, 180000); - // } return result; } From 405f419616394dffdd8bc8dddf3902a89f1dfc1f Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 16:41:39 +0900 Subject: [PATCH 132/236] feat: delete user api --- .../agreements/entities/agreement.entity.ts | 2 +- src/APIs/auth/auth.controller.ts | 3 + src/APIs/auth/auth.service.ts | 6 +- src/APIs/users/entities/user.entity.ts | 5 ++ .../interfaces/users.service.interface.ts | 4 + src/APIs/users/users.controller.ts | 34 +++++++- src/APIs/users/users.service.ts | 79 +++++++++++++++++++ 7 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 12fb5d0..7d3557f 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; +import { IsBoolean, IsEnum } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; import { AgreementType } from 'src/common/enums/agreement-type.enum'; import { diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 7614f37..386170c 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -18,6 +18,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; @ApiTags('인증 API') @Controller('auth') @@ -109,6 +110,8 @@ export class AuthController { summary: '로그아웃(clear cookie)', description: '클라이언트의 로그인 관련 쿠키를 초기화한다.', }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) @Get('logout') @HttpCode(204) async logout(@Res() res: Response) { diff --git a/src/APIs/auth/auth.service.ts b/src/APIs/auth/auth.service.ts index 72fd9ed..5d2a77d 100644 --- a/src/APIs/auth/auth.service.ts +++ b/src/APIs/auth/auth.service.ts @@ -21,11 +21,15 @@ export class AuthService { } async kakaoValidateUser(kakaoUserDto: KakaoUserDto) { - let user = await this.usersService.findUserByKakaoId(kakaoUserDto); // 유저 조회 + let user = + await this.usersService.findUserByKakaoIdWithDelete(kakaoUserDto); // 유저 조회 if (!user) { // 회원 가입 로직 user = await this.usersService.create(kakaoUserDto); } + if (user.date_deleted != null) { + await this.usersService.activateUser({ kakaoId: kakaoUserDto.kakaoId }); + } return user; } diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 081d96a..58b7ffb 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -1,9 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Agreement } from 'src/APIs/agreements/entities/agreement.entity'; import { Column, CreateDateColumn, DeleteDateColumn, Entity, + OneToMany, // PrimaryColumn, // PrimaryGeneratedColumn, } from 'typeorm'; @@ -67,4 +69,7 @@ export class User { @DeleteDateColumn() @ApiProperty({ description: '삭제된 날짜', type: Date }) date_deleted: Date; + + // @OneToMany(() => Agreement, (agreement) => agreement.user) + // agreements: Agreement[]; } diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index a69b396..dfacbfc 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -8,6 +8,10 @@ export interface IUsersServiceFindUserByKakaoId { kakaoId: number; } +export interface IUsersServiceDelete { + kakaoId: number; +} + export interface IUsersServiceFindUserByHandle extends Pick {} export interface IUsersServiceFindUser { diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index ba2e4a6..73cb46d 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -1,12 +1,14 @@ import { Body, Controller, + Delete, Get, HttpCode, Param, Patch, Post, Req, + Res, UploadedFile, UseGuards, UseInterceptors, @@ -17,11 +19,12 @@ import { ApiConsumes, ApiCookieAuth, ApiCreatedResponse, + ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { Request } from 'express'; +import { Request, Response } from 'express'; import { UserResponseDto, UserResponseDtoWithFollowing, @@ -186,4 +189,33 @@ export class UsersController { file, }); } + + @ApiOperation({ + summary: '회원 탈퇴(soft delete)', + description: '회원을 탈퇴하고 연동된 게시글과 댓글을 soft delete한다.', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @ApiNoContentResponse() + @HttpCode(204) + @Delete('me') + async deleteUser(@Req() req: Request, @Res() res: Response) { + const kakaoId = req.user.userId; + const clientDomain = process.env.CLIENT_DOMAIN; + await this.usersService.delete({ kakaoId }); + res.clearCookie('accessToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('refreshToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('isLoggedIn', { httpOnly: false, domain: clientDomain }); + return res.send(); + } } diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 5f527f6..9527e64 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { IUsersServiceCreate, + IUsersServiceDelete, IUsersServiceFindUserByHandle, IUsersServiceFindUserByKakaoId, } from './interfaces/users.service.interface'; @@ -19,6 +20,11 @@ import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; import { UsersRepository } from './users.repository'; +import { DataSource, UpdateResult } from 'typeorm'; +import { Posts } from '../posts/entities/posts.entity'; +import { Follow } from '../follows/entities/follow.entity'; +import { User } from './entities/user.entity'; +import { Comment } from '../comments/entities/comment.entity'; @Injectable() export class UsersService { @@ -26,6 +32,7 @@ export class UsersService { private readonly usersRepository: UsersRepository, private readonly awsService: AwsService, private readonly utilsService: UtilsService, + private readonly dataSource: DataSource, ) {} // 배포 때 삭제 !!!! @@ -68,6 +75,24 @@ export class UsersService { return result; } + async findUserByKakaoIdWithDelete({ + kakaoId, + }: IUsersServiceFindUserByKakaoId): Promise { + const result = await this.usersRepository.findOne({ + select: USER_SELECT_OPTION, + where: { kakaoId: kakaoId }, + withDeleted: true, // 소프트 삭제된 사용자도 포함 + }); + return result; + } + + async activateUser({ kakaoId }): Promise { + return await this.usersRepository.update( + { kakaoId: kakaoId }, + { date_deleted: null }, + ); + } + async findUserByHandle({ handle, }: IUsersServiceFindUserByHandle): Promise { @@ -170,4 +195,58 @@ export class UsersService { ); return { image_url }; } + + async delete({ kakaoId }: IUsersServiceDelete): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + // 연동된 게시글 soft delete + await queryRunner.manager.softDelete(Posts, { userKakaoId: kakaoId }); + // 연동된 댓글 soft delete + await queryRunner.manager.softDelete(Comment, { userKakaoId: kakaoId }); + // 팔로우 일괄 취소 + const followingsToDelete = await queryRunner.manager.find(Follow, { + where: { fromUserKakaoId: kakaoId }, + }); + await queryRunner.manager.delete(Follow, { fromUserKakaoId: kakaoId }); + await queryRunner.manager.update( + User, + { kakaoId }, + { + following_count: 0, + follower_count: 0, + }, + ); + for (const following of followingsToDelete) { + await queryRunner.manager.decrement( + User, + { kakaoId: following.toUserKakaoId }, + 'follower_count', + 1, + ); + } + // 팔로잉 일괄 취소 + const followersToDelete = await queryRunner.manager.find(Follow, { + where: { toUserKakaoId: kakaoId }, + }); + await queryRunner.manager.delete(Follow, { toUserKakaoId: kakaoId }); + for (const following of followersToDelete) { + await queryRunner.manager.decrement( + User, + { kakaoId: following.fromUserKakaoId }, + 'following_count', + 1, + ); + } + await queryRunner.manager.softDelete(User, { kakaoId }); + await queryRunner.commitTransaction(); + return; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } } From 00f24dc9a01df68700180fa7d66a4e3b2305c6af Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 22:44:44 +0900 Subject: [PATCH 133/236] feat: add feedback type column --- src/APIs/auth/auth.controller.ts | 3 ++- src/APIs/feedbacks/entities/feedback.entity.ts | 8 +++++++- src/APIs/feedbacks/feedbacks.controller.ts | 7 ++++++- src/APIs/feedbacks/feedbacks.module.ts | 2 +- src/APIs/feedbacks/feedbacks.service.ts | 2 ++ .../interfaces/feedbacks.service.interface.ts | 3 ++- src/APIs/users/dtos/delete-user.dto.ts | 5 +++++ src/APIs/users/interfaces/users.service.interface.ts | 3 +++ src/APIs/users/users.controller.ts | 9 +++++++-- src/APIs/users/users.service.ts | 10 ++++++++-- src/common/enums/feedback-type.enum.ts | 6 ++++++ 11 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 src/APIs/users/dtos/delete-user.dto.ts create mode 100644 src/common/enums/feedback-type.enum.ts diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 386170c..01263f8 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, HttpCode, + Post, Req, Res, UnauthorizedException, @@ -112,7 +113,7 @@ export class AuthController { }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @Get('logout') + @Post('logout') @HttpCode(204) async logout(@Res() res: Response) { const clientDomain = process.env.CLIENT_DOMAIN; diff --git a/src/APIs/feedbacks/entities/feedback.entity.ts b/src/APIs/feedbacks/entities/feedback.entity.ts index d1a6b72..5c3b148 100644 --- a/src/APIs/feedbacks/entities/feedback.entity.ts +++ b/src/APIs/feedbacks/entities/feedback.entity.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsString } from 'class-validator'; +import { IsEnum, IsNumber, IsString } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; +import { FeedbackType } from 'src/common/enums/feedback-type.enum'; import { Column, CreateDateColumn, @@ -33,6 +34,11 @@ export class Feedback { }) user: User; + @ApiProperty({ description: '피드백 종류', type: 'enum', enum: FeedbackType }) + @IsEnum(FeedbackType) + @Column() + type: FeedbackType; + @IsNumber() @ApiProperty({ type: Number, diff --git a/src/APIs/feedbacks/feedbacks.controller.ts b/src/APIs/feedbacks/feedbacks.controller.ts index d8b094a..813e74d 100644 --- a/src/APIs/feedbacks/feedbacks.controller.ts +++ b/src/APIs/feedbacks/feedbacks.controller.ts @@ -11,6 +11,7 @@ import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { CreateFeedbackInput } from './dtos/create-feedback.dto'; import { Request } from 'express'; import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; +import { FeedbackType } from 'src/common/enums/feedback-type.enum'; @ApiTags('유저 API') @Controller('users') @@ -27,7 +28,11 @@ export class FeedbacksController { @Req() req: Request, ): Promise { const kakaoId = req.user.userId; - return await this.feedbacksService.create({ ...body, kakaoId }); + return await this.feedbacksService.create({ + ...body, + kakaoId, + type: FeedbackType.GENERAL_FEEDBACK, + }); } @ApiTags('어드민 API') diff --git a/src/APIs/feedbacks/feedbacks.module.ts b/src/APIs/feedbacks/feedbacks.module.ts index 95856a2..1c2efcd 100644 --- a/src/APIs/feedbacks/feedbacks.module.ts +++ b/src/APIs/feedbacks/feedbacks.module.ts @@ -10,6 +10,6 @@ import { UsersModule } from '../users/users.module'; imports: [TypeOrmModule.forFeature([Feedback]), UsersModule], controllers: [FeedbacksController], providers: [FeedbacksService, FeedbacksRepository], - exports: [], + exports: [FeedbacksService], }) export class FeedbacksModule {} diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts index 2564209..134257d 100644 --- a/src/APIs/feedbacks/feedbacks.service.ts +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -17,8 +17,10 @@ export class FeedbacksService { async create({ kakaoId, content, + type, }: IFeedbacksServiceCreate): Promise { return await this.feedbacksRepository.save({ + type, content, userKakaoId: kakaoId, }); diff --git a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts index 15bec31..2704bd4 100644 --- a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts +++ b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts @@ -1,6 +1,7 @@ import { Feedback } from '../entities/feedback.entity'; -export interface IFeedbacksServiceCreate extends Pick { +export interface IFeedbacksServiceCreate + extends Pick { kakaoId: number; } diff --git a/src/APIs/users/dtos/delete-user.dto.ts b/src/APIs/users/dtos/delete-user.dto.ts new file mode 100644 index 0000000..3b37ac9 --- /dev/null +++ b/src/APIs/users/dtos/delete-user.dto.ts @@ -0,0 +1,5 @@ +import { PickType } from '@nestjs/swagger'; + +import { Feedback } from 'src/APIs/feedbacks/entities/feedback.entity'; + +export class DeleteUserInput extends PickType(Feedback, ['content', 'type']) {} diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index dfacbfc..1d8162f 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -1,3 +1,4 @@ +import { FeedbackType } from 'src/common/enums/feedback-type.enum'; import { User } from '../entities/user.entity'; export interface IUsersServiceCreate { @@ -9,6 +10,8 @@ export interface IUsersServiceFindUserByKakaoId { } export interface IUsersServiceDelete { + type: FeedbackType; + content: string; kakaoId: number; } diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 73cb46d..d2d8780 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -34,6 +34,7 @@ import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { DeleteUserInput } from './dtos/delete-user.dto'; @ApiTags('유저 API') @Controller('users') @@ -199,10 +200,14 @@ export class UsersController { @ApiNoContentResponse() @HttpCode(204) @Delete('me') - async deleteUser(@Req() req: Request, @Res() res: Response) { + async deleteUser( + @Req() req: Request, + @Res() res: Response, + @Body() body: DeleteUserInput, + ) { const kakaoId = req.user.userId; const clientDomain = process.env.CLIENT_DOMAIN; - await this.usersService.delete({ kakaoId }); + await this.usersService.delete({ kakaoId, ...body }); res.clearCookie('accessToken', { httpOnly: true, domain: clientDomain, diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 9527e64..676d076 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -25,6 +25,7 @@ import { Posts } from '../posts/entities/posts.entity'; import { Follow } from '../follows/entities/follow.entity'; import { User } from './entities/user.entity'; import { Comment } from '../comments/entities/comment.entity'; +import { Feedback } from '../feedbacks/entities/feedback.entity'; @Injectable() export class UsersService { @@ -196,12 +197,17 @@ export class UsersService { return { image_url }; } - async delete({ kakaoId }: IUsersServiceDelete): Promise { + async delete({ kakaoId, type, content }: IUsersServiceDelete): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - // 연동된 게시글 soft delete + // 피드백 생성 + await queryRunner.manager.save(Feedback, { + type, + content, + userKakaoId: kakaoId, + }); await queryRunner.manager.softDelete(Posts, { userKakaoId: kakaoId }); // 연동된 댓글 soft delete await queryRunner.manager.softDelete(Comment, { userKakaoId: kakaoId }); diff --git a/src/common/enums/feedback-type.enum.ts b/src/common/enums/feedback-type.enum.ts new file mode 100644 index 0000000..a3b9edb --- /dev/null +++ b/src/common/enums/feedback-type.enum.ts @@ -0,0 +1,6 @@ +export enum FeedbackType { + TOO_MANY_ERRORS = 'TOO_MANY_ERRORS', // 과도한 오류 + REJOIN_AFTER_DEACTIVATION = 'REJOIN_AFTER_DEACTIVATION', // 탈퇴 후 재가입 + OTHER_ISSUES = 'OTHER_ISSUES', // 기타 문제 + GENERAL_FEEDBACK = 'GENERAL_FEEDBACK', // 일반 피드백 +} From ccf0356a29462400779b86e2b196ef48728df8f0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 23:01:30 +0900 Subject: [PATCH 134/236] feat: orm migration init --- package-lock.json | 6 +++--- package.json | 1 + src/app.module.ts | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d88fbb1..6b18359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9289,9 +9289,9 @@ "dev": true }, "node_modules/mysql2": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", - "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz", + "integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", diff --git a/package.json b/package.json index 2291156..6ae4bf3 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { + "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", diff --git a/src/app.module.ts b/src/app.module.ts index 2b5e0ab..cd4fa66 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -73,6 +73,11 @@ import { HttpModule } from '@nestjs/axios'; password: configService.get('DATABASE_PASSWORD'), database: configService.get('DATABASE_DATABASE'), entities: [__dirname + '/APIs/**/*.entity.*'], + migrations: ['dist/migrations/*{.ts,.js}'], // migration 수행할 파일 + cli: { + migrationsDir: 'src/migrations', // migration 파일을 생성할 디렉토리 + }, + migrationsTableName: 'migrations', // migration 내용이 기록될 테이블명(default = migration), synchronize: parseBoolean( configService.get('DATABASE_SYNCHRO'), ), From 3b17003c5c382709c0215ce3d32b3f312081ba5e Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 23:04:22 +0900 Subject: [PATCH 135/236] fix: add fetch without delete option --- src/APIs/posts/posts.repository.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 4acfbed..8626888 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -41,6 +41,7 @@ export class PostsRepository extends Repository { ]) .where('p.isPublished = true') .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) + .andWhere('p.date_deleted IS NULL') //sql injection 방지를 위해 반드시 enum 거칠 것 .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { search: `%${page.search}%`, @@ -69,6 +70,7 @@ export class PostsRepository extends Repository { ]) .where('p.id = :id', { id }) .andWhere('p.scope IN (:scope)', { scope }) + .andWhere('p.date_deleted IS NULL') .getOne(); } async fetchPostForUpdate(id) { @@ -84,6 +86,7 @@ export class PostsRepository extends Repository { 'user.username', ]) .where('p.id = :id', { id }) + .andWhere('p.date_deleted IS NULL') .getOne(); } @@ -100,6 +103,7 @@ export class PostsRepository extends Repository { 'user.username', ]) .where(`p.userKakaoId = any(${subQuery})`) + .andWhere('p.date_deleted IS NULL') .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC], }) //sql injection 방지를 위해 만드시 enum 거칠 것 @@ -130,6 +134,7 @@ export class PostsRepository extends Repository { ]) .where('p.userKakaoId = :kakaoId', { kakaoId }) .andWhere(`p.isPublished = false`) + .andWhere('p.date_deleted IS NULL') .orderBy('p.id', 'DESC') .getMany(); } @@ -159,6 +164,7 @@ export class PostsRepository extends Repository { .andWhere(queryByOrderSort, { customCursor: cursor, }) + .andWhere('p.date_deleted IS NULL') .orderBy(`p.${_order}`, sort as any) .addOrderBy('p.id', sort as any); From 9e18315d95b22af4ee0d5ed89d2cd6852751b217 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 23:11:36 +0900 Subject: [PATCH 136/236] fix: fetching onboarding contract open scope --- src/APIs/agreements/agreements.controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index f1897fe..da2fe01 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -30,8 +30,6 @@ export class AgreementsController { constructor(private readonly agreementsService: AgreementsService) {} @ApiOperation({ summary: 'contract fetch' }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) @Get('contracts') async fetchContract(@Query() query: FetchContractDto) { const data = await this.agreementsService.fetchContract({ ...query }); From 3671e7208af87d6e0fc273f4471f9bc4f6599300 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 2 Jun 2024 23:22:09 +0900 Subject: [PATCH 137/236] fix: profile/background image's ext to jpg --- src/APIs/users/users.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 676d076..d844c46 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -187,8 +187,7 @@ export class UsersService { file: Express.Multer.File, ): Promise { const imageName = this.utilsService.getUUID(); - const ext = file.originalname.split('.').pop(); - + const ext = 'jpg'; const image_url = await this.awsService.imageUploadToS3( `${imageName}.${ext}`, file, From 0d17ab253e97c6f78d9bf7b43d2d8420badb419f Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 3 Jun 2024 10:07:15 +0900 Subject: [PATCH 138/236] fix: change exception to logger on s3 delete api --- src/utils/aws/aws.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index 326a000..a3fa515 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -1,5 +1,5 @@ // aws.service.ts -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DeleteObjectCommand, @@ -12,7 +12,10 @@ import sharp from 'sharp'; export class AwsService { s3Client: S3Client; - constructor(private configService: ConfigService) { + constructor( + private configService: ConfigService, + private readonly logger = new Logger(AwsService.name), + ) { // AWS S3 클라이언트 초기화. 환경 설정 정보를 사용하여 AWS 리전, Access Key, Secret Key를 설정. this.s3Client = new S3Client({ region: this.configService.get('AWS_REGION'), // AWS Region @@ -49,7 +52,7 @@ export class AwsService { const command = new DeleteObjectCommand(deleteParams); return await this.s3Client.send(command); } catch (e) { - throw new BadRequestException('존재하지 않거나 적합하지 않은 url입니다.'); + this.logger.error('Error deleting object from S3', e.stack); } } @@ -58,7 +61,7 @@ export class AwsService { file: Express.Multer.File, // 업로드할 파일 ext: string, // 파일 확장자 ) { - const resizedImageBuffer = await this.resizeImage(file.buffer, 800); + const resizedImageBuffer = await this.resizeImage(file.buffer, 1200); // AWS S3에 이미지 업로드 명령을 생성합니다. 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정합니다. const command = new PutObjectCommand({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 From 2ddd35822708111ae4df3327ade3a4bf136e8120 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 3 Jun 2024 10:11:13 +0900 Subject: [PATCH 139/236] feat: delete image from s3 when patch profile&background image --- src/APIs/users/users.service.ts | 2 ++ src/utils/aws/aws.service.ts | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index d844c46..7df8c70 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -165,6 +165,7 @@ export class UsersService { }); const { image_url } = await this.saveImage(file); await this.usersRepository.save({ ...user, profile_image: image_url }); + await this.awsService.deleteImageFromS3({ url: user.profile_image }); return { image_url }; } async saveImage(file: Express.Multer.File): Promise { @@ -180,6 +181,7 @@ export class UsersService { }); const { image_url } = await this.saveImage(file); await this.usersRepository.save({ ...user, background_image: image_url }); + await this.awsService.deleteImageFromS3({ url: user.background_image }); return { image_url }; } diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index a3fa515..add4761 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -11,11 +11,9 @@ import sharp from 'sharp'; @Injectable() export class AwsService { s3Client: S3Client; + private readonly logger = new Logger(AwsService.name); - constructor( - private configService: ConfigService, - private readonly logger = new Logger(AwsService.name), - ) { + constructor(private configService: ConfigService) { // AWS S3 클라이언트 초기화. 환경 설정 정보를 사용하여 AWS 리전, Access Key, Secret Key를 설정. this.s3Client = new S3Client({ region: this.configService.get('AWS_REGION'), // AWS Region From ec9d605390c613ff0c4d7f12312164021a561ff5 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 3 Jun 2024 10:36:43 +0900 Subject: [PATCH 140/236] feat: divide image resize options --- .../postBackgrounds.controller.ts | 12 +++++++----- .../postBackgrounds/postBackgrounds.service.ts | 1 + src/APIs/posts/posts.controller.ts | 2 +- src/APIs/posts/posts.service.ts | 4 +--- src/APIs/stickers/stickers.service.ts | 9 ++++----- .../users/interfaces/users.service.interface.ts | 5 +++++ src/APIs/users/users.service.ts | 16 ++++++++-------- src/utils/aws/aws.service.ts | 3 ++- 8 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/APIs/postBackgrounds/postBackgrounds.controller.ts b/src/APIs/postBackgrounds/postBackgrounds.controller.ts index 3cb511b..cf66922 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.controller.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.controller.ts @@ -22,13 +22,13 @@ import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto import { PostBackground } from './entities/postBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; -@ApiTags('[잠정 사용X] 내지 API') -@Controller('postbg') +@Controller('') export class PostBackgroundsController { constructor( private readonly postBackgroundsService: PostBackgroundsService, ) {} + @ApiTags('어드민 API') @ApiOperation({ summary: '내지 업로드' }) @ApiConsumes('multipart/form-data') @ApiBody({ @@ -39,7 +39,7 @@ export class PostBackgroundsController { description: '이미지 서버에 파일 업로드 완료', type: ImageUploadResponseDto, }) - @Post() + @Post('users/admin/posts/background') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async uploadImage( @@ -49,18 +49,20 @@ export class PostBackgroundsController { return url; } + @ApiTags('게시글 API') @ApiOperation({ summary: '내지 모두 불러오기' }) @ApiOkResponse({ description: '모든 내지 fetch 완료', type: [PostBackground], }) - @Get() + @Get('posts/backgrounds') async fetchAll(): Promise { return await this.postBackgroundsService.fetchAll(); } + @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) - @Delete(':id') + @Delete('users/admin/posts/background/:id') async delete(@Param('id') id: string) { return await this.postBackgroundsService.delete({ id }); } diff --git a/src/APIs/postBackgrounds/postBackgrounds.service.ts b/src/APIs/postBackgrounds/postBackgrounds.service.ts index 0cd80fc..5aa889b 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.service.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.service.ts @@ -28,6 +28,7 @@ export class PostBackgroundsService { `${imageName}.${ext}`, file, ext, + 2000, ); await this.postBackgroundsRepository.save({ image_url }); return { image_url }; diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 540e931..b48a81f 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -146,7 +146,7 @@ export class PostsController { @Req() req: Request, @UploadedFile() file: Express.Multer.File, ): Promise { - return await this.postsService.saveImage(file); + return await this.postsService.imageUpload(file); } // @ApiOperation({ diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 7cfd7c6..70fa957 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -55,9 +55,6 @@ export class PostsService { private readonly followsService: FollowsService, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} - async saveImage(file: Express.Multer.File) { - return await this.imageUpload(file); - } async imageUpload( file: Express.Multer.File, @@ -69,6 +66,7 @@ export class PostsService { `${imageName}.${ext}`, file, ext, + 1200, ); return { image_url }; diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 7ea3313..a0203bc 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -28,9 +28,7 @@ export class StickersService { const data = await this.findStickerById({ id }); if (!data) throw new NotFoundException('스티커를 찾을 수 없습니다.'); } - async saveImage(file: Express.Multer.File): Promise { - return await this.imageUpload(file); - } + async imageUpload( file: Express.Multer.File, ): Promise { @@ -41,6 +39,7 @@ export class StickersService { `${imageName}.${ext}`, file, ext, + 1600, ); return { image_url }; } @@ -48,7 +47,7 @@ export class StickersService { userKakaoId, file, }: CreateStickerDto): Promise { - const { image_url } = await this.saveImage(file); + const { image_url } = await this.imageUpload(file); const insertData = await this.stickersRepository .createQueryBuilder() .insert() @@ -67,7 +66,7 @@ export class StickersService { file, }: CreateStickerDto): Promise { await this.usersService.adminCheck({ kakaoId: userKakaoId }); - const { image_url } = await this.saveImage(file); + const { image_url } = await this.imageUpload(file); const insertData = await this.stickersRepository .createQueryBuilder() .insert() diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index 1d8162f..15d8e89 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -20,3 +20,8 @@ export interface IUsersServiceFindUserByHandle extends Pick {} export interface IUsersServiceFindUser { id: string; } + +export interface IUsersServiceImageUpload { + file: Express.Multer.File; + resize: number; +} diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 7df8c70..c1097ca 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -9,6 +9,7 @@ import { IUsersServiceDelete, IUsersServiceFindUserByHandle, IUsersServiceFindUserByKakaoId, + IUsersServiceImageUpload, } from './interfaces/users.service.interface'; import { USER_SELECT_OPTION, @@ -163,14 +164,11 @@ export class UsersService { const user = await this.findUserByKakaoIdWithToken({ kakaoId: userKakaoId, }); - const { image_url } = await this.saveImage(file); + const { image_url } = await this.imageUpload({ file, resize: 800 }); await this.usersRepository.save({ ...user, profile_image: image_url }); await this.awsService.deleteImageFromS3({ url: user.profile_image }); return { image_url }; } - async saveImage(file: Express.Multer.File): Promise { - return await this.imageUpload(file); - } async uploadBackgroundImage({ userKakaoId, @@ -179,21 +177,23 @@ export class UsersService { const user = await this.findUserByKakaoIdWithToken({ kakaoId: userKakaoId, }); - const { image_url } = await this.saveImage(file); + const { image_url } = await this.imageUpload({ file, resize: 1600 }); await this.usersRepository.save({ ...user, background_image: image_url }); await this.awsService.deleteImageFromS3({ url: user.background_image }); return { image_url }; } - async imageUpload( - file: Express.Multer.File, - ): Promise { + async imageUpload({ + file, + resize, + }: IUsersServiceImageUpload): Promise { const imageName = this.utilsService.getUUID(); const ext = 'jpg'; const image_url = await this.awsService.imageUploadToS3( `${imageName}.${ext}`, file, ext, + resize, ); return { image_url }; } diff --git a/src/utils/aws/aws.service.ts b/src/utils/aws/aws.service.ts index add4761..7f27722 100644 --- a/src/utils/aws/aws.service.ts +++ b/src/utils/aws/aws.service.ts @@ -58,8 +58,9 @@ export class AwsService { fileName: string, // 업로드될 파일의 이름 file: Express.Multer.File, // 업로드할 파일 ext: string, // 파일 확장자 + resize: number, // 리사이징 크기 ) { - const resizedImageBuffer = await this.resizeImage(file.buffer, 1200); + const resizedImageBuffer = await this.resizeImage(file.buffer, resize); // AWS S3에 이미지 업로드 명령을 생성합니다. 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정합니다. const command = new PutObjectCommand({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 From 567206c6eef3bd9e04691d8aef3ded64267a4eeb Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 10:04:50 +0900 Subject: [PATCH 141/236] feat: add design_licence on agreement --- .../terms/{CUSTOM_AGREEMENT.txt => CUSTOM_AGREEMENT.html} | 0 src/assets/terms/DESIGN_LICENCE.html | 1 + src/assets/terms/MARKETING_CONSENT.html | 3 +++ src/assets/terms/MARKETING_CONSENT.txt | 1 - src/assets/terms/PRIVACY_POLICY.html | 2 ++ src/assets/terms/PRIVACY_POLICY.txt | 1 - .../terms/{TERMS_OF_SERVICE.txt => TERMS_OF_SERVICE.html} | 0 src/common/enums/agreement-type.enum.ts | 1 + 8 files changed, 7 insertions(+), 2 deletions(-) rename src/assets/terms/{CUSTOM_AGREEMENT.txt => CUSTOM_AGREEMENT.html} (100%) create mode 100644 src/assets/terms/DESIGN_LICENCE.html create mode 100644 src/assets/terms/MARKETING_CONSENT.html delete mode 100644 src/assets/terms/MARKETING_CONSENT.txt create mode 100644 src/assets/terms/PRIVACY_POLICY.html delete mode 100644 src/assets/terms/PRIVACY_POLICY.txt rename src/assets/terms/{TERMS_OF_SERVICE.txt => TERMS_OF_SERVICE.html} (100%) diff --git a/src/assets/terms/CUSTOM_AGREEMENT.txt b/src/assets/terms/CUSTOM_AGREEMENT.html similarity index 100% rename from src/assets/terms/CUSTOM_AGREEMENT.txt rename to src/assets/terms/CUSTOM_AGREEMENT.html diff --git a/src/assets/terms/DESIGN_LICENCE.html b/src/assets/terms/DESIGN_LICENCE.html new file mode 100644 index 0000000..247bc4c --- /dev/null +++ b/src/assets/terms/DESIGN_LICENCE.html @@ -0,0 +1 @@ +디자인 라이센스 \ No newline at end of file diff --git a/src/assets/terms/MARKETING_CONSENT.html b/src/assets/terms/MARKETING_CONSENT.html new file mode 100644 index 0000000..b323329 --- /dev/null +++ b/src/assets/terms/MARKETING_CONSENT.html @@ -0,0 +1,3 @@ +

html

+marketing consent + diff --git a/src/assets/terms/MARKETING_CONSENT.txt b/src/assets/terms/MARKETING_CONSENT.txt deleted file mode 100644 index dff1857..0000000 --- a/src/assets/terms/MARKETING_CONSENT.txt +++ /dev/null @@ -1 +0,0 @@ -marketing consent \ No newline at end of file diff --git a/src/assets/terms/PRIVACY_POLICY.html b/src/assets/terms/PRIVACY_POLICY.html new file mode 100644 index 0000000..4cb8e31 --- /dev/null +++ b/src/assets/terms/PRIVACY_POLICY.html @@ -0,0 +1,2 @@ +

개인정보처리방침

+privacy policy \ No newline at end of file diff --git a/src/assets/terms/PRIVACY_POLICY.txt b/src/assets/terms/PRIVACY_POLICY.txt deleted file mode 100644 index 8c4196d..0000000 --- a/src/assets/terms/PRIVACY_POLICY.txt +++ /dev/null @@ -1 +0,0 @@ -privacy policy \ No newline at end of file diff --git a/src/assets/terms/TERMS_OF_SERVICE.txt b/src/assets/terms/TERMS_OF_SERVICE.html similarity index 100% rename from src/assets/terms/TERMS_OF_SERVICE.txt rename to src/assets/terms/TERMS_OF_SERVICE.html diff --git a/src/common/enums/agreement-type.enum.ts b/src/common/enums/agreement-type.enum.ts index d3823ef..9a3ddf6 100644 --- a/src/common/enums/agreement-type.enum.ts +++ b/src/common/enums/agreement-type.enum.ts @@ -3,4 +3,5 @@ export enum AgreementType { TERMS_OF_SERVICE = 'TERMS_OF_SERVICE', // 이용약관 MARKETING_CONSENT = 'MARKETING_CONSENT', // 마케팅 수신 동의 CUSTOM_AGREEMENT = 'CUSTOM_AGREEMENT', // 사용자 정의 약관 + DESIGN_LICENCE = 'DESIGN_LICENCE', } From d1ae69947e50ee1b2a21c1e1b309c78b1b2c32e0 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 10:07:08 +0900 Subject: [PATCH 142/236] fix: change agreement reserving file type from txt to html --- src/APIs/agreements/agreements.service.ts | 2 +- src/assets/terms/MARKETING_CONSENT.html | 3 ++- src/assets/terms/PRIVACY_POLICY.html | 1 + src/common/enums/agreement-type.enum.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index eec77f0..b27fb31 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -40,7 +40,7 @@ export class AgreementsService { } async fetchContract({ agreementType }: IAgreementsServiceFetchContract) { - const fileName = agreementType + '.txt'; + const fileName = agreementType + '.html'; const rootPath = process.cwd(); const filePath = path.join(rootPath, 'src', 'assets', 'terms', fileName); const data = await fs.promises.readFile(filePath, 'utf8'); diff --git a/src/assets/terms/MARKETING_CONSENT.html b/src/assets/terms/MARKETING_CONSENT.html index b323329..d5ba4f2 100644 --- a/src/assets/terms/MARKETING_CONSENT.html +++ b/src/assets/terms/MARKETING_CONSENT.html @@ -1,3 +1,4 @@ -

html

+

html 양식

marketing consent +만들어야됨 diff --git a/src/assets/terms/PRIVACY_POLICY.html b/src/assets/terms/PRIVACY_POLICY.html index 4cb8e31..c3f0e37 100644 --- a/src/assets/terms/PRIVACY_POLICY.html +++ b/src/assets/terms/PRIVACY_POLICY.html @@ -1,2 +1,3 @@

개인정보처리방침

+만들어야됨 html형식 정적 리소스 제공 privacy policy \ No newline at end of file diff --git a/src/common/enums/agreement-type.enum.ts b/src/common/enums/agreement-type.enum.ts index 9a3ddf6..452f3ea 100644 --- a/src/common/enums/agreement-type.enum.ts +++ b/src/common/enums/agreement-type.enum.ts @@ -3,5 +3,5 @@ export enum AgreementType { TERMS_OF_SERVICE = 'TERMS_OF_SERVICE', // 이용약관 MARKETING_CONSENT = 'MARKETING_CONSENT', // 마케팅 수신 동의 CUSTOM_AGREEMENT = 'CUSTOM_AGREEMENT', // 사용자 정의 약관 - DESIGN_LICENCE = 'DESIGN_LICENCE', + DESIGN_LICENCE = 'DESIGN_LICENCE', // 디자인 라이센스 } From 9e10b2a8d7f4266a6dd9a28c63b5e8d6ada9fac4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 10:32:00 +0900 Subject: [PATCH 143/236] fix: token refresh logic --- src/APIs/auth/auth.controller.ts | 1 - src/APIs/auth/auth.service.ts | 10 +++++----- src/APIs/users/users.service.ts | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 01263f8..d6ad1a4 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -38,7 +38,6 @@ export class AuthController { @UseGuards(AuthGuard('kakao')) // kakao.strategy를 실행시켜 줍니다. @HttpCode(301) async kakaoLogin(@Req() req: Request, @Res() res: Response) { - // console.log(req.user); const { accessToken, refreshToken } = await this.authService.getJWT({ kakaoId: req.user.kakaoId, }); diff --git a/src/APIs/auth/auth.service.ts b/src/APIs/auth/auth.service.ts index 5d2a77d..876f187 100644 --- a/src/APIs/auth/auth.service.ts +++ b/src/APIs/auth/auth.service.ts @@ -14,7 +14,6 @@ export class AuthService { ) {} async getJWT(kakaoUserDto: KakaoUserDto) { const user = await this.kakaoValidateUser(kakaoUserDto); // 카카오 정보 검증 및 회원가입 로직 - // console.log('[AUTHSERVICE] user:', user); const accessToken = this.generateAccessToken(user); // AccessToken 생성 const refreshToken = await this.generateRefreshToken(user); // refreshToken 생성 return { accessToken, refreshToken }; @@ -46,11 +45,11 @@ export class AuthService { }); const saltOrRounds = 10; const current_refresh_token = await bcrypt.hash(refreshToken, saltOrRounds); - - await this.usersService.setCurrentRefreshToken({ + const user = await this.usersService.setCurrentRefreshToken({ kakaoId: payload.userId, current_refresh_token, }); + console.log(user); return refreshToken; } async refresh(refreshToken: string): Promise { @@ -62,8 +61,9 @@ export class AuthService { const kakaoId = decodedRefreshToken.userId; // 데이터베이스에서 User 객체 가져오기 - const user = await this.usersService.findUserByKakaoIdWithToken(kakaoId); - + const user = await this.usersService.findUserByKakaoIdWithToken({ + kakaoId, + }); // 2차 검증 const isRefreshTokenMatching = await bcrypt.compare( refreshToken, diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index c1097ca..69a85d6 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -116,7 +116,7 @@ export class UsersService { async setCurrentRefreshToken({ kakaoId, current_refresh_token }) { const user = await this.findUserByKakaoId({ kakaoId }); - this.usersRepository.save({ + return await this.usersRepository.save({ ...user, current_refresh_token, }); From fdb426bf6369be238b06a77f2aad89f3b869386b Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 11:00:02 +0900 Subject: [PATCH 144/236] fix: comment delete logic --- src/APIs/comments/comments.service.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 802b24a..c2bbb62 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -116,18 +116,25 @@ export class CommentsService { }: ICommentsServiceDelete): Promise { await this.dataSource.transaction(async (manager: EntityManager) => { const data = await this.existCheck({ id }); + let childrenData = null; if (data.postsId !== postsId) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); } - - await manager.softRemove(Comment, { - user: { kakaoId: userKakaoId }, - id, - }); - - await manager.update(Posts, data.postsId, { - comment_count: () => 'comment_count - 1', - }); + if (data.parentId == null) + childrenData = await manager.find(Comment, { + where: { parentId: data.id }, + }); + if (childrenData != null) { + await manager.delete(Comment, { user: { kakaoId: userKakaoId }, id }); + await manager.update(Posts, data.postsId, { + comment_count: () => 'comment_count - 1', + }); + } else { + await manager.softRemove(Comment, { + user: { kakaoId: userKakaoId }, + id, + }); + } }); } } From c496f0066043f90b7709c5257ffc3f3f5d75d665 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 11:12:47 +0900 Subject: [PATCH 145/236] fix: comment delete logic --- src/APIs/comments/comments.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index c2bbb62..d39033b 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -116,7 +116,7 @@ export class CommentsService { }: ICommentsServiceDelete): Promise { await this.dataSource.transaction(async (manager: EntityManager) => { const data = await this.existCheck({ id }); - let childrenData = null; + let childrenData = []; if (data.postsId !== postsId) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); } @@ -124,7 +124,7 @@ export class CommentsService { childrenData = await manager.find(Comment, { where: { parentId: data.id }, }); - if (childrenData != null) { + if (childrenData.length == 0) { await manager.delete(Comment, { user: { kakaoId: userKakaoId }, id }); await manager.update(Posts, data.postsId, { comment_count: () => 'comment_count - 1', From 7b29380ab8c582d89fc485b3a06d147d7b468d08 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 11:31:49 +0900 Subject: [PATCH 146/236] feat: delete images & stickerBlocks when deleting post --- src/APIs/posts/posts.service.ts | 16 ++++++++ .../entities/stickerblock.entity.ts | 20 ++++------ .../stickerBlocks/stickerBlocks.service.ts | 13 +++++++ src/APIs/stickers/stickers.service.ts | 37 ++++++++----------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 70fa957..f247698 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -212,10 +212,26 @@ export class PostsService { } async softDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { + const data = await this.postsRepository.findOne({ + where: { user: { kakaoId }, id }, + }); + if (data) { + await this.awsService.deleteImageFromS3({ url: data.image_url }); + await this.awsService.deleteImageFromS3({ url: data.main_image_url }); + await this.stickerBlocksService.deleteBlocks({ kakaoId, postsId: id }); + } return await this.postsRepository.softDelete({ user: { kakaoId }, id }); } async hardDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { + const data = await this.postsRepository.findOne({ + where: { user: { kakaoId }, id }, + }); + if (data) { + await this.awsService.deleteImageFromS3({ url: data.image_url }); + await this.awsService.deleteImageFromS3({ url: data.main_image_url }); + await this.stickerBlocksService.deleteBlocks({ kakaoId, postsId: id }); + } return await this.postsRepository.delete({ user: { kakaoId }, id }); } diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 0a2a444..2d8766b 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -42,26 +42,22 @@ export class StickerBlock { }) posts: Posts; - @ApiProperty({ description: '스티커의 width', type: Number }) + @ApiProperty({ description: '스티커의 posX', type: Number }) @Column({ type: 'float' }) - width: number; + posX: number; - @ApiProperty({ description: '스티커의 top', type: Number }) + @ApiProperty({ description: '스티커의 posY', type: Number }) @Column({ type: 'float' }) - top: number; - - @ApiProperty({ description: '스티커의 left', type: Number }) - @Column({ type: 'float' }) - left: number; - - @ApiProperty({ description: '스티커의 rotate', type: Number }) - @Column({ type: 'float' }) - rotate: number; + posY: number; @ApiProperty({ description: '스티커의 scale', type: Number }) @Column({ type: 'float' }) scale: number; + @ApiProperty({ description: '스티커의 angle', type: Number }) + @Column({ type: 'float' }) + angle: number; + @ApiProperty({ description: '스티커의 zindex', type: Number }) @Column({ type: 'float' }) zindex: number; diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 9f2f045..a204421 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -38,5 +38,18 @@ export class StickerBlocksService { }); } + async deleteBlocks({ kakaoId, postsId }): Promise { + const blocksToDelete = await this.stickerBlocksRepository.find({ + relations: ['sticker'], + where: { postsId }, + }); + for (const block of blocksToDelete) { + if (block.sticker.isReusable === false) + await this.stickersService.delete({ kakaoId, id: block.id }); + await this.stickerBlocksRepository.remove(block); + } + return; + } + async updateBlock() {} } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index a0203bc..f5f9bbe 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -7,7 +7,6 @@ import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; -import { FindStickerDto } from './dtos/find-sticker.dto'; import { UpdateStickerDto } from './dtos/update-sticker.dto'; @Injectable() @@ -107,33 +106,29 @@ export class StickersService { throw new NotFoundException( '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', ); - const data = await this.stickersRepository.findOne({ where: { id } }); - if (isReusable) data.isReusable = isReusable; + if (isReusable) sticker.isReusable = isReusable; if (image_url) { - await this.removeFromS3({ id, kakaoId }); - data.image_url = image_url; + await this.awsService.deleteImageFromS3({ url: sticker.image_url }); + sticker.image_url = image_url; } - const result = await this.stickersRepository.save(data); + const result = await this.stickersRepository.save(sticker); return result; } catch (e) { throw e; } } - async removeFromS3({ id, kakaoId }: FindStickerDto) { - try { - const sticker = await this.stickersRepository.findOne({ - where: { id, user: { kakaoId } }, - }); - if (!sticker) - throw new NotFoundException( - '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', - ); - return await this.awsService.deleteImageFromS3({ - url: sticker.image_url, - }); - } catch (e) { - throw e; - } + async delete({ id, kakaoId }) { + const sticker = await this.stickersRepository.findOne({ + where: { id, user: { kakaoId } }, + }); + if (!sticker) + throw new NotFoundException( + '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', + ); + await this.awsService.deleteImageFromS3({ + url: sticker.image_url, + }); + return await this.stickersRepository.remove(sticker); } } From 85bb6163d911fb0d2a7d702cf5648eb123b45927 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 11:39:54 +0900 Subject: [PATCH 147/236] feat: delete sticker api --- src/APIs/stickers/stickers.controller.ts | 15 +++++++++++++++ src/APIs/stickers/stickers.service.ts | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index aa6044f..cbbc894 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, HttpCode, Param, @@ -17,6 +18,7 @@ import { ApiConsumes, ApiCookieAuth, ApiCreatedResponse, + ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -140,4 +142,17 @@ export class StickersController { file, }); } + + @ApiOperation({ summary: '스티커 삭제', description: '스티커를 삭제한다.' }) + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @ApiNoContentResponse({ description: '삭제 성공' }) + @Delete('stickers/:id') + async deleteSticker( + @Req() req: Request, + @Param('id') id: number, + ): Promise { + const kakaoId = req.user.userId; + return await this.stickersService.delete({ id, kakaoId }); + } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index f5f9bbe..accc420 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -118,7 +118,7 @@ export class StickersService { } } - async delete({ id, kakaoId }) { + async delete({ id, kakaoId }): Promise { const sticker = await this.stickersRepository.findOne({ where: { id, user: { kakaoId } }, }); @@ -129,6 +129,7 @@ export class StickersService { await this.awsService.deleteImageFromS3({ url: sticker.image_url, }); - return await this.stickersRepository.remove(sticker); + await this.stickersRepository.remove(sticker); + return; } } From 781bb438b2f2a52d6dd5f8bdc919334b0e9a7c06 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 4 Jun 2024 18:41:33 +0900 Subject: [PATCH 148/236] fix: select children comments which is not soft deleted --- src/APIs/comments/comments.repository.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index a487a78..bddefed 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -54,7 +54,11 @@ export class CommentsRepository extends Repository { 'childrenUser.profile_image', 'childrenUser.handle', ]) - .leftJoinAndSelect('c.children', 'children') + .leftJoinAndSelect( + 'c.children', + 'children', + 'children.date_deleted IS NOT NULL', + ) .leftJoin('children.user', 'childrenUser') .where('c.postsId = :postsId', { postsId }) .andWhere('c.parentId IS NULL') From 61888b6db75d0ec203408b2dcce1458e55f6de34 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 01:30:02 +0900 Subject: [PATCH 149/236] feat: change username searching algorithm to fulltext idx search --- src/APIs/users/entities/user.entity.ts | 4 ++-- src/APIs/users/users.repository.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 58b7ffb..0057ee6 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -1,11 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Agreement } from 'src/APIs/agreements/entities/agreement.entity'; import { Column, CreateDateColumn, DeleteDateColumn, Entity, - OneToMany, + Index, // PrimaryColumn, // PrimaryGeneratedColumn, } from 'typeorm'; @@ -46,6 +45,7 @@ export class User { }) follower_count: number; + @Index({ fulltext: true, parser: 'ngram' }) @Column({ unique: true }) @ApiProperty({ description: '유저 이름', type: String }) username: string; diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index 1e8dcd0..d18c837 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -45,10 +45,12 @@ export class UsersRepository extends Repository { async fetchUsersWithNameAndFollowing({ kakaoId, username }) { const queryBuilder = this.getFollowQuery({ kakaoId }); const users = await queryBuilder - .andWhere('LOWER(user.username) LIKE LOWER(:username)', { - username: `%${username}%`, + .andWhere('MATCH(user.username) AGAINST (:username IN BOOLEAN MODE)', { + username: `*${username}*`, }) - .setParameters({ username: `%${username}%` }) + // .andWhere('LOWER(user.username) LIKE LOWER(:username)', { + // username: `%${username}%`, + // }) .getRawMany(); return users.map((user) => ({ From 3d4956e050e259c8755952eafa3f4e29bd400765 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 16:29:15 +0900 Subject: [PATCH 150/236] fix: comment fetching logic without soft del col --- deploy/rds.setting.txt | 17 +++++++++++++++++ src/APIs/comments/comments.repository.ts | 14 ++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 deploy/rds.setting.txt diff --git a/deploy/rds.setting.txt b/deploy/rds.setting.txt new file mode 100644 index 0000000..d7d9add --- /dev/null +++ b/deploy/rds.setting.txt @@ -0,0 +1,17 @@ +time_zone = Asia/Seoul + +charactercharacter_set_client = utf8mb4 +character_set_connection = utf8mb4 +character_set_database = utf8mb4 +character_set_filesystem = utf8mb4 +character_set_results = utf8mb4 +character_set_server = utf8mb4 + +ngram_token_size = 1 +innodb_ft_enable_stopword = 0 + +collation_connection = utf8mb4_general_ci +collation_server = utf8mb4_general_ci + + +alter table user add fulltext ft_idx_username (username) with PARSER ngram; \ No newline at end of file diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index bddefed..ff85ca9 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -37,7 +37,7 @@ export class CommentsRepository extends Repository { async fetchComments({ postsId, }: ICommentsRepositoryfetchComments): Promise { - return await this.createQueryBuilder('c') + const comments = await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') .addSelect([ @@ -54,16 +54,18 @@ export class CommentsRepository extends Repository { 'childrenUser.profile_image', 'childrenUser.handle', ]) - .leftJoinAndSelect( - 'c.children', - 'children', - 'children.date_deleted IS NOT NULL', - ) + .leftJoinAndSelect('c.children', 'children') .leftJoin('children.user', 'childrenUser') .where('c.postsId = :postsId', { postsId }) .andWhere('c.parentId IS NULL') .orderBy('c.date_created', 'ASC') .addOrderBy('children.date_created', 'ASC') .getMany(); + comments.forEach((comment) => { + comment.children = comment.children.filter( + (child) => child.date_deleted !== null, + ); + }); + return comments; } } From f20ad10e7411fe9347d387215ffa7c77ab6b5aa9 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 17:53:45 +0900 Subject: [PATCH 151/236] fix: inner filtering comments --- src/APIs/comments/comments.repository.ts | 3 ++- src/APIs/comments/comments.service.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index ff85ca9..2a130b8 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -61,9 +61,10 @@ export class CommentsRepository extends Repository { .orderBy('c.date_created', 'ASC') .addOrderBy('children.date_created', 'ASC') .getMany(); + comments.forEach((comment) => { comment.children = comment.children.filter( - (child) => child.date_deleted !== null, + (child) => child.date_deleted === null, ); }); return comments; diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index d39033b..c751085 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -6,7 +6,7 @@ import { } from '@nestjs/common'; import { CreateCommentDto } from './dtos/create-comment.dto'; import { CommentsRepository } from './comments.repository'; -import { DataSource, EntityManager } from 'typeorm'; +import { DataSource, EntityManager, UpdateResult } from 'typeorm'; import { Posts } from '../posts/entities/posts.entity'; import { ChildrenComment, @@ -117,6 +117,7 @@ export class CommentsService { await this.dataSource.transaction(async (manager: EntityManager) => { const data = await this.existCheck({ id }); let childrenData = []; + let deletedResult: UpdateResult; if (data.postsId !== postsId) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); } @@ -130,10 +131,12 @@ export class CommentsService { comment_count: () => 'comment_count - 1', }); } else { - await manager.softRemove(Comment, { + deletedResult = await manager.softDelete(Comment, { user: { kakaoId: userKakaoId }, id, }); + if (deletedResult.affected < 1) + throw new NotFoundException('삭제할 댓글이 존재하지 않습니다'); } }); } From 9fc3361e857643408e510532ff5215de246ed08e Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 18:41:58 +0900 Subject: [PATCH 152/236] feat: bulkInsert sticker API --- .../dtos/create-stickerBlock.dto.ts | 1 + .../dtos/create-stickerBlocks.dto.ts | 18 +++++++++++ .../entities/stickerblock.entity.ts | 4 +++ .../stickerBlocks/stickerBlocks.controller.ts | 31 +++++++++++++++++-- .../stickerBlocks/stickerBlocks.service.ts | 13 ++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts index 66876a0..8eb0b17 100644 --- a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts +++ b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts @@ -12,4 +12,5 @@ export class CreateStickerBlockInput extends OmitType(StickerBlock, [ export class CreateStickerBlockDto extends CreateStickerBlockInput { postsId: number; stickerId: number; + kakaoId: number; } diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts new file mode 100644 index 0000000..3195001 --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; +import { CreateStickerBlockInput } from './create-stickerBlock.dto'; + +export class CreateStickerBlocksInput { + @ApiProperty({ type: [CreateStickerBlockInput] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateStickerBlockInput) + stickerBlocks: CreateStickerBlockInput[]; +} + +export class CreateStickerBlocksDto { + stickerBlocks: CreateStickerBlockInput[]; + postsId: number; + kakaoId: number; +} diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 2d8766b..5590e4f 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -61,4 +61,8 @@ export class StickerBlock { @ApiProperty({ description: '스티커의 zindex', type: Number }) @Column({ type: 'float' }) zindex: number; + + @ApiProperty({ description: '스티커의 clientId', type: String }) + @Column() + clientId: string; } diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index 9f05fa3..da4ebee 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -1,7 +1,10 @@ -import { Body, Controller, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; import { CreateStickerBlockInput } from './dtos/create-stickerBlock.dto'; +import { CreateStickerBlocksInput } from './dtos/create-stickerBlocks.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request } from 'express'; @ApiTags('게시글 API') @Controller('posts/:postId/stickers') @@ -13,16 +16,40 @@ export class StickerBlocksController { description: '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) @Post(':stickerId') async createStickerBlock( @Body() body: CreateStickerBlockInput, @Param('postId') postsId: number, @Param('stickerId') stickerId: number, + @Req() req: Request, ) { + const kakaoId = req.user.userId; return await this.stickerBlocksService.create({ ...body, + kakaoId, postsId, stickerId, }); } + + @ApiOperation({ + summary: '게시글 속 스티커 생성', + description: + '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', + }) + @Post('bulk') + async createStickerBlocks( + @Body() body: CreateStickerBlocksInput, + @Param('postId') postsId: number, + @Req() req: Request, + ) { + const kakaoId = req.user.userId; + return await this.stickerBlocksService.bulkInsert({ + ...body, + postsId, + kakaoId, + }); + } } diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index a204421..c4a188d 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -4,6 +4,7 @@ import { StickerBlock } from './entities/stickerblock.entity'; import { Repository } from 'typeorm'; import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; import { StickersService } from '../stickers/stickers.service'; +import { CreateStickerBlocksDto } from './dtos/create-stickerBlocks.dto'; @Injectable() export class StickerBlocksService { @@ -32,6 +33,18 @@ export class StickerBlocksService { } } + async bulkInsert({ + stickerBlocks, + postsId, + kakaoId, + }: CreateStickerBlocksDto) { + const stickerBlocksToInsert = stickerBlocks.map((stickerBlock) => ({ + ...stickerBlock, + postsId, + })); + return await this.stickerBlocksRepository.save(stickerBlocksToInsert); + } + async fetchBlocks({ postsId }) { return await this.stickerBlocksRepository.find({ where: { postsId }, From d225c0dae9457725c5872f54d8aecbb626bb9f63 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 18:51:38 +0900 Subject: [PATCH 153/236] feat: add stickers.service.interface --- .../interfaces/stickers.service.interface.ts | 12 ++++++++++++ src/APIs/stickers/stickers.controller.ts | 5 +++-- src/APIs/stickers/stickers.service.ts | 17 ++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 src/APIs/stickers/interfaces/stickers.service.interface.ts diff --git a/src/APIs/stickers/interfaces/stickers.service.interface.ts b/src/APIs/stickers/interfaces/stickers.service.interface.ts new file mode 100644 index 0000000..0a513ee --- /dev/null +++ b/src/APIs/stickers/interfaces/stickers.service.interface.ts @@ -0,0 +1,12 @@ +export interface IStickersServiceId { + id: number; +} + +export interface IStickersServiceDelete { + id: number; + kakaoId: number; +} + +export interface IStickersServiceFetchUserStickers { + userKakaoId: number; +} diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index cbbc894..fc4d759 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -87,12 +87,13 @@ export class StickersController { @Patch('stickers/:id') @UseGuards(AuthGuardV2) @ApiCookieAuth() + @ApiOkResponse({ type: Sticker }) @HttpCode(200) - async toggleReusable( + async patchSticker( @Req() req: Request, @Param('id') id: number, @Body() body: UpdateStickerInput, - ) { + ): Promise { const kakaoId = req.user.userId; return await this.stickersService.updateSticker({ kakaoId, diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index accc420..37e652d 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -8,6 +8,11 @@ import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; import { UpdateStickerDto } from './dtos/update-sticker.dto'; +import { + IStickersServiceDelete, + IStickersServiceFetchUserStickers, + IStickersServiceId, +} from './interfaces/stickers.service.interface'; @Injectable() export class StickersService { @@ -19,11 +24,11 @@ export class StickersService { private readonly usersService: UsersService, ) {} - async findStickerById({ id }) { + async findStickerById({ id }: IStickersServiceId): Promise { return await this.stickersRepository.findOne({ where: { id } }); } - async existCheck({ id }) { + async existCheck({ id }: IStickersServiceId): Promise { const data = await this.findStickerById({ id }); if (!data) throw new NotFoundException('스티커를 찾을 수 없습니다.'); } @@ -80,13 +85,15 @@ export class StickersService { return data; } - async fetchUserStickers({ userKakaoId }): Promise { + async fetchUserStickers({ + userKakaoId, + }: IStickersServiceFetchUserStickers): Promise { return await this.stickersRepository.find({ where: { userKakaoId, isReusable: true, isDefault: false }, }); } - async fetchPublicStickers() { + async fetchPublicStickers(): Promise { return await this.stickersRepository.find({ where: { isDefault: true }, }); @@ -118,7 +125,7 @@ export class StickersService { } } - async delete({ id, kakaoId }): Promise { + async delete({ id, kakaoId }: IStickersServiceDelete): Promise { const sticker = await this.stickersRepository.findOne({ where: { id, user: { kakaoId } }, }); From 70de51c5521ef90dd6f5fef6735fc5b4666c7996 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 19:22:23 +0900 Subject: [PATCH 154/236] fix: divide comments delete logic when delete user --- src/APIs/comments/comments.repository.ts | 10 ++-- src/APIs/comments/entities/comment.entity.ts | 2 +- .../dtos/create-stickerBlocks.dto.ts | 21 +++++-- .../stickerBlocks/stickerBlocks.controller.ts | 59 +++++++++++-------- .../stickerBlocks/stickerBlocks.service.ts | 5 ++ src/APIs/users/users.service.ts | 24 ++++++++ 6 files changed, 87 insertions(+), 34 deletions(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 2a130b8..5f76375 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -62,11 +62,11 @@ export class CommentsRepository extends Repository { .addOrderBy('children.date_created', 'ASC') .getMany(); - comments.forEach((comment) => { - comment.children = comment.children.filter( - (child) => child.date_deleted === null, - ); - }); + // comments.forEach((comment) => { + // comment.children = comment.children.filter( + // (child) => child.date_deleted === null, + // ); + // }); return comments; } } diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index cc799c1..e758e21 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -80,7 +80,7 @@ export class Comment { @UpdateDateColumn() date_updated: Date; - @ApiProperty({ type: Date, description: '논리 삭제 칼럼' }) + @ApiProperty({ type: Date, description: '논리 삭제 칼럼', nullable: true }) @DeleteDateColumn() date_deleted: Date; } diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts index 3195001..13d6f51 100644 --- a/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts +++ b/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts @@ -3,16 +3,29 @@ import { Type } from 'class-transformer'; import { IsArray, ValidateNested } from 'class-validator'; import { CreateStickerBlockInput } from './create-stickerBlock.dto'; +export class BulkInsertStickerInput extends CreateStickerBlockInput { + @ApiProperty({ type: Number, description: '스티커의 id' }) + stickerId: number; +} + export class CreateStickerBlocksInput { - @ApiProperty({ type: [CreateStickerBlockInput] }) + @ApiProperty({ type: [BulkInsertStickerInput] }) @IsArray() @ValidateNested({ each: true }) - @Type(() => CreateStickerBlockInput) - stickerBlocks: CreateStickerBlockInput[]; + @Type(() => BulkInsertStickerInput) + stickerBlocks: BulkInsertStickerInput[]; } export class CreateStickerBlocksDto { - stickerBlocks: CreateStickerBlockInput[]; + stickerBlocks: BulkInsertStickerInput[]; postsId: number; kakaoId: number; } + +export class CreateStickerBlocksResponseDto extends BulkInsertStickerInput { + @ApiProperty({ type: Number, description: '게시글 아이디' }) + postsId: number; + + @ApiProperty({ type: Number, description: '스티커블록 아이디' }) + id: number; +} diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index da4ebee..d700f35 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -1,8 +1,16 @@ import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; -import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; import { CreateStickerBlockInput } from './dtos/create-stickerBlock.dto'; -import { CreateStickerBlocksInput } from './dtos/create-stickerBlocks.dto'; +import { + CreateStickerBlocksInput, + CreateStickerBlocksResponseDto, +} from './dtos/create-stickerBlocks.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; @@ -11,6 +19,29 @@ import { Request } from 'express'; export class StickerBlocksController { constructor(private readonly stickerBlocksService: StickerBlocksService) {} + // @ApiOperation({ + // summary: '게시글 속 스티커 생성', + // description: + // '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', + // }) + // @ApiCookieAuth() + // @UseGuards(AuthGuardV2) + // @Post(':stickerId') + // async createStickerBlock( + // @Body() body: CreateStickerBlockInput, + // @Param('postId') postsId: number, + // @Param('stickerId') stickerId: number, + // @Req() req: Request, + // ) { + // const kakaoId = req.user.userId; + // return await this.stickerBlocksService.create({ + // ...body, + // kakaoId, + // postsId, + // stickerId, + // }); + // } + @ApiOperation({ summary: '게시글 속 스티커 생성', description: @@ -18,33 +49,13 @@ export class StickerBlocksController { }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @Post(':stickerId') - async createStickerBlock( - @Body() body: CreateStickerBlockInput, - @Param('postId') postsId: number, - @Param('stickerId') stickerId: number, - @Req() req: Request, - ) { - const kakaoId = req.user.userId; - return await this.stickerBlocksService.create({ - ...body, - kakaoId, - postsId, - stickerId, - }); - } - - @ApiOperation({ - summary: '게시글 속 스티커 생성', - description: - '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', - }) + @ApiCreatedResponse({ type: [CreateStickerBlocksResponseDto] }) @Post('bulk') async createStickerBlocks( @Body() body: CreateStickerBlocksInput, @Param('postId') postsId: number, @Req() req: Request, - ) { + ): Promise { const kakaoId = req.user.userId; return await this.stickerBlocksService.bulkInsert({ ...body, diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index c4a188d..1c37637 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -42,6 +42,11 @@ export class StickerBlocksService { ...stickerBlock, postsId, })); + stickerBlocksToInsert.forEach(async (stickerBlock) => { + await this.stickersService.existCheck({ + id: stickerBlock.stickerId, + }); + }); return await this.stickerBlocksRepository.save(stickerBlocksToInsert); } diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 69a85d6..5062463 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -209,6 +209,30 @@ export class UsersService { content, userKakaoId: kakaoId, }); + const commentsToDelete = await queryRunner.manager.find(Comment, { + where: { userKakaoId: kakaoId }, + }); + for (const data of commentsToDelete) { + let childrenData = []; + if (data.parentId == null) + childrenData = await queryRunner.manager.find(Comment, { + where: { parentId: data.id }, + }); + if (childrenData.length == 0) { + await queryRunner.manager.delete(Comment, { + id: data.id, + user: { kakaoId }, + }); + await queryRunner.manager.update(Posts, data.postsId, { + comment_count: () => 'comment_count - 1', + }); + } else { + await queryRunner.manager.softDelete(Comment, { + user: { kakaoId }, + id: data.id, + }); + } + } await queryRunner.manager.softDelete(Posts, { userKakaoId: kakaoId }); // 연동된 댓글 soft delete await queryRunner.manager.softDelete(Comment, { userKakaoId: kakaoId }); From d1e76332e0bb60f9a6d253093d7b23d6a4b219b7 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 19:40:59 +0900 Subject: [PATCH 155/236] fix: revise refresh-token cookie domain --- src/APIs/auth/auth.controller.ts | 5 +++++ src/APIs/stickerBlocks/stickerBlocks.controller.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index d6ad1a4..d41e3e5 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -81,8 +81,13 @@ export class AuthController { const newAccessToken = await this.authService.refresh( req.cookies.refreshToken, ); + const clientDomain = process.env.CLIENT_DOMAIN; + res.cookie('accessToken', newAccessToken, { httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, }); return res.send(); } catch (e) { diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index d700f35..f1479f8 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -6,7 +6,6 @@ import { ApiTags, } from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; -import { CreateStickerBlockInput } from './dtos/create-stickerBlock.dto'; import { CreateStickerBlocksInput, CreateStickerBlocksResponseDto, From 78e2707dc6630a4a84c02b49e5833e0e163a1724 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 19:55:41 +0900 Subject: [PATCH 156/236] feat: add monitoring dependency(nestjs-prometheus) --- package-lock.json | 44 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 46 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6b18359..e1fca0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@nestjs/terminus": "^10.2.3", "@nestjs/typeorm": "^10.0.2", "@types/multer": "^1.4.11", + "@willsoto/nestjs-prometheus": "^6.0.0", "axios": "^1.6.7", "bcrypt": "^5.1.1", "bull": "^4.12.8", @@ -38,6 +39,7 @@ "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", "passport-kakao": "^1.0.1", + "prom-client": "^15.1.2", "redis": "^4.6.14", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -3138,6 +3140,14 @@ "npm": ">=5.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4541,6 +4551,15 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@willsoto/nestjs-prometheus": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.0.tgz", + "integrity": "sha512-Krmda5CT9xDPjab8Eqdqiwi7xkZSX60A5rEGVLEDjUG6J6Rw5SCZ/BPaRk+MxNGWzUrRkM7K5FtTg38vWIOt1Q==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "prom-client": "^15.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5071,6 +5090,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -10072,6 +10096,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prom-client": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.2.tgz", + "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/promise-coalesce": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", @@ -11340,6 +11376,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/terser": { "version": "5.29.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", diff --git a/package.json b/package.json index 6ae4bf3..c594e8f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@nestjs/terminus": "^10.2.3", "@nestjs/typeorm": "^10.0.2", "@types/multer": "^1.4.11", + "@willsoto/nestjs-prometheus": "^6.0.0", "axios": "^1.6.7", "bcrypt": "^5.1.1", "bull": "^4.12.8", @@ -51,6 +52,7 @@ "mysql2": "^3.9.2", "passport-jwt": "^4.0.1", "passport-kakao": "^1.0.1", + "prom-client": "^15.1.2", "redis": "^4.6.14", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", From a7f2352f4774be4a492836cee3f22ee7a82c66da Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 5 Jun 2024 21:10:03 +0900 Subject: [PATCH 157/236] feat: add prom monitorting --- .../postBackgrounds/postBackgrounds.module.ts | 2 +- .../postBackgrounds.service.ts | 3 +- src/APIs/posts/posts.module.ts | 2 +- src/APIs/posts/posts.service.ts | 3 +- src/APIs/stickers/stickers.module.ts | 2 +- src/APIs/stickers/stickers.service.ts | 2 +- src/APIs/users/users.module.ts | 2 +- src/APIs/users/users.service.ts | 2 +- src/app.module.ts | 2 + .../interceptors/prometheus.interceptor.ts | 109 ++++++++++++++++++ .../interceptors/response.interceptor.ts | 36 ++++++ src/main.ts | 4 + src/{utils => modules}/aws/aws.module.ts | 0 src/{utils => modules}/aws/aws.service.ts | 0 src/modules/metrics/metrics.module.ts | 15 +++ 15 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/common/interceptors/prometheus.interceptor.ts create mode 100644 src/common/interceptors/response.interceptor.ts rename src/{utils => modules}/aws/aws.module.ts (100%) rename src/{utils => modules}/aws/aws.service.ts (100%) create mode 100644 src/modules/metrics/metrics.module.ts diff --git a/src/APIs/postBackgrounds/postBackgrounds.module.ts b/src/APIs/postBackgrounds/postBackgrounds.module.ts index 30e311e..c5d0389 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.module.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtStrategy } from '../auth/strategies/jwt.strategy'; import { UtilsModule } from 'src/utils/utils.module'; -import { AwsModule } from 'src/utils/aws/aws.module'; import { PostBackground } from './entities/postBackground.entity'; import { PostBackgroundsService } from './postBackgrounds.service'; import { PostBackgroundsController } from './postBackgrounds.controller'; +import { AwsModule } from 'src/modules/aws/aws.module'; @Module({ imports: [TypeOrmModule.forFeature([PostBackground]), UtilsModule, AwsModule], diff --git a/src/APIs/postBackgrounds/postBackgrounds.service.ts b/src/APIs/postBackgrounds/postBackgrounds.service.ts index 5aa889b..9324353 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.service.ts +++ b/src/APIs/postBackgrounds/postBackgrounds.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { AwsService } from 'src/utils/aws/aws.service'; + import { UtilsService } from 'src/utils/utils.service'; import { PostBackground } from './entities/postBackground.entity'; import { Repository } from 'typeorm'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; +import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() export class PostBackgroundsService { diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/posts/posts.module.ts index 1728cc4..de63506 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/posts/posts.module.ts @@ -4,13 +4,13 @@ import { Posts } from './entities/posts.entity'; import { User } from '../users/entities/user.entity'; import { PostsController } from './posts.controller'; import { UtilsModule } from 'src/utils/utils.module'; -import { AwsModule } from 'src/utils/aws/aws.module'; import { PostsService } from './posts.service'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { PostsRepository } from './posts.repository'; import { FollowsModule } from '../follows/follows.module'; +import { AwsModule } from 'src/modules/aws/aws.module'; @Module({ imports: [ diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index f247698..ab7f17c 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -6,7 +6,7 @@ import { NotFoundException, UnauthorizedException, } from '@nestjs/common'; -import { AwsService } from 'src/utils/aws/aws.service'; + import { UtilsService } from 'src/utils/utils.service'; import { DataSource } from 'typeorm'; import { Posts } from './entities/posts.entity'; @@ -43,6 +43,7 @@ import { } from './interfaces/posts.service.interface'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; +import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() export class PostsService { diff --git a/src/APIs/stickers/stickers.module.ts b/src/APIs/stickers/stickers.module.ts index 1e71155..28ca9ad 100644 --- a/src/APIs/stickers/stickers.module.ts +++ b/src/APIs/stickers/stickers.module.ts @@ -3,9 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Sticker } from './entities/sticker.entity'; import { StickersController } from './stickers.controller'; import { StickersService } from './stickers.service'; -import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UsersModule } from '../users/users.module'; +import { AwsService } from 'src/modules/aws/aws.service'; @Module({ imports: [TypeOrmModule.forFeature([Sticker]), UsersModule], diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 37e652d..8d4fa5c 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -3,7 +3,6 @@ import { Repository } from 'typeorm'; import { Sticker } from './entities/sticker.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { CreateStickerDto } from './dtos/create-sticker.dto'; -import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { UsersService } from '../users/users.service'; @@ -13,6 +12,7 @@ import { IStickersServiceFetchUserStickers, IStickersServiceId, } from './interfaces/stickers.service.interface'; +import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() export class StickersService { diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index c70996f..5a83b65 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -3,9 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UsersRepository } from './users.repository'; +import { AwsService } from 'src/modules/aws/aws.service'; @Module({ imports: [TypeOrmModule.forFeature([User])], diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 5062463..1c3c5f2 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -17,7 +17,6 @@ import { UserResponseDtoWithFollowing, } from './dtos/user-response.dto'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { AwsService } from 'src/utils/aws/aws.service'; import { UtilsService } from 'src/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; import { UsersRepository } from './users.repository'; @@ -27,6 +26,7 @@ import { Follow } from '../follows/entities/follow.entity'; import { User } from './entities/user.entity'; import { Comment } from '../comments/entities/comment.entity'; import { Feedback } from '../feedbacks/entities/feedback.entity'; +import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() export class UsersService { diff --git a/src/app.module.ts b/src/app.module.ts index cd4fa66..5a96394 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,9 +29,11 @@ import { BullModule } from '@nestjs/bull'; import { redisStore } from 'cache-manager-redis-yet'; import { TerminusModule } from '@nestjs/terminus'; import { HttpModule } from '@nestjs/axios'; +import { MetricsModule } from './modules/metrics/metrics.module'; @Module({ imports: [ + MetricsModule, AnnouncementsModule, AgreementsModule, FeedbacksModule, diff --git a/src/common/interceptors/prometheus.interceptor.ts b/src/common/interceptors/prometheus.interceptor.ts new file mode 100644 index 0000000..d9c764d --- /dev/null +++ b/src/common/interceptors/prometheus.interceptor.ts @@ -0,0 +1,109 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + OnModuleInit, +} from '@nestjs/common'; +import { Counter, Gauge, Histogram } from 'prom-client'; +import { Observable, tap } from 'rxjs'; + +@Injectable() +export class PrometheusInterceptor implements NestInterceptor, OnModuleInit { + onModuleInit() { + this.requestSuccessHistogram.reset(); + this.requestFailHistogram.reset(); + this.failureCounter.reset(); + } + // status code 2XX + private readonly requestSuccessHistogram = new Histogram({ + name: 'nestjs_success_requests', + help: 'NestJs success requests - duration in seconds', + labelNames: ['endpoint', 'method'], + buckets: [ + 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.09, 0.1, 0.25, 0.5, 1, + 2.5, 5, 10, + ], + }); + + // status code != 2XX + private readonly requestFailHistogram = new Histogram({ + name: 'nestjs_fail_requests', + help: 'NestJs fail requests - duration in seconds', + labelNames: ['endpoint', 'method'], + buckets: [ + 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.09, 0.1, 0.25, 0.5, 1, + 2.5, 5, 10, + ], + }); + + private readonly failureCounter = new Counter({ + name: 'nestjs_requests_failed_count', + help: 'NestJs requests that failed', + labelNames: ['endpoint', 'error', 'method'], + }); + + static registerServiceInfo(serviceInfo: { + domain: string; + name: string; + version: string; + }): PrometheusInterceptor { + new Gauge({ + name: 'nestjs_info', + help: 'NestJs service version info', + labelNames: ['domain', 'name', 'version'], + }).set( + { + domain: serviceInfo.domain, + name: `${serviceInfo.domain}.${serviceInfo.name}`, + version: serviceInfo.version, + }, + 1, + ); + + return new PrometheusInterceptor(); + } + + // metrics url 요청은 트래킹 필요 x + private isAvailableMetricsUrl(url: string): boolean { + const excludePaths = 'metrics'; + if (url.includes(excludePaths)) { + return false; + } + return true; + } + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const originUrl = context.switchToHttp().getRequest().url.toString(); + + const method = context.switchToHttp().getRequest().method.toString(); + const labels = { + endpoint: `${context.getClass().name}#${context.getHandler().name}`, // 라벨을 합침 + method: method, + }; + + try { + const requestSuccessTimer = + this.requestSuccessHistogram.startTimer(labels); + const requestFailTimer = this.requestFailHistogram.startTimer(labels); + return next.handle().pipe( + tap({ + next: () => { + if (this.isAvailableMetricsUrl(originUrl)) { + requestSuccessTimer(); + } + // Handle the next event here + }, + error: () => { + if (this.isAvailableMetricsUrl(originUrl)) { + requestFailTimer(); + this.failureCounter.labels({ ...labels }).inc(1); + } + + // Handle the error event here + }, + }), + ); + } catch (error) {} + } +} diff --git a/src/common/interceptors/response.interceptor.ts b/src/common/interceptors/response.interceptor.ts new file mode 100644 index 0000000..aeba69e --- /dev/null +++ b/src/common/interceptors/response.interceptor.ts @@ -0,0 +1,36 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { defaultIfEmpty, map } from 'rxjs'; + +@Injectable() +export class ResponseInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): Observable { + const req: Request = _context.switchToHttp().getRequest(); + + const excludePaths = ['/metrics']; + + return next + .handle() + .pipe(defaultIfEmpty(null)) + .pipe( + map((result) => { + if (excludePaths.includes(req.url)) { + return result; + } + // CAPA4.0 표준 응답으로 변환하여 반환 + return new ResponseObj(true, result); + }), + ); + } +} +export class ResponseObj { + constructor( + public success: boolean, + public data: any, + ) {} +} diff --git a/src/main.ts b/src/main.ts index 32f8e2b..5ebe7ed 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,8 @@ import cookieParser from 'cookie-parser'; import { HttpExceptionFilter } from './common/filter/http-exception.filter'; import { ValidationPipe } from '@nestjs/common'; import expressBasicAuth from 'express-basic-auth'; +import { PrometheusInterceptor } from './common/interceptors/prometheus.interceptor'; +import { ResponseInterceptor } from './common/interceptors/response.interceptor'; // import * as expressBasicAuth from 'express-basic-auth'; async function bootstrap() { @@ -33,6 +35,8 @@ async function bootstrap() { }), ); app.useGlobalFilters(new HttpExceptionFilter()); + // app.useGlobalInterceptors(new ResponseInterceptor()); + app.useGlobalInterceptors(new PrometheusInterceptor()); // wooserk.tistory.com/105 const config = new DocumentBuilder() diff --git a/src/utils/aws/aws.module.ts b/src/modules/aws/aws.module.ts similarity index 100% rename from src/utils/aws/aws.module.ts rename to src/modules/aws/aws.module.ts diff --git a/src/utils/aws/aws.service.ts b/src/modules/aws/aws.service.ts similarity index 100% rename from src/utils/aws/aws.service.ts rename to src/modules/aws/aws.service.ts diff --git a/src/modules/metrics/metrics.module.ts b/src/modules/metrics/metrics.module.ts new file mode 100644 index 0000000..cd6afc1 --- /dev/null +++ b/src/modules/metrics/metrics.module.ts @@ -0,0 +1,15 @@ +// metrics.module.ts +import { Module } from '@nestjs/common'; +import { PrometheusModule as Prometheus } from '@willsoto/nestjs-prometheus'; + +@Module({ + imports: [ + Prometheus.register({ + path: '/nestjs-metrics', + defaultMetrics: { + enabled: true, + }, + }), + ], +}) +export class MetricsModule {} From 35d363994a90f30e0abe0326027c1e18319d290a Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 03:37:48 +0900 Subject: [PATCH 158/236] feat: add promQueries --- deploy/promQueries.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 deploy/promQueries.txt diff --git a/deploy/promQueries.txt b/deploy/promQueries.txt new file mode 100644 index 0000000..e0ea436 --- /dev/null +++ b/deploy/promQueries.txt @@ -0,0 +1,5 @@ +// 전체 엔드포인트의 분포 +histogram_quantile(0.95, sum(rate(nestjs_success_requests_bucket[5m])) by (le, endpoint)) + +// 특정 엔드포인트의 성공 요청 시간 분포 +histogram_quantile(0.95, sum(rate(nestjs_success_requests_bucket{endpoint="AppController#healthCheck", method="GET"}[5m])) by (le)) From e2b1bf2a31586e04867de0267dda2968d3f38636 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 03:46:20 +0900 Subject: [PATCH 159/236] fix: change sticker mapping logic to bulk inserting --- .../stickerCategories/dtos/map-category.dto.ts | 8 ++++++++ .../stickerCategories.service.interface.ts | 6 ++++++ .../stickerCategories.controller.ts | 4 ++-- .../stickerCategories.service.ts | 17 +++++++---------- 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts diff --git a/src/APIs/stickerCategories/dtos/map-category.dto.ts b/src/APIs/stickerCategories/dtos/map-category.dto.ts index 12547f3..764d237 100644 --- a/src/APIs/stickerCategories/dtos/map-category.dto.ts +++ b/src/APIs/stickerCategories/dtos/map-category.dto.ts @@ -7,3 +7,11 @@ export class MapCategoryDto { @ApiProperty({ description: '매핑 하고자 하는 카테고리의 id', type: Number }) stickerCategoryId: number; } + +export class BulkMapCategoryDto { + @ApiProperty({ + description: '매핑할 카테고리 및 스티커 배열', + type: [MapCategoryDto], + }) + maps: MapCategoryDto[]; +} diff --git a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts new file mode 100644 index 0000000..de5dd85 --- /dev/null +++ b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts @@ -0,0 +1,6 @@ +import { MapCategoryDto } from '../dtos/map-category.dto'; + +export interface IStickerCategoriesServiceMapCategory { + kakaoId: number; + maps: MapCategoryDto[]; +} diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 2d08eed..37c76b9 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -15,7 +15,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { MapCategoryDto } from './dtos/map-category.dto'; +import { BulkMapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { CreateStickerCategoryInput } from './dtos/create-sticker-category.dto'; @@ -77,7 +77,7 @@ export class StickerCategoriesController { @Post('users/admin/stickers/map') async mapCategory( @Req() req: Request, - @Body() mapCategoryDto: MapCategoryDto, + @Body() mapCategoryDto: BulkMapCategoryDto, ) { const kakaoId = req.user.userId; return await this.stickerCategoriesService.mapCategory({ diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 64209d7..2667887 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; import { UsersService } from '../users/users.service'; import { StickersService } from '../stickers/stickers.service'; +import { IStickerCategoriesServiceMapCategory } from './interfaces/stickerCategories.service.interface'; @Injectable() export class StickerCategoriesService { @@ -42,17 +43,13 @@ export class StickerCategoriesService { return await this.stickerCategoriesRepository.save({ name }); } - async mapCategory({ kakaoId, stickerId, stickerCategoryId }) { + async mapCategory({ kakaoId, maps }: IStickerCategoriesServiceMapCategory) { await this.usersService.adminCheck({ kakaoId }); - await this.stickersService.existCheck({ id: stickerId }); - await this.existCheckById({ id: stickerCategoryId }); - return await this.stickerCategoryMappersRepository - .createQueryBuilder() - .insert() - .into(StickerCategoryMapper, ['stickerId', 'stickerCategoryId']) - .values({ stickerId, stickerCategoryId }) - .orIgnore() - .execute(); + maps.forEach(async (map) => { + await this.existCheckById({ id: map.stickerCategoryId }); + await this.stickersService.existCheck({ id: map.stickerId }); + }); + return await this.stickerCategoryMappersRepository.save(maps); } async fetchStickersByCategoryId({ id }) { From 87ff1d5b1d2d45b5b08ad700a9b8c893a6ee0d04 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 03:56:03 +0900 Subject: [PATCH 160/236] fix: add basic resizing on POST posts/image --- src/APIs/posts/posts.controller.ts | 3 ++- src/APIs/posts/posts.service.ts | 2 +- src/modules/aws/aws.service.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index b48a81f..7cba428 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -126,7 +126,8 @@ export class PostsController { @ApiOperation({ summary: '이미지 업로드', - description: '이미지를 서버에 업로드한다. url을 반환 받는다.', + description: + '이미지를 서버에 업로드한다. url을 반환 받는다. 게시글 내부 이미지 업로드 및 캡처 이미지 업로드용. ', }) @ApiConsumes('multipart/form-data') @ApiBody({ diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index ab7f17c..0c06ea1 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -67,7 +67,7 @@ export class PostsService { `${imageName}.${ext}`, file, ext, - 1200, + 2000, ); return { image_url }; diff --git a/src/modules/aws/aws.service.ts b/src/modules/aws/aws.service.ts index 7f27722..a4e75eb 100644 --- a/src/modules/aws/aws.service.ts +++ b/src/modules/aws/aws.service.ts @@ -79,7 +79,7 @@ export class AwsService { async resizeImage(buffer: Buffer, width: number) { const resizedImageBuffer = await sharp(buffer, { failOnError: false }) - .resize({ width }) + .resize({ width, withoutEnlargement: true }) .toBuffer(); return resizedImageBuffer; } From dad932bbb29a919ee3cd672855ac1d9b7df88303 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 04:01:32 +0900 Subject: [PATCH 161/236] feat: add interfaces on stickerCategoriesService --- src/APIs/posts/posts.controller.ts | 2 +- src/APIs/posts/posts.service.ts | 2 +- .../entities/stickerCategoryMapper.entity.ts | 3 +++ .../stickerCategories.service.interface.ts | 13 +++++++++++ .../stickerCategories.service.ts | 22 +++++++++++++------ 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 7cba428..c85c5ba 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -127,7 +127,7 @@ export class PostsController { @ApiOperation({ summary: '이미지 업로드', description: - '이미지를 서버에 업로드한다. url을 반환 받는다. 게시글 내부 이미지 업로드 및 캡처 이미지 업로드용. ', + '이미지를 서버에 업로드한다. url을 반환 받는다. 게시글 내부 이미지 업로드 및 캡처 이미지 업로드용. max_width=1280px', }) @ApiConsumes('multipart/form-data') @ApiBody({ diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 0c06ea1..e9ece19 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -67,7 +67,7 @@ export class PostsService { `${imageName}.${ext}`, file, ext, - 2000, + 1280, ); return { image_url }; diff --git a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts index 9db2e66..da7cf9b 100644 --- a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts @@ -7,9 +7,11 @@ import { RelationId, } from 'typeorm'; import { StickerCategory } from './stickerCategory.entity'; +import { ApiProperty } from '@nestjs/swagger'; @Entity() export class StickerCategoryMapper { + @ApiProperty({ type: Number, description: '스티커 아이디' }) @PrimaryColumn() @RelationId( (stickerCategoryMapper: StickerCategoryMapper) => @@ -24,6 +26,7 @@ export class StickerCategoryMapper { }) sticker: Sticker; + @ApiProperty({ type: Number, description: '스티커 카테고리 아이디' }) @PrimaryColumn() @RelationId( (stickerCategoryMapper: StickerCategoryMapper) => diff --git a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts index de5dd85..efaaf2c 100644 --- a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts +++ b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts @@ -4,3 +4,16 @@ export interface IStickerCategoriesServiceMapCategory { kakaoId: number; maps: MapCategoryDto[]; } + +export interface IStickerCategoriesServiceId { + id: number; +} + +export interface IStickerCategoriesServiceName { + name: string; +} + +export interface IStickerCategoriesServiceCreateCategory { + kakaoId: number; + name: string; +} diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 2667887..6ff5581 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -5,7 +5,12 @@ import { InjectRepository } from '@nestjs/typeorm'; import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; import { UsersService } from '../users/users.service'; import { StickersService } from '../stickers/stickers.service'; -import { IStickerCategoriesServiceMapCategory } from './interfaces/stickerCategories.service.interface'; +import { + IStickerCategoriesServiceCreateCategory, + IStickerCategoriesServiceId, + IStickerCategoriesServiceMapCategory, + IStickerCategoriesServiceName, +} from './interfaces/stickerCategories.service.interface'; @Injectable() export class StickerCategoriesService { @@ -18,18 +23,18 @@ export class StickerCategoriesService { private readonly stickersService: StickersService, ) {} - async findCategoryByName({ name }) { + async findCategoryByName({ name }: IStickerCategoriesServiceName) { return await this.stickerCategoriesRepository.findOne({ where: { name } }); } - async findCategoryById({ id }) { + async findCategoryById({ id }: IStickerCategoriesServiceId) { return await this.stickerCategoriesRepository.findOne({ where: { id } }); } - async existCheckByName({ name }) { + async existCheckByName({ name }: IStickerCategoriesServiceName) { const data = await this.findCategoryByName({ name }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } - async existCheckById({ id }) { + async existCheckById({ id }: IStickerCategoriesServiceId) { const data = await this.findCategoryById({ id }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } @@ -38,7 +43,10 @@ export class StickerCategoriesService { return await this.stickerCategoriesRepository.find(); } - async createCategory({ kakaoId, name }) { + async createCategory({ + kakaoId, + name, + }: IStickerCategoriesServiceCreateCategory) { await this.usersService.adminCheck({ kakaoId }); return await this.stickerCategoriesRepository.save({ name }); } @@ -52,7 +60,7 @@ export class StickerCategoriesService { return await this.stickerCategoryMappersRepository.save(maps); } - async fetchStickersByCategoryId({ id }) { + async fetchStickersByCategoryId({ id }: IStickerCategoriesServiceId) { await this.existCheckById({ id }); return await this.stickerCategoryMappersRepository.find({ relations: { sticker: true, stickerCategory: true }, From 9788db4c1b0a9a745f9b0cbb23188a91cf8c0259 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 04:08:15 +0900 Subject: [PATCH 162/236] add responseType on stickerCategoriesController --- .../stickerCategories.controller.ts | 16 +++++++---- .../stickerCategories.service.ts | 27 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 37c76b9..e3f329e 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -19,6 +19,7 @@ import { BulkMapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { CreateStickerCategoryInput } from './dtos/create-sticker-category.dto'; +import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; @ApiTags('스티커 API') @Controller() @@ -31,8 +32,9 @@ export class StickerCategoriesController { summary: '카테고리 fetchAll', description: '카테고리를 모두 조회한다.', }) + @ApiOkResponse({ type: [StickerCategory] }) @Get('stickers/categories') - async fetchCategories() { + async fetchCategories(): Promise { return await this.stickerCategoriesService.fetchCategories(); } @@ -40,8 +42,11 @@ export class StickerCategoriesController { summary: '카테고리 id에 해당하는 스티커를 fetchAll', description: '카테고리를 id로 찾고, 이에 매핑된 스티커들을 가져온다', }) + @ApiOkResponse({ type: [StickerCategoryMapper] }) @Get('stickers/categories/:id') - async fetchStickersByCategoryName(@Param('id') id: string) { + async fetchStickersByCategoryName( + @Param('id') id: number, + ): Promise { return await this.stickerCategoriesService.fetchStickersByCategoryId({ id, }); @@ -52,14 +57,14 @@ export class StickerCategoriesController { summary: '[어드민용] 스티커 카테고리 생성', description: '[어드민 전용] 스티커 카테고리를 만든다.', }) - @ApiOkResponse({ description: '생성 완료', type: StickerCategory }) + @ApiOkResponse({ type: StickerCategory }) @ApiCookieAuth() @UseGuards(AuthGuardV2) @Post('users/admin/stickers/categories') async createCategory( @Req() req: Request, @Body() body: CreateStickerCategoryInput, - ) { + ): Promise { const kakaoId = req.user.userId; return await this.stickerCategoriesService.createCategory({ kakaoId, @@ -73,12 +78,13 @@ export class StickerCategoriesController { description: '[어드민 전용] 스티커에 카테고리를 매핑한다.', }) @ApiCookieAuth() + @ApiOkResponse({ type: [StickerCategoryMapper] }) @UseGuards(AuthGuardV2) @Post('users/admin/stickers/map') async mapCategory( @Req() req: Request, @Body() mapCategoryDto: BulkMapCategoryDto, - ) { + ): Promise { const kakaoId = req.user.userId; return await this.stickerCategoriesService.mapCategory({ kakaoId, diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 6ff5581..f911afc 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -23,35 +23,44 @@ export class StickerCategoriesService { private readonly stickersService: StickersService, ) {} - async findCategoryByName({ name }: IStickerCategoriesServiceName) { + async findCategoryByName({ + name, + }: IStickerCategoriesServiceName): Promise { return await this.stickerCategoriesRepository.findOne({ where: { name } }); } - async findCategoryById({ id }: IStickerCategoriesServiceId) { + async findCategoryById({ + id, + }: IStickerCategoriesServiceId): Promise { return await this.stickerCategoriesRepository.findOne({ where: { id } }); } - async existCheckByName({ name }: IStickerCategoriesServiceName) { + async existCheckByName({ + name, + }: IStickerCategoriesServiceName): Promise { const data = await this.findCategoryByName({ name }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } - async existCheckById({ id }: IStickerCategoriesServiceId) { + async existCheckById({ id }: IStickerCategoriesServiceId): Promise { const data = await this.findCategoryById({ id }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } - async fetchCategories() { + async fetchCategories(): Promise { return await this.stickerCategoriesRepository.find(); } async createCategory({ kakaoId, name, - }: IStickerCategoriesServiceCreateCategory) { + }: IStickerCategoriesServiceCreateCategory): Promise { await this.usersService.adminCheck({ kakaoId }); return await this.stickerCategoriesRepository.save({ name }); } - async mapCategory({ kakaoId, maps }: IStickerCategoriesServiceMapCategory) { + async mapCategory({ + kakaoId, + maps, + }: IStickerCategoriesServiceMapCategory): Promise { await this.usersService.adminCheck({ kakaoId }); maps.forEach(async (map) => { await this.existCheckById({ id: map.stickerCategoryId }); @@ -60,7 +69,9 @@ export class StickerCategoriesService { return await this.stickerCategoryMappersRepository.save(maps); } - async fetchStickersByCategoryId({ id }: IStickerCategoriesServiceId) { + async fetchStickersByCategoryId({ + id, + }: IStickerCategoriesServiceId): Promise { await this.existCheckById({ id }); return await this.stickerCategoryMappersRepository.find({ relations: { sticker: true, stickerCategory: true }, From 9b9f7d2ac8ac31c44dd86bef20e4485c01fbfa60 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 04:38:52 +0900 Subject: [PATCH 163/236] fix: change postBackground nullable option to true --- src/APIs/posts/dtos/publish-post.input.ts | 12 +++++++++--- src/APIs/posts/posts.service.ts | 15 ++++++++------- .../entities/stickerCategory.entity.ts | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 24d102e..219b0c6 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsEnum, IsOptional, IsString } from 'class-validator'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { IsBoolean } from 'src/common/validators/isBoolean'; @@ -11,9 +11,15 @@ export class PublishPostInput { @IsString() postCategoryId: string; - @ApiProperty({ description: '연결된 내지 fk', type: String }) + @ApiProperty({ + description: '연결된 내지 fk', + type: String, + nullable: true, + required: false, + }) @IsString() - postBackgroundId: string; + @IsOptional() + postBackgroundId?: string; @ApiProperty({ description: '제목(최대 100자)', diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index e9ece19..a119485 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -44,6 +44,7 @@ import { import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { AwsService } from 'src/modules/aws/aws.service'; +import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; @Injectable() export class PostsService { @@ -90,13 +91,13 @@ export class PostsService { .getOne(); if (!pc && !passNonEssentail) throw new BadRequestException('존재하지 않는 post_category입니다.'); - // const pg = await this.dataSource - // .getRepository(PostBackground) - // .createQueryBuilder('pg') - // .where('pg.id = :id', { id: posts.postBackgroundId }) - // .getOne(); - // if (!pg && !passNonEssentail) - // throw new BadRequestException('존재하지 않는 post_background입니다.'); + const pg = await this.dataSource + .getRepository(PostBackground) + .createQueryBuilder('pg') + .where('pg.id = :id', { id: posts.postBackgroundId }) + .getOne(); + if (!pg && !passNonEssentail) + throw new BadRequestException('존재하지 않는 post_background입니다.'); const us = await this.dataSource .getRepository(User) .createQueryBuilder('us') diff --git a/src/APIs/stickerCategories/entities/stickerCategory.entity.ts b/src/APIs/stickerCategories/entities/stickerCategory.entity.ts index 520f13b..76f5d86 100644 --- a/src/APIs/stickerCategories/entities/stickerCategory.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategory.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class StickerCategory { From 54d858e0847649f918a48ad04cf0f3be12bd62f6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 05:17:29 +0900 Subject: [PATCH 164/236] feat: add title_html column on posts --- src/APIs/posts/dtos/create-post.input.ts | 4 ++++ src/APIs/posts/dtos/fetch-posts.dto.ts | 1 + src/APIs/posts/dtos/publish-post.input.ts | 4 ++++ src/APIs/posts/entities/posts.entity.ts | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index d6913f1..bd58f11 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -24,6 +24,10 @@ export class CreatePostInput { @IsString() title: string; + @ApiProperty({ description: '수정용 제목', type: String }) + @IsString() + title_html: string; + @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean, diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/posts/dtos/fetch-posts.dto.ts index efbef5d..6319efb 100644 --- a/src/APIs/posts/dtos/fetch-posts.dto.ts +++ b/src/APIs/posts/dtos/fetch-posts.dto.ts @@ -49,6 +49,7 @@ export const FETCH_POST_OPTION = { date_deleted: true, }, title: true, + title_html: true, content: true, main_description: true, image_url: true, diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 219b0c6..6f27800 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -28,6 +28,10 @@ export class PublishPostInput { @IsString() title: string; + @ApiProperty({ description: '수정용 제목', type: String }) + @IsString() + title_html: string; + @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index de965e5..650d3f1 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -45,6 +45,10 @@ export class Posts { @Column({ length: 100, default: '' }) title: string; + @ApiProperty({ description: '수정용 제목', type: String }) + @Column({ length: 100, default: '' }) + title_html: string; + @ApiProperty({ description: '임시저장(false), 발행(true)', type: Boolean }) @Column({ default: false }) isPublished: boolean; From 98e00be7181f2624b4732eb10adba7ae83c76856 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 05:24:10 +0900 Subject: [PATCH 165/236] fix: change temp post dto --- src/APIs/posts/dtos/create-post.input.ts | 12 ++++++++++-- src/APIs/posts/entities/posts.entity.ts | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index bd58f11..d6b49a8 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -52,12 +52,20 @@ export class CreatePostInput { @IsString() content: string; - @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) + @ApiProperty({ + description: '게시글 캡쳐 이미지 url', + type: String, + required: false, + }) @IsString() @IsOptional() image_url?: string; - @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) + @ApiProperty({ + description: '게시글 대표 이미지 url', + type: String, + required: false, + }) @IsString() @IsOptional() main_image_url?: string; diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts index 650d3f1..1c4ca2b 100644 --- a/src/APIs/posts/entities/posts.entity.ts +++ b/src/APIs/posts/entities/posts.entity.ts @@ -46,7 +46,7 @@ export class Posts { title: string; @ApiProperty({ description: '수정용 제목', type: String }) - @Column({ length: 100, default: '' }) + @Column({ default: '' }) title_html: string; @ApiProperty({ description: '임시저장(false), 발행(true)', type: Boolean }) @@ -104,11 +104,11 @@ export class Posts { main_description: string; @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) - @Column() + @Column({ default: '' }) image_url: string; @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) - @Column() + @Column({ default: '' }) main_image_url: string; @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) From dd28fc1b0ab229a6c53792ed1c5ed3f54de8a02f Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 7 Jun 2024 05:44:52 +0900 Subject: [PATCH 166/236] fix: check comment allowance when inserting comment --- src/APIs/comments/comments.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index c751085..c54a5e5 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -56,6 +56,11 @@ export class CommentsService { } async insert(createCommentDto: CreateCommentDto): Promise { + const post = await this.dataSource.manager.findOne(Posts, { + where: { id: createCommentDto.postsId }, + }); + if (post.allow_comment === false) + throw new ForbiddenException('댓글이 허용되지 않은 게시물 입니다.'); if (createCommentDto.parentId) await this.postsIdValidCheck({ parentId: createCommentDto.parentId, From 44f79fbfbd4fedb1414fe08ceceb5edd5c27d399 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 9 Jun 2024 16:46:11 +0900 Subject: [PATCH 167/236] feat: add interfaces on stickerBlock.service --- .../stickerBlocks.service.interface.ts | 8 +++++++ .../stickerBlocks/stickerBlocks.service.ts | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts diff --git a/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts b/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts new file mode 100644 index 0000000..91c72e5 --- /dev/null +++ b/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts @@ -0,0 +1,8 @@ +export interface IStikcerBlocksServiceFetchBlocks { + postsId: number; +} + +export interface IStikcerBlocksServiceDeleteBlocks { + kakaoId: number; + postsId: number; +} diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 1c37637..110ae72 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -4,7 +4,14 @@ import { StickerBlock } from './entities/stickerblock.entity'; import { Repository } from 'typeorm'; import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; import { StickersService } from '../stickers/stickers.service'; -import { CreateStickerBlocksDto } from './dtos/create-stickerBlocks.dto'; +import { + CreateStickerBlocksDto, + CreateStickerBlocksResponseDto, +} from './dtos/create-stickerBlocks.dto'; +import { + IStikcerBlocksServiceDeleteBlocks, + IStikcerBlocksServiceFetchBlocks, +} from './interfaces/stickerBlocks.service.interface'; @Injectable() export class StickerBlocksService { @@ -14,7 +21,9 @@ export class StickerBlocksService { private readonly stickerBlocksRepository: Repository, ) {} - async create(createStickerBlockDto: CreateStickerBlockDto) { + async create( + createStickerBlockDto: CreateStickerBlockDto, + ): Promise { // 순환참조 막기 위해 자체 에러 헨들링 // await this.postsService.existCheck({ // id: createStickerBlockDto.postsId, @@ -37,7 +46,7 @@ export class StickerBlocksService { stickerBlocks, postsId, kakaoId, - }: CreateStickerBlocksDto) { + }: CreateStickerBlocksDto): Promise { const stickerBlocksToInsert = stickerBlocks.map((stickerBlock) => ({ ...stickerBlock, postsId, @@ -50,13 +59,18 @@ export class StickerBlocksService { return await this.stickerBlocksRepository.save(stickerBlocksToInsert); } - async fetchBlocks({ postsId }) { + async fetchBlocks({ + postsId, + }: IStikcerBlocksServiceFetchBlocks): Promise { return await this.stickerBlocksRepository.find({ where: { postsId }, }); } - async deleteBlocks({ kakaoId, postsId }): Promise { + async deleteBlocks({ + kakaoId, + postsId, + }: IStikcerBlocksServiceDeleteBlocks): Promise { const blocksToDelete = await this.stickerBlocksRepository.find({ relations: ['sticker'], where: { postsId }, From e9de3e84d5ec61ea7feba6569249558d668e11df Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 9 Jun 2024 18:13:58 +0900 Subject: [PATCH 168/236] feat: change image GET url from s3 to cloudfront --- src/modules/aws/aws.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/aws/aws.service.ts b/src/modules/aws/aws.service.ts index a4e75eb..135a533 100644 --- a/src/modules/aws/aws.service.ts +++ b/src/modules/aws/aws.service.ts @@ -35,7 +35,8 @@ export class AwsService { ContentType: `image/${ext}`, // 파일 타입, }); await this.s3Client.send(command); - return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; + // return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; + return `https://${this.configService.get('CLOUDFRONT_DOMAIN_NAME')}/${fileName}`; } async deleteImageFromS3({ url }) { @@ -74,7 +75,8 @@ export class AwsService { await this.s3Client.send(command); // 업로드된 이미지의 URL을 반환합니다. - return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; + // return `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_S3_BUCKET_NAME}/${fileName}`; + return `https://${this.configService.get('CLOUDFRONT_DOMAIN_NAME')}/${fileName}`; } async resizeImage(buffer: Buffer, width: number) { From 8e3b53ad320aa1d0cb35d050827a8ae70a6125bf Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 11 Jun 2024 00:30:52 +0900 Subject: [PATCH 169/236] fix: change fetchNotiResponse --- src/APIs/notifications/dtos/fetch-noti.dto.ts | 5 ++- .../notifications/notifications.repository.ts | 31 ++++++++++++++----- .../notifications/notifications.service.ts | 11 ++++--- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts index 4473f65..8de239f 100644 --- a/src/APIs/notifications/dtos/fetch-noti.dto.ts +++ b/src/APIs/notifications/dtos/fetch-noti.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; import { DateOption } from 'src/common/enums/date-option'; import { Notification } from '../entities/notification.entity'; +import { User } from 'src/APIs/users/entities/user.entity'; export class FetchNotiInput { @ApiProperty({ @@ -30,4 +31,6 @@ export class FetchNotiDto extends FetchNotiInput { export class FetchNotiResponse extends OmitType(Notification, [ 'targetUser', 'user', -]) {} +]) { + user: Pick; +} diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index 2d217d5..a8d308e 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -3,6 +3,7 @@ import { Notification } from './entities/notification.entity'; import { DataSource, Repository } from 'typeorm'; import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiResponse } from './dtos/fetch-noti.dto'; +import { INotificationsServiceRead } from './interfaces/notifications.service.interface'; @Injectable() export class NotificationsRepository extends Repository { @@ -18,26 +19,40 @@ export class NotificationsRepository extends Repository { .execute(); } + async fetchOne({ + id, + targetUserKakaoId, + }: INotificationsServiceRead): Promise { + return await this.createQueryBuilder('n') + .leftJoin('n.user', 'user') + .addSelect(['user.profile_image', 'user.username', 'user.handle']) + .where('n.id = :id', { id }) + .andWhere('n.targetUserKakaoId = :targetUserKakaoId', { + targetUserKakaoId, + }) + .getOne(); + } + async fetchAll({ kakaoId, date_created, is_checked, }): Promise { - const query = this.createQueryBuilder('').where( - 'targetUserKakaoId = :kakaoId', - { + const query = this.createQueryBuilder('n') + .leftJoinAndSelect('n.user', 'user') + .addSelect(['user.profile_image', 'user.username', 'user.handle']) + .where('n.targetUserKakaoId = :kakaoId', { kakaoId, - }, - ); + }); if (!is_checked) { - query.andWhere('is_checked = true'); + query.andWhere('n.is_checked = true'); } if (date_created) { - query.andWhere('date_created > :date_created', { date_created }); + query.andWhere('n.date_created > :date_created', { date_created }); } // 열 이름을 별칭으로 지정하여 원래 이름 그대로 출력 const columnNames = (await this.metadata.columns).map( - (column) => `${column.databaseName} AS ${column.propertyName}`, + (column) => `n.${column.databaseName} AS ${column.propertyName}`, ); query.select(columnNames); return await query.execute(); diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 44783d9..36b38b5 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -59,10 +59,12 @@ export class NotificationsService { targetUserKakaoId, type, }); - console.log(data); // Redis 큐에 이벤트를 전송 await this.redisQueue.add(this.queueName, data); - return data; + return await this.notificationsRepository.fetchOne({ + id: data.id, + targetUserKakaoId, + }); } catch (e) { throw new BadRequestException('대상을 찾을 수 없습니다.'); } @@ -108,8 +110,9 @@ export class NotificationsService { if (updateResult.affected < 1) { throw new BadRequestException('알림을 찾을 수 없거나 권한이 없습니다.'); } - return await this.notificationsRepository.findOne({ - where: { id, targetUserKakaoId }, + return await this.notificationsRepository.fetchOne({ + id, + targetUserKakaoId, }); } } From 114f204872d6dc2f15c6f55497ab970ebe84c0e4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 11 Jun 2024 01:52:15 +0900 Subject: [PATCH 170/236] fix: change sse sending data --- src/APIs/notifications/dtos/fetch-noti.dto.ts | 5 ++++- src/APIs/notifications/notifications.repository.ts | 12 ++++++------ src/APIs/notifications/notifications.service.ts | 9 +++++---- src/APIs/posts/posts.service.ts | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts index 8de239f..34c9be1 100644 --- a/src/APIs/notifications/dtos/fetch-noti.dto.ts +++ b/src/APIs/notifications/dtos/fetch-noti.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; import { DateOption } from 'src/common/enums/date-option'; import { Notification } from '../entities/notification.entity'; @@ -32,5 +32,8 @@ export class FetchNotiResponse extends OmitType(Notification, [ 'targetUser', 'user', ]) { + @ApiProperty({ + type: PickType(User, ['username', 'profile_image', 'handle']), + }) user: Pick; } diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index a8d308e..a9c53f7 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -39,7 +39,7 @@ export class NotificationsRepository extends Repository { is_checked, }): Promise { const query = this.createQueryBuilder('n') - .leftJoinAndSelect('n.user', 'user') + .leftJoin('n.user', 'user') .addSelect(['user.profile_image', 'user.username', 'user.handle']) .where('n.targetUserKakaoId = :kakaoId', { kakaoId, @@ -51,10 +51,10 @@ export class NotificationsRepository extends Repository { query.andWhere('n.date_created > :date_created', { date_created }); } // 열 이름을 별칭으로 지정하여 원래 이름 그대로 출력 - const columnNames = (await this.metadata.columns).map( - (column) => `n.${column.databaseName} AS ${column.propertyName}`, - ); - query.select(columnNames); - return await query.execute(); + // const columnNames = (await this.metadata.columns).map( + // (column) => `n.${column.databaseName} AS ${column.propertyName}`, + // ); + // query.addSelect(columnNames); + return await query.getMany(); } } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 36b38b5..4df4986 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -18,7 +18,7 @@ export class NotificationsService { @InjectQueue('audio') private redisQueue: Queue, private readonly notificationsRepository: NotificationsRepository, ) {} - private notis$: Subject = new Subject(); + private notis$: Subject = new Subject(); private observer = this.notis$.asObservable(); private readonly queueName = 'audio'; @@ -59,12 +59,13 @@ export class NotificationsService { targetUserKakaoId, type, }); - // Redis 큐에 이벤트를 전송 - await this.redisQueue.add(this.queueName, data); - return await this.notificationsRepository.fetchOne({ + const response = await this.notificationsRepository.fetchOne({ id: data.id, targetUserKakaoId, }); + // Redis 큐에 이벤트를 전송 + await this.redisQueue.add(this.queueName, response); + return response; } catch (e) { throw new BadRequestException('대상을 찾을 수 없습니다.'); } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index a119485..297d3cf 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -96,7 +96,7 @@ export class PostsService { .createQueryBuilder('pg') .where('pg.id = :id', { id: posts.postBackgroundId }) .getOne(); - if (!pg && !passNonEssentail) + if (!pg && posts.postBackgroundId && !passNonEssentail) throw new BadRequestException('존재하지 않는 post_background입니다.'); const us = await this.dataSource .getRepository(User) From 9afa95c299b7861f5dd62d2c72859d070f8e6334 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 11 Jun 2024 02:01:48 +0900 Subject: [PATCH 171/236] fix: change sse timeout to 10m --- src/APIs/notifications/notifications.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index ab5961c..9567a45 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -6,6 +6,7 @@ import { Post, Query, Req, + Res, Sse, UseGuards, } from '@nestjs/common'; @@ -17,7 +18,7 @@ import { ApiProduces, ApiTags, } from '@nestjs/swagger'; -import { Request } from 'express'; +import { Request, Response } from 'express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; @@ -62,8 +63,14 @@ export class NotificationsController { @UseGuards(AuthGuardV2) async fetchNoti( @Req() req: Request, + @Res() res: Response, @Query() fetchNotiInput: FetchNotiInput, ): Promise { + res.setTimeout(60 * 10000); // 600초로 설정, 필요에 따라 변경 가능 + req.on('close', () => { + subscription.unsubscribe(); + res.end(); + }); const kakaoId = req.user.userId; return await this.notificationsService.fetch({ kakaoId, From 5afdc47c61c2413f01766005aa9ab2e7de01b67d Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 11 Jun 2024 05:22:23 +0900 Subject: [PATCH 172/236] fix: block self emitting notification --- deploy/deploy.sh | 2 +- deploy/nginx.conf | 18 ++++++++++++++++-- src/APIs/comments/comments.service.ts | 3 +++ src/APIs/likes/likes.service.ts | 12 +++++++----- .../notifications/notifications.controller.ts | 16 ++++++++-------- .../notifications/notifications.service.ts | 1 - 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 0d27ec6..f7e2e66 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -88,6 +88,6 @@ ssh -i $PEM_PATH $SERVER "sudo docker stop $OLD_SERVICE_NAME" ssh -i $PEM_PATH $SERVER "sudo docker rm $OLD_SERVICE_NAME" echo -e "$ECR_URL/$SERVICE_NAME:$DOCKER_TAG" ssh -i $PEM_PATH $SERVER "docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi" -# yes | ssh -i $PEM_PATH $SERVER "sudo docker system prune -a" +ssh -i $PEM_PATH $SERVER "yes | sudo docker system prune -a" echo -e "\n## 배포 완료. $NEW_SERVICE_NAME ##\n" \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 4b32f01..fede971 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -25,6 +25,22 @@ http { index index.html index.htm; } + location /notifications/subscribe { + proxy_pass http://blccu-backend; + proxy_read_timeout 3600s; # 1시간으로 설정 + proxy_send_timeout 3600s; # 1시간으로 설정 + proxy_connect_timeout 3600s; # 1시간으로 설정 + } + # proxy_buffering off; # SSE의 경우 버퍼링 비활성화 + # chunked_transfer_encoding off; # chunked 전송 인코딩 비활성화 + # proxy_cache off; # 프록시 캐시 비활성화 + # proxy_set_header Connection ''; # 연결을 끊지 않도록 설정 + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } + location /metrics { proxy_pass http://localhost:9100/metrics; # Node Exporter의 주소 proxy_set_header Host $host; @@ -53,5 +69,3 @@ http { server_name api.blccu.com; return 404; # managed by Certbot - - }} \ No newline at end of file diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index c54a5e5..cd23f57 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -77,6 +77,9 @@ export class CommentsService { const { posts, parent, ...result } = await this.commentsRepository.fetchCommentWithNotiInfo({ id }); + // 자신에게 알람 보내는 경우 바로 return + if (result.userKakaoId === posts.userKakaoId) return result; + await this.notificationsService.emitAlarm({ userKakaoId: result.userKakaoId, targetUserKakaoId: posts.userKakaoId, diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index e5178ff..d97366a 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -47,11 +47,13 @@ export class LikesService { like_count: () => 'like_count +1', }); await await queryRunner.commitTransaction(); - await this.notificationsService.emitAlarm({ - userKakaoId: kakaoId, - targetUserKakaoId: postData.userKakaoId, - type: NotType.LIKE, - }); + if (kakaoId != postData.userKakaoId) { + await this.notificationsService.emitAlarm({ + userKakaoId: kakaoId, + targetUserKakaoId: postData.userKakaoId, + type: NotType.LIKE, + }); + } return likeData; } } catch (e) { diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index 9567a45..ed6165d 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -22,6 +22,7 @@ import { Request, Response } from 'express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; +import { interval, map, merge } from 'rxjs'; @ApiTags('알림 API') @Controller('notifications') @@ -44,12 +45,17 @@ export class NotificationsController { @ApiProduces('text/event-stream') @UseGuards(AuthGuardV2) @Sse('subscribe') - connectUser(@Req() req: Request) { + connectUser(@Req() req: Request, @Res() res: Response) { const targetUserKakaoId = req.user.userId; + res.setTimeout(60 * 10000); // 600초로 설정, 필요에 따라 변경 가능 nginx도 함께 변경할 것. + const sseStream = this.notificationsService.connectUser({ targetUserKakaoId, }); - return sseStream; + const pingStream = interval(30000).pipe( + map(() => ({ type: 'ping', data: 'keep-alive' })), + ); + return merge(sseStream, pingStream); } @ApiOperation({ @@ -63,14 +69,8 @@ export class NotificationsController { @UseGuards(AuthGuardV2) async fetchNoti( @Req() req: Request, - @Res() res: Response, @Query() fetchNotiInput: FetchNotiInput, ): Promise { - res.setTimeout(60 * 10000); // 600초로 설정, 필요에 따라 변경 가능 - req.on('close', () => { - subscription.unsubscribe(); - res.end(); - }); const kakaoId = req.user.userId; return await this.notificationsService.fetch({ kakaoId, diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index 4df4986..ef92772 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable, MessageEvent } from '@nestjs/common'; import { NotificationsRepository } from './notifications.repository'; import { Observable, Subject, filter, map } from 'rxjs'; -import { Notification } from './entities/notification.entity'; import { EmitNotiDto } from './dtos/emit-noti.dto'; import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; import { DateOption } from 'src/common/enums/date-option'; From 1c8008e4e00cc08daa3ac4355c0f73ba2630bd75 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 12 Jun 2024 17:41:31 +0900 Subject: [PATCH 173/236] fix: send notification for replies to self-comments on own posts --- src/APIs/comments/comments.service.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index cd23f57..9a0f438 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -77,21 +77,22 @@ export class CommentsService { const { posts, parent, ...result } = await this.commentsRepository.fetchCommentWithNotiInfo({ id }); - // 자신에게 알람 보내는 경우 바로 return - if (result.userKakaoId === posts.userKakaoId) return result; - - await this.notificationsService.emitAlarm({ - userKakaoId: result.userKakaoId, - targetUserKakaoId: posts.userKakaoId, - type: NotType.COMMENT, - }); - if (result.parentId) { + if (result.parentId && parent.userKakaoId != result.userKakaoId) { await this.notificationsService.emitAlarm({ userKakaoId: result.userKakaoId, targetUserKakaoId: parent.userKakaoId, type: NotType.REPLY, }); } + // 자신에게 알림 보내는 경우 생략 + if (result.userKakaoId != posts.userKakaoId) { + await this.notificationsService.emitAlarm({ + userKakaoId: result.userKakaoId, + targetUserKakaoId: posts.userKakaoId, + type: NotType.COMMENT, + }); + } + return result; } From 65b457c40a2a55f9e312987818df7b1cac1afd74 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 12 Jun 2024 17:58:52 +0900 Subject: [PATCH 174/236] feat: filter soft_deleted comments not having chidlren --- src/APIs/comments/comments.repository.ts | 15 +++++++++------ src/APIs/users/users.controller.ts | 2 ++ src/APIs/users/users.service.ts | 2 ++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 5f76375..6b9a20a 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -37,7 +37,7 @@ export class CommentsRepository extends Repository { async fetchComments({ postsId, }: ICommentsRepositoryfetchComments): Promise { - const comments = await this.createQueryBuilder('c') + let comments = await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') .addSelect([ @@ -62,11 +62,14 @@ export class CommentsRepository extends Repository { .addOrderBy('children.date_created', 'ASC') .getMany(); - // comments.forEach((comment) => { - // comment.children = comment.children.filter( - // (child) => child.date_deleted === null, - // ); - // }); + comments = comments.filter((comment) => { + comment.children = comment.children.filter( + (child) => child.date_deleted === null, + ); + // comment.children.length가 0이고 comment.date_deleted가 null이 아닌 경우를 제외 + return !(comment.children.length === 0 && comment.date_deleted !== null); + }); + return comments; } } diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index d2d8780..081e5b9 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -20,6 +20,7 @@ import { ApiCookieAuth, ApiCreatedResponse, ApiNoContentResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -88,6 +89,7 @@ export class UsersController { description: 'handle이 일치하는 유저 프로필을 조회한다.', }) @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) + @ApiNotFoundResponse({ description: '핸들에 일치하는 유저가 없습니다' }) @HttpCode(200) @Get('profile/handle/:handle') async findUserByHandle( diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 1c3c5f2..c58e65e 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, ConflictException, Injectable, + NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { @@ -102,6 +103,7 @@ export class UsersService { select: USER_SELECT_OPTION, where: { handle }, }); + if (!result) throw new NotFoundException('유저를 찾을 수 없습니다.'); return result; } From 445e748e9f8175b243bb0f4c2a63f1efba329f43 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 12 Jun 2024 23:09:53 +0900 Subject: [PATCH 175/236] fix: comments fetching filtering --- src/APIs/users/users.controller.ts | 2 -- src/APIs/users/users.service.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index 081e5b9..d2d8780 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -20,7 +20,6 @@ import { ApiCookieAuth, ApiCreatedResponse, ApiNoContentResponse, - ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -89,7 +88,6 @@ export class UsersController { description: 'handle이 일치하는 유저 프로필을 조회한다.', }) @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) - @ApiNotFoundResponse({ description: '핸들에 일치하는 유저가 없습니다' }) @HttpCode(200) @Get('profile/handle/:handle') async findUserByHandle( diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index c58e65e..1c3c5f2 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, ConflictException, Injectable, - NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { @@ -103,7 +102,6 @@ export class UsersService { select: USER_SELECT_OPTION, where: { handle }, }); - if (!result) throw new NotFoundException('유저를 찾을 수 없습니다.'); return result; } From 5a80db745cba409980ff5feb7d1b1b5f4ff7226a Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 14 Jun 2024 22:11:09 +0900 Subject: [PATCH 176/236] feat: delete user's information when soft deleting user --- src/APIs/posts/posts.controller.ts | 2 ++ src/APIs/users/users.service.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index c85c5ba..375ad7c 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -249,6 +249,8 @@ export class PostsController { description: '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) @Get('cursor/friends') @ApiOkResponse({ type: CursorPagePostResponseDto }) async fetchFriendsCursor( diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 1c3c5f2..a9d1bf5 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -27,6 +27,7 @@ import { User } from './entities/user.entity'; import { Comment } from '../comments/entities/comment.entity'; import { Feedback } from '../feedbacks/entities/feedback.entity'; import { AwsService } from 'src/modules/aws/aws.service'; +import { Agreement } from '../agreements/entities/agreement.entity'; @Injectable() export class UsersService { @@ -270,6 +271,17 @@ export class UsersService { 1, ); } + await queryRunner.manager.delete(Agreement, { userKakaoId: kakaoId }); + const userData = await queryRunner.manager.findOne(User, { + where: { kakaoId }, + }); + const userTempName = 'USER' + this.utilsService.getUUID().substring(0, 8); + userData.username = userTempName; + userData.handle = userTempName; + userData.description = ''; + userData.profile_image = ''; + userData.background_image = ''; + await queryRunner.manager.save(User, userData); await queryRunner.manager.softDelete(User, { kakaoId }); await queryRunner.commitTransaction(); return; From 4270de448bc35a482de069b64e24c05203198d43 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 15 Jun 2024 12:17:13 +0900 Subject: [PATCH 177/236] feat: delete notification when deleting user --- src/APIs/users/users.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index a9d1bf5..6839e00 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -234,6 +234,9 @@ export class UsersService { }); } } + await queryRunner.manager.delete(Notification, { + targetUserKakaoId: kakaoId, + }); await queryRunner.manager.softDelete(Posts, { userKakaoId: kakaoId }); // 연동된 댓글 soft delete await queryRunner.manager.softDelete(Comment, { userKakaoId: kakaoId }); From 1faf65ed154cdc0032ab9cdc1546f88c31c0dc3d Mon Sep 17 00:00:00 2001 From: do-huni Date: Sun, 16 Jun 2024 00:51:56 +0900 Subject: [PATCH 178/236] feat: add fk post & postId column on notification --- src/APIs/auth/auth.controller.ts | 1 + src/APIs/comments/comments.service.ts | 2 ++ src/APIs/follows/follows.service.ts | 1 + src/APIs/likes/likes.service.ts | 1 + src/APIs/notifications/dtos/emit-noti.dto.ts | 10 +++++++-- src/APIs/notifications/dtos/fetch-noti.dto.ts | 1 + .../entities/notification.entity.ts | 22 +++++++++++++++++++ 7 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index d41e3e5..4e82503 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -60,6 +60,7 @@ export class AuthController { res.cookie('isLoggedIn', true, { httpOnly: false, domain: clientDomain }); return res.redirect(process.env.CLIENT_URL); + // return res.send(); } @ApiOperation({ diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 9a0f438..7982321 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -82,6 +82,7 @@ export class CommentsService { userKakaoId: result.userKakaoId, targetUserKakaoId: parent.userKakaoId, type: NotType.REPLY, + postId: result.postsId, }); } // 자신에게 알림 보내는 경우 생략 @@ -90,6 +91,7 @@ export class CommentsService { userKakaoId: result.userKakaoId, targetUserKakaoId: posts.userKakaoId, type: NotType.COMMENT, + postId: result.postsId, }); } diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index e0b46a5..f0ca490 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -112,6 +112,7 @@ export class FollowsService { userKakaoId: from_user, targetUserKakaoId: to_user, type: NotType.FOLLOW, + postId: null, }); return await this.followsRepository.findOne({ where: { id: follow.id } }); } catch (e) { diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index d97366a..273728e 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -52,6 +52,7 @@ export class LikesService { userKakaoId: kakaoId, targetUserKakaoId: postData.userKakaoId, type: NotType.LIKE, + postId: postData.id, }); } return likeData; diff --git a/src/APIs/notifications/dtos/emit-noti.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts index 3d8809e..9ad823f 100644 --- a/src/APIs/notifications/dtos/emit-noti.dto.ts +++ b/src/APIs/notifications/dtos/emit-noti.dto.ts @@ -1,9 +1,15 @@ -import { OmitType, PickType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; import { Notification } from '../entities/notification.entity'; export class EmitNotiDto extends PickType(Notification, [ 'userKakaoId', 'targetUserKakaoId', 'type', -]) {} +]) { + @ApiProperty({ + type: Number, + description: '알림이 발생한 게시글 id(nullable)', + }) + postId: number; +} export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts index 34c9be1..8782816 100644 --- a/src/APIs/notifications/dtos/fetch-noti.dto.ts +++ b/src/APIs/notifications/dtos/fetch-noti.dto.ts @@ -31,6 +31,7 @@ export class FetchNotiDto extends FetchNotiInput { export class FetchNotiResponse extends OmitType(Notification, [ 'targetUser', 'user', + 'post', ]) { @ApiProperty({ type: PickType(User, ['username', 'profile_image', 'handle']), diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 33cd522..88406a7 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Posts } from 'src/APIs/posts/entities/posts.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { NotType } from 'src/common/enums/not-type.enum'; import { @@ -53,4 +54,25 @@ export class Notification { @ApiProperty({ description: '삭제된 날짜', type: Date }) @DeleteDateColumn() date_deleted: Date; + + @ApiProperty({ + type: Number, + description: '알림이 발생한 게시글 id(nullable)', + }) + @Column({ nullable: true }) + @RelationId((notification: Notification) => notification.post) + postId: number; + + @ApiProperty({ + type: Posts, + description: '알림이 연결된 게시물', + nullable: true, + }) + @JoinColumn() + @ManyToOne(() => Posts, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }) // 게시물을 참조하는 경우 + post: Posts; } From 5374651d39629635e9f4abebe9fc80d8cf4dfd44 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 24 Jun 2024 17:05:07 +0900 Subject: [PATCH 179/236] docs: fix noti entity swagger description --- src/APIs/notifications/entities/notification.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 88406a7..85bbdaa 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -65,7 +65,7 @@ export class Notification { @ApiProperty({ type: Posts, - description: '알림이 연결된 게시물', + description: '알림이 발생한 게시물', nullable: true, }) @JoinColumn() From e48555e63b7f584341fd8df1e412120113041157 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 24 Jun 2024 18:21:59 +0900 Subject: [PATCH 180/236] feat: check mutual follow condition and decide scope when cursor fetching friends' posts --- .../notifications/notifications.controller.ts | 7 --- .../interfaces/posts.repository.interface.ts | 2 +- src/APIs/posts/posts.repository.ts | 53 ++++++++++++++++--- src/APIs/posts/posts.service.ts | 8 +-- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index ed6165d..b3d49fc 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -29,13 +29,6 @@ import { interval, map, merge } from 'rxjs'; export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} - /* - 로직: 유저가 접속하면 알림을 구독한다. - 최초 구독 시 알림을 한번 fetch 한다. getManyAndCount로 개수 체크해서 메인에 띄워줌 - 댓글을 달거나 등 알림이 생기면 kakaoId 옵저버에 next로 이벤트 갱신해준다. - 갱신이 발생하면 sub해둔 프론트가 refetching을 한다. - 브라우저를 끄거나 refetching이 한동안 일어나지 않으면 sse를 끊는다. - */ @ApiOperation({ summary: '[SSE] 알림을 구독한다.', description: diff --git a/src/APIs/posts/interfaces/posts.repository.interface.ts b/src/APIs/posts/interfaces/posts.repository.interface.ts index 8b505fa..8331abd 100644 --- a/src/APIs/posts/interfaces/posts.repository.interface.ts +++ b/src/APIs/posts/interfaces/posts.repository.interface.ts @@ -22,7 +22,7 @@ export interface IPostsRepoFetchPostsCursor export interface IPostsRepoFetchFriendsPostsCursor extends Pick { date_filter: Date; - subQuery: string; + kakaoId: number; } export interface IPostsRepoFetchUserPostsCursor diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/posts/posts.repository.ts index 8626888..4187396 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/posts/posts.repository.ts @@ -1,4 +1,4 @@ -import { DataSource, Repository } from 'typeorm'; +import { Brackets, DataSource, Repository } from 'typeorm'; import { Posts } from './entities/posts.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/common/enums/open-scope.enum'; @@ -13,6 +13,7 @@ import { IPostsRepoFetchUserPostsCursor, IPostsRepoGetCursorQuery, } from './interfaces/posts.repository.interface'; +import { Follow } from '../follows/entities/follow.entity'; @Injectable() export class PostsRepository extends Repository { constructor(private dataSource: DataSource) { @@ -195,17 +196,45 @@ export class PostsRepository extends Repository { async fetchFriendsPostsCursor({ cursorOption, - subQuery, + kakaoId, date_filter, }: IPostsRepoFetchFriendsPostsCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); + // const subQuery = await this.dataSource + // .createQueryBuilder(Follow, 'n') + // .select('n.toUserKakaoId') + // .where('n.fromUserKakaoId = :kakaoId', { kakaoId }) + // .getQuery(); + + const mutualFollows = await this.dataSource + .createQueryBuilder(Follow, 'f1') + .select('f1.fromUserKakaoId', 'user1') + .addSelect('f1.toUserKakaoId', 'user2') + .innerJoin( + Follow, + 'f2', + 'f1.fromUserKakaoId = f2.toUserKakaoId AND f1.toUserKakaoId = f2.fromUserKakaoId', + ); + queryBuilder - .andWhere(`p.userKakaoId = any(${subQuery})`) // 만약 서로이웃으로 scope하려면, 정반대 옵션으로 subQuery2를 만들고 andWhere()하나 추가하면 될듯 - .andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC], - }); //sql injection 방지를 위해 만드시 enum 거칠 것 + .innerJoin(Follow, 'f', 'p.userKakaoId = f.toUserKakaoId') + .leftJoin( + `(${mutualFollows.getQuery()})`, + 'mf', + 'p.userKakaoId = mf.user1 AND f.fromUserKakaoId = mf.user2', + ) + .where('f.fromUserKakaoId = :kakaoId', { kakaoId }) + .andWhere( + new Brackets((qb) => { + qb.where('mf.user1 IS NOT NULL AND p.scope IN (:...scopes)', { + scopes: ['PUBLIC', 'PROTECTED'], + }).orWhere('mf.user1 IS NULL AND p.scope = :publicScope', { + publicScope: 'PUBLIC', + }); + }), + ); if (date_filter) { queryBuilder.andWhere('p.date_created > :date_filter', { @@ -213,6 +242,18 @@ export class PostsRepository extends Repository { }); } + // queryBuilder + // .andWhere(`p.userKakaoId = any(${subQuery})`) // 만약 서로이웃으로 scope하려면, 정반대 옵션으로 subQuery2를 만들고 andWhere()하나 추가하면 될듯 + // .andWhere('p.scope IN (:...scopes)', { + // scopes: [OpenScope.PUBLIC], + // }); //sql injection 방지를 위해 만드시 enum 거칠 것 + + // if (date_filter) { + // queryBuilder.andWhere('p.date_created > :date_filter', { + // date_filter: date_filter, + // }); + // } + const posts: Posts[] = await queryBuilder.getMany(); return { posts }; diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 297d3cf..56aa65b 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -308,14 +308,10 @@ export class PostsService { let date_filter: Date; if (cursorOption.date_created) date_filter = this.getDate(cursorOption.date_created); - const subQuery = await this.dataSource - .createQueryBuilder(Follow, 'n') - .select('n.toUserKakaoId') - .where(`n.fromUserKakaoId = ${kakaoId}`) - .getQuery(); + const { posts } = await this.postsRepository.fetchFriendsPostsCursor({ cursorOption, - subQuery, + kakaoId, date_filter, }); return await this.createCursorResponse({ posts, cursorOption }); From ab3a65ca52fffc10045791f8d3b346d5dcbab4b1 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 24 Jun 2024 18:28:47 +0900 Subject: [PATCH 181/236] fix: temporary change username searching --- src/APIs/users/users.repository.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index d18c837..34f461d 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -45,12 +45,12 @@ export class UsersRepository extends Repository { async fetchUsersWithNameAndFollowing({ kakaoId, username }) { const queryBuilder = this.getFollowQuery({ kakaoId }); const users = await queryBuilder - .andWhere('MATCH(user.username) AGAINST (:username IN BOOLEAN MODE)', { - username: `*${username}*`, - }) - // .andWhere('LOWER(user.username) LIKE LOWER(:username)', { - // username: `%${username}%`, + // .andWhere('MATCH(user.username) AGAINST (:username IN BOOLEAN MODE)', { + // username: `*${username}*`, // }) + .andWhere('LOWER(user.username) LIKE LOWER(:username)', { + username: `%${username}%`, + }) .getRawMany(); return users.map((user) => ({ From 8cef506a8a13c8dbe313ff920985f5e948c737ad Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 2 Jul 2024 01:11:25 +0900 Subject: [PATCH 182/236] feat: upload stickerBlock if publish post --- src/APIs/posts/dtos/create-post.input.ts | 17 ++++++++++++++++- src/APIs/posts/dtos/delete-post.dto.ts | 13 +++++++++++++ src/APIs/posts/dtos/publish-post.dto.ts | 13 +++++++++++-- src/APIs/posts/dtos/publish-post.input.ts | 16 +++++++++++++++- src/APIs/posts/posts.controller.ts | 15 ++++++++++++--- src/APIs/posts/posts.service.ts | 12 +++++++++--- src/APIs/users/entities/user.entity.ts | 2 +- 7 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/APIs/posts/dtos/delete-post.dto.ts diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/posts/dtos/create-post.input.ts index d6b49a8..4ab23ca 100644 --- a/src/APIs/posts/dtos/create-post.input.ts +++ b/src/APIs/posts/dtos/create-post.input.ts @@ -1,8 +1,23 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsEnum, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { BulkInsertStickerInput } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; import { OpenScope } from 'src/common/enums/open-scope.enum'; export class CreatePostInput { + @ApiProperty({ type: [BulkInsertStickerInput] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => BulkInsertStickerInput) + stickerBlocks: BulkInsertStickerInput[]; + @ApiProperty({ description: '연결된 카테고리 fk', type: String, diff --git a/src/APIs/posts/dtos/delete-post.dto.ts b/src/APIs/posts/dtos/delete-post.dto.ts new file mode 100644 index 0000000..d47109f --- /dev/null +++ b/src/APIs/posts/dtos/delete-post.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class DeletePostInput { + @ApiProperty({ + description: '물리 삭제 여부(nullable)', + required: false, + nullable: true, + }) + @IsBoolean() + @IsOptional() + isHardDelete?: boolean; +} diff --git a/src/APIs/posts/dtos/publish-post.dto.ts b/src/APIs/posts/dtos/publish-post.dto.ts index 203b755..757dbe2 100644 --- a/src/APIs/posts/dtos/publish-post.dto.ts +++ b/src/APIs/posts/dtos/publish-post.dto.ts @@ -1,8 +1,17 @@ -import { OmitType } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Posts } from '../entities/posts.entity'; +import { CreateStickerBlocksResponseDto } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; -export class PublishPostDto extends OmitType(Posts, [ +export class PublishPostResult extends OmitType(Posts, [ 'postBackground', 'user', 'postCategory', ]) {} + +export class PublishPostDto { + @ApiProperty({ type: [PublishPostResult] }) + postData: PublishPostResult; + + @ApiProperty({ type: [CreateStickerBlocksResponseDto] }) + stickerBlockData: CreateStickerBlocksResponseDto[]; +} diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/posts/dtos/publish-post.input.ts index 6f27800..e077376 100644 --- a/src/APIs/posts/dtos/publish-post.input.ts +++ b/src/APIs/posts/dtos/publish-post.input.ts @@ -1,9 +1,23 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsEnum, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { BulkInsertStickerInput } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { IsBoolean } from 'src/common/validators/isBoolean'; export class PublishPostInput { + @ApiProperty({ type: [BulkInsertStickerInput] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => BulkInsertStickerInput) + stickerBlocks: BulkInsertStickerInput[]; + @ApiProperty({ description: '연결된 카테고리 fk', type: String, diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/posts/posts.controller.ts index 375ad7c..17ee53b 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/posts/posts.controller.ts @@ -44,6 +44,7 @@ import { SortOption } from 'src/common/enums/sort-option'; import { CursorFetchPosts } from './dtos/cursor-fetch-posts.dto'; import { CursorPagePostResponseDto } from './dtos/cursor-page-post-response.dto'; import { PatchPostInput } from './dtos/patch-post.dto'; +import { DeletePostInput } from './dtos/delete-post.dto'; @ApiTags('게시글 API') @Controller('posts') @@ -67,14 +68,22 @@ export class PostsController { } @ApiOperation({ - summary: '게시글 논리 삭제', - description: '로그인 된 유저의 postId에 해당하는 게시글을 논리삭제한다.', + summary: '게시글 삭제', + description: + '로그인 된 유저의 postId에 해당하는 게시글을 삭제한다. isHardDelete(nullable)을 통해 삭제 방식 결정', }) @ApiCookieAuth() @UseGuards(AuthGuardV2) @Delete(':postId') - async softDelete(@Req() req: Request, @Param('postId') id: number) { + async softDelete( + @Req() req: Request, + @Param('postId') id: number, + @Body() body: DeletePostInput, + ) { const kakaoId = req.user.userId; + if (body.isHardDelete === true) { + return await this.postsService.hardDelete({ kakaoId, id }); + } return await this.postsService.softDelete({ kakaoId, id }); } diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/posts/posts.service.ts index 56aa65b..b11ee94 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/posts/posts.service.ts @@ -45,6 +45,7 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { AwsService } from 'src/modules/aws/aws.service'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; +import { PublishPostDto } from './dtos/publish-post.dto'; @Injectable() export class PostsService { @@ -106,7 +107,7 @@ export class PostsService { if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); } - async save(createPostDto: IPostsServiceCreate) { + async save(createPostDto: IPostsServiceCreate): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -129,10 +130,15 @@ export class PostsService { .values(post) .execute(); await queryRunner.commitTransaction(); - const result = this.postsRepository.findOne({ + const postData = await this.postsRepository.findOne({ where: { id: data.identifiers[0].id }, }); - return result; + const stickerBlockData = await this.stickerBlocksService.bulkInsert({ + postsId: postData.id, + kakaoId: createPostDto.userKakaoId, + stickerBlocks: createPostDto.stickerBlocks, + }); + return { postData, stickerBlockData }; } catch (e) { await queryRunner.rollbackTransaction(); throw e; diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 0057ee6..57cb6bb 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -9,6 +9,7 @@ import { // PrimaryGeneratedColumn, } from 'typeorm'; +@Index('ngramUser', ['username'], { fulltext: true, parser: 'ngram' }) @Entity() export class User { @Column({ type: 'bigint', primary: true }) @@ -45,7 +46,6 @@ export class User { }) follower_count: number; - @Index({ fulltext: true, parser: 'ngram' }) @Column({ unique: true }) @ApiProperty({ description: '유저 이름', type: String }) username: string; From 5d4dbf384cf7389d11c0975c8529d6c3993cc0d3 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 14:09:45 +0900 Subject: [PATCH 183/236] feat: add refactoring dependency(@nestjs/devtools-integration) --- package-lock.json | 14 ++++++++++++++ package.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index e1fca0e..fc42b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/devtools-integration": "^0.1.6", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", "@nestjs/passport": "^10.0.3", @@ -2846,6 +2847,19 @@ } } }, + "node_modules/@nestjs/devtools-integration": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/devtools-integration/-/devtools-integration-0.1.6.tgz", + "integrity": "sha512-7v2Oa7g2zxYB2DfRz5RsRyNOlrbPc32NG3FowyI3nGCOWtNaRAuZtAx7LjdnIphQGXDuIgVNMLROQaKpxI1eWg==", + "dependencies": { + "chalk": "^4.1.2", + "node-fetch": "^2.6.9" + }, + "peerDependencies": { + "@nestjs/common": "^9.3.7 || ^10.0.0", + "@nestjs/core": "^9.3.7 || ^10.0.0" + } + }, "node_modules/@nestjs/jwt": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", diff --git a/package.json b/package.json index c594e8f..37c6bb9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/devtools-integration": "^0.1.6", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", "@nestjs/passport": "^10.0.3", From 71fd55ee965783cdb7610752e6e89618981cfd57 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 14:11:49 +0900 Subject: [PATCH 184/236] feat: add DevToolsModule on appModule --- src/app.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 5a96394..b806250 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,6 +30,7 @@ import { redisStore } from 'cache-manager-redis-yet'; import { TerminusModule } from '@nestjs/terminus'; import { HttpModule } from '@nestjs/axios'; import { MetricsModule } from './modules/metrics/metrics.module'; +import { DevtoolsModule } from '@nestjs/devtools-integration'; @Module({ imports: [ @@ -55,6 +56,9 @@ import { MetricsModule } from './modules/metrics/metrics.module'; ConfigModule.forRoot({ isGlobal: true, }), + DevtoolsModule.register({ + http: true, + }), JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], From 2990b163be079d14c500ca72a839a104f54d7e7a Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 14:55:27 +0900 Subject: [PATCH 185/236] refactor(agreement): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../agreements/entities/agreement.entity.ts | 34 ++++++------------- src/APIs/auth/auth.module.ts | 1 - "src/common/\bentities/common.entity.ts" | 16 +++++++++ src/main.ts | 6 ++-- src/modules/aws/aws.module.ts | 3 +- 5 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 "src/common/\bentities/common.entity.ts" diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 7d3557f..3a5a8f7 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -1,23 +1,22 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum } from 'class-validator'; +import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/\bentities/common.entity'; import { AgreementType } from 'src/common/enums/agreement-type.enum'; import { Column, - CreateDateColumn, - DeleteDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId, - UpdateDateColumn, } from 'typeorm'; @Entity() -export class Agreement { +export class Agreement extends CommonEntity { @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @JoinColumn() @@ -29,34 +28,23 @@ export class Agreement { user: User; @ApiProperty({ type: Number, description: '약관에 동의한 유저 id' }) - @Column() + @Column({ name: 'user_id' }) @RelationId((agreement: Agreement) => agreement.user) - // @IsNumber() - userKakaoId: number; + @IsNumber() + userId: number; @ApiProperty({ type: 'enum', enum: AgreementType, description: '약관의 종류', + nullable: false, }) - @Column() + @Column({ name: 'agreement_type' }) @IsEnum(AgreementType) agreementType: AgreementType; - @ApiProperty({ type: Boolean, description: '약관 동의 유무' }) - @Column({ default: false }) + @ApiProperty({ type: Boolean, description: '약관 동의 유무', default: false }) + @Column({ name: 'is_agreed', default: false }) @IsBoolean() isAgreed: boolean; // 동의 여부, 기본값은 false - - @ApiProperty({ type: Date, description: '생성된 날짜' }) - @CreateDateColumn() - date_created: Date; - - @ApiProperty({ type: Date, description: '수정된 날짜' }) - @UpdateDateColumn() - date_updated: Date; - - @ApiProperty({ type: Date, description: '삭제된 날짜' }) - @DeleteDateColumn() - date_deleted: Date; } diff --git a/src/APIs/auth/auth.module.ts b/src/APIs/auth/auth.module.ts index 03dc025..2ac7467 100644 --- a/src/APIs/auth/auth.module.ts +++ b/src/APIs/auth/auth.module.ts @@ -9,7 +9,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ - ConfigModule, PassportModule, UsersModule, JwtModule.registerAsync({ diff --git "a/src/common/\bentities/common.entity.ts" "b/src/common/\bentities/common.entity.ts" new file mode 100644 index 0000000..13523b2 --- /dev/null +++ "b/src/common/\bentities/common.entity.ts" @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm'; + +export abstract class CommonEntity { + @ApiProperty({ type: Date, description: '생성된 날짜' }) + @CreateDateColumn({ name: 'date_created' }) + dateCreated: Date; + + @ApiProperty({ type: Date, description: '수정된 날짜' }) + @UpdateDateColumn({ name: 'date_updated' }) + dateUpdated: Date; + + @ApiProperty({ type: Date, description: '삭제된 날짜' }) + @DeleteDateColumn({ name: 'date_deleted' }) + date_deleted: Date; +} diff --git a/src/main.ts b/src/main.ts index 5ebe7ed..17ea2cf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,13 @@ import { HttpExceptionFilter } from './common/filter/http-exception.filter'; import { ValidationPipe } from '@nestjs/common'; import expressBasicAuth from 'express-basic-auth'; import { PrometheusInterceptor } from './common/interceptors/prometheus.interceptor'; -import { ResponseInterceptor } from './common/interceptors/response.interceptor'; // import * as expressBasicAuth from 'express-basic-auth'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create( + AppModule, + { snapshot: true }, // you should delete here! + ); app.use(cookieParser()); app.use( ['/api-docs'], diff --git a/src/modules/aws/aws.module.ts b/src/modules/aws/aws.module.ts index 9aa580a..e61e720 100644 --- a/src/modules/aws/aws.module.ts +++ b/src/modules/aws/aws.module.ts @@ -1,10 +1,9 @@ // aws.module.ts import { Module } from '@nestjs/common'; import { AwsService } from './aws.service'; -import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [ConfigModule], + imports: [], providers: [AwsService], exports: [AwsService], }) From a4f5ec1a70ebe53efa4414d1d57126219dc1d5c2 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 15:03:56 +0900 Subject: [PATCH 186/236] refactor(announcement): unify naming convention - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../agreements/entities/agreement.entity.ts | 2 +- .../entities/announcement.entity.ts | 24 +++---------------- .../common/entities/common.entity.ts | 0 3 files changed, 4 insertions(+), 22 deletions(-) rename "src/common/\bentities/common.entity.ts" => src/common/entities/common.entity.ts (100%) diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 3a5a8f7..6a665c9 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; -import { CommonEntity } from 'src/common/\bentities/common.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { AgreementType } from 'src/common/enums/agreement-type.enum'; import { Column, diff --git a/src/APIs/announcements/entities/announcement.entity.ts b/src/APIs/announcements/entities/announcement.entity.ts index f7de067..796a875 100644 --- a/src/APIs/announcements/entities/announcement.entity.ts +++ b/src/APIs/announcements/entities/announcement.entity.ts @@ -1,15 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { CommonEntity } from 'src/common/entities/common.entity'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() -export class Announcement { +export class Announcement extends CommonEntity { @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() id: number; @@ -21,16 +15,4 @@ export class Announcement { @ApiProperty({ type: String, description: '내용' }) @Column() content: string; - - @ApiProperty({ type: Date, description: '생성된 날짜' }) - @CreateDateColumn() - date_created: Date; - - @ApiProperty({ type: Date, description: '수정된 날짜' }) - @UpdateDateColumn() - date_updated: Date; - - @ApiProperty({ type: Date, description: '삭제된 날짜' }) - @DeleteDateColumn() - date_deleted: Date; } diff --git "a/src/common/\bentities/common.entity.ts" b/src/common/entities/common.entity.ts similarity index 100% rename from "src/common/\bentities/common.entity.ts" rename to src/common/entities/common.entity.ts From 72e81961bee0e2595d2f9b8d0b7f2a701fa89fd2 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 15:35:59 +0900 Subject: [PATCH 187/236] refactor(article): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) - change domain name post -> article --- .../articles.controller.ts} | 2 +- .../articles.module.ts} | 8 +- .../articles.repository.ts} | 4 +- .../articles.service.ts} | 4 +- .../dtos/create-post.input.ts | 0 .../dtos/cursor-fetch-posts.dto.ts | 0 .../dtos/cursor-page-post-response.dto.ts | 0 .../dtos/delete-post.dto.ts | 0 .../dtos/fetch-friends-posts.dto.ts | 0 .../dtos/fetch-post-detail.dto.ts | 0 .../dtos/fetch-post-for-update.dto.ts | 0 .../dtos/fetch-posts.dto.ts | 0 .../dtos/fetch-user-posts.dto.ts | 0 .../dtos/fetch-user-posts.input.ts | 0 .../dtos/page-post-response.dto.ts | 0 .../dtos/patch-post.dto.ts | 0 .../dtos/post-response.dto.ts | 2 +- .../dtos/publish-post.dto.ts | 2 +- .../dtos/publish-post.input.ts | 0 src/APIs/articles/entities/article.entity.ts | 191 ++++++++++++++++++ .../articles.repository.interface.ts} | 0 .../interfaces/posts.service.interface.ts | 2 +- src/APIs/comments/comments.service.ts | 2 +- src/APIs/comments/entities/comment.entity.ts | 2 +- src/APIs/likes/dtos/fetch-likes.dto.ts | 2 +- .../likes/dtos/toggle-like-response.dto.ts | 2 +- src/APIs/likes/entities/like.entity.ts | 2 +- src/APIs/likes/likes.module.ts | 2 +- src/APIs/likes/likes.service.ts | 2 +- .../entities/notification.entity.ts | 2 +- .../entities/postCategory.entity.ts | 2 +- src/APIs/posts/entities/posts.entity.ts | 140 ------------- src/APIs/reports/entities/report.entity.ts | 2 +- src/APIs/reports/reports.service.ts | 2 +- .../entities/stickerblock.entity.ts | 2 +- src/APIs/users/users.service.ts | 2 +- src/app.module.ts | 2 +- 37 files changed, 217 insertions(+), 166 deletions(-) rename src/APIs/{posts/posts.controller.ts => articles/articles.controller.ts} (99%) rename src/APIs/{posts/posts.module.ts => articles/articles.module.ts} (80%) rename src/APIs/{posts/posts.repository.ts => articles/articles.repository.ts} (98%) rename src/APIs/{posts/posts.service.ts => articles/articles.service.ts} (99%) rename src/APIs/{posts => articles}/dtos/create-post.input.ts (100%) rename src/APIs/{posts => articles}/dtos/cursor-fetch-posts.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/cursor-page-post-response.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/delete-post.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-friends-posts.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-post-detail.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-post-for-update.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-posts.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-user-posts.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/fetch-user-posts.input.ts (100%) rename src/APIs/{posts => articles}/dtos/page-post-response.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/patch-post.dto.ts (100%) rename src/APIs/{posts => articles}/dtos/post-response.dto.ts (89%) rename src/APIs/{posts => articles}/dtos/publish-post.dto.ts (90%) rename src/APIs/{posts => articles}/dtos/publish-post.input.ts (100%) create mode 100644 src/APIs/articles/entities/article.entity.ts rename src/APIs/{posts/interfaces/posts.repository.interface.ts => articles/interfaces/articles.repository.interface.ts} (100%) rename src/APIs/{posts => articles}/interfaces/posts.service.interface.ts (95%) delete mode 100644 src/APIs/posts/entities/posts.entity.ts diff --git a/src/APIs/posts/posts.controller.ts b/src/APIs/articles/articles.controller.ts similarity index 99% rename from src/APIs/posts/posts.controller.ts rename to src/APIs/articles/articles.controller.ts index 17ee53b..6a22fc8 100644 --- a/src/APIs/posts/posts.controller.ts +++ b/src/APIs/articles/articles.controller.ts @@ -23,7 +23,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { PostsService } from './posts.service'; +import { PostsService } from './articles.service'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PublishPostDto } from './dtos/publish-post.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; diff --git a/src/APIs/posts/posts.module.ts b/src/APIs/articles/articles.module.ts similarity index 80% rename from src/APIs/posts/posts.module.ts rename to src/APIs/articles/articles.module.ts index de63506..0c05df2 100644 --- a/src/APIs/posts/posts.module.ts +++ b/src/APIs/articles/articles.module.ts @@ -1,14 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Posts } from './entities/posts.entity'; +import { Posts } from './entities/article.entity'; import { User } from '../users/entities/user.entity'; -import { PostsController } from './posts.controller'; +import { PostsController } from './articles.controller'; import { UtilsModule } from 'src/utils/utils.module'; -import { PostsService } from './posts.service'; +import { PostsService } from './articles.service'; import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; -import { PostsRepository } from './posts.repository'; +import { PostsRepository } from './articles.repository'; import { FollowsModule } from '../follows/follows.module'; import { AwsModule } from 'src/modules/aws/aws.module'; diff --git a/src/APIs/posts/posts.repository.ts b/src/APIs/articles/articles.repository.ts similarity index 98% rename from src/APIs/posts/posts.repository.ts rename to src/APIs/articles/articles.repository.ts index 4187396..068be33 100644 --- a/src/APIs/posts/posts.repository.ts +++ b/src/APIs/articles/articles.repository.ts @@ -1,5 +1,5 @@ import { Brackets, DataSource, Repository } from 'typeorm'; -import { Posts } from './entities/posts.entity'; +import { Posts } from './entities/article.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { PostResponseDto } from './dtos/post-response.dto'; @@ -12,7 +12,7 @@ import { IPostsRepoFetchPostsCursor, IPostsRepoFetchUserPostsCursor, IPostsRepoGetCursorQuery, -} from './interfaces/posts.repository.interface'; +} from './interfaces/articles.repository.interface'; import { Follow } from '../follows/entities/follow.entity'; @Injectable() export class PostsRepository extends Repository { diff --git a/src/APIs/posts/posts.service.ts b/src/APIs/articles/articles.service.ts similarity index 99% rename from src/APIs/posts/posts.service.ts rename to src/APIs/articles/articles.service.ts index b11ee94..0c70026 100644 --- a/src/APIs/posts/posts.service.ts +++ b/src/APIs/articles/articles.service.ts @@ -9,7 +9,7 @@ import { import { UtilsService } from 'src/utils/utils.service'; import { DataSource } from 'typeorm'; -import { Posts } from './entities/posts.entity'; +import { Posts } from './entities/article.entity'; import { Page } from '../../utils/pages/page'; import { FetchPostsDto } from './dtos/fetch-posts.dto'; import { PagePostResponseDto } from './dtos/page-post-response.dto'; @@ -18,7 +18,7 @@ import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; -import { PostsRepository } from './posts.repository'; +import { PostsRepository } from './articles.repository'; import { PostOnlyResponseDto, PostResponseDto } from './dtos/post-response.dto'; import { FetchPostForUpdateDto, diff --git a/src/APIs/posts/dtos/create-post.input.ts b/src/APIs/articles/dtos/create-post.input.ts similarity index 100% rename from src/APIs/posts/dtos/create-post.input.ts rename to src/APIs/articles/dtos/create-post.input.ts diff --git a/src/APIs/posts/dtos/cursor-fetch-posts.dto.ts b/src/APIs/articles/dtos/cursor-fetch-posts.dto.ts similarity index 100% rename from src/APIs/posts/dtos/cursor-fetch-posts.dto.ts rename to src/APIs/articles/dtos/cursor-fetch-posts.dto.ts diff --git a/src/APIs/posts/dtos/cursor-page-post-response.dto.ts b/src/APIs/articles/dtos/cursor-page-post-response.dto.ts similarity index 100% rename from src/APIs/posts/dtos/cursor-page-post-response.dto.ts rename to src/APIs/articles/dtos/cursor-page-post-response.dto.ts diff --git a/src/APIs/posts/dtos/delete-post.dto.ts b/src/APIs/articles/dtos/delete-post.dto.ts similarity index 100% rename from src/APIs/posts/dtos/delete-post.dto.ts rename to src/APIs/articles/dtos/delete-post.dto.ts diff --git a/src/APIs/posts/dtos/fetch-friends-posts.dto.ts b/src/APIs/articles/dtos/fetch-friends-posts.dto.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-friends-posts.dto.ts rename to src/APIs/articles/dtos/fetch-friends-posts.dto.ts diff --git a/src/APIs/posts/dtos/fetch-post-detail.dto.ts b/src/APIs/articles/dtos/fetch-post-detail.dto.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-post-detail.dto.ts rename to src/APIs/articles/dtos/fetch-post-detail.dto.ts diff --git a/src/APIs/posts/dtos/fetch-post-for-update.dto.ts b/src/APIs/articles/dtos/fetch-post-for-update.dto.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-post-for-update.dto.ts rename to src/APIs/articles/dtos/fetch-post-for-update.dto.ts diff --git a/src/APIs/posts/dtos/fetch-posts.dto.ts b/src/APIs/articles/dtos/fetch-posts.dto.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-posts.dto.ts rename to src/APIs/articles/dtos/fetch-posts.dto.ts diff --git a/src/APIs/posts/dtos/fetch-user-posts.dto.ts b/src/APIs/articles/dtos/fetch-user-posts.dto.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-user-posts.dto.ts rename to src/APIs/articles/dtos/fetch-user-posts.dto.ts diff --git a/src/APIs/posts/dtos/fetch-user-posts.input.ts b/src/APIs/articles/dtos/fetch-user-posts.input.ts similarity index 100% rename from src/APIs/posts/dtos/fetch-user-posts.input.ts rename to src/APIs/articles/dtos/fetch-user-posts.input.ts diff --git a/src/APIs/posts/dtos/page-post-response.dto.ts b/src/APIs/articles/dtos/page-post-response.dto.ts similarity index 100% rename from src/APIs/posts/dtos/page-post-response.dto.ts rename to src/APIs/articles/dtos/page-post-response.dto.ts diff --git a/src/APIs/posts/dtos/patch-post.dto.ts b/src/APIs/articles/dtos/patch-post.dto.ts similarity index 100% rename from src/APIs/posts/dtos/patch-post.dto.ts rename to src/APIs/articles/dtos/patch-post.dto.ts diff --git a/src/APIs/posts/dtos/post-response.dto.ts b/src/APIs/articles/dtos/post-response.dto.ts similarity index 89% rename from src/APIs/posts/dtos/post-response.dto.ts rename to src/APIs/articles/dtos/post-response.dto.ts index 57a873a..e4d8760 100644 --- a/src/APIs/posts/dtos/post-response.dto.ts +++ b/src/APIs/articles/dtos/post-response.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Posts } from '../entities/posts.entity'; +import { Posts } from '../entities/article.entity'; export class PostResponseDto extends OmitType(Posts, ['user']) { @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) diff --git a/src/APIs/posts/dtos/publish-post.dto.ts b/src/APIs/articles/dtos/publish-post.dto.ts similarity index 90% rename from src/APIs/posts/dtos/publish-post.dto.ts rename to src/APIs/articles/dtos/publish-post.dto.ts index 757dbe2..cbbb323 100644 --- a/src/APIs/posts/dtos/publish-post.dto.ts +++ b/src/APIs/articles/dtos/publish-post.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Posts } from '../entities/posts.entity'; +import { Posts } from '../entities/article.entity'; import { CreateStickerBlocksResponseDto } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; export class PublishPostResult extends OmitType(Posts, [ diff --git a/src/APIs/posts/dtos/publish-post.input.ts b/src/APIs/articles/dtos/publish-post.input.ts similarity index 100% rename from src/APIs/posts/dtos/publish-post.input.ts rename to src/APIs/articles/dtos/publish-post.input.ts diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts new file mode 100644 index 0000000..1157abd --- /dev/null +++ b/src/APIs/articles/entities/article.entity.ts @@ -0,0 +1,191 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsEnum, + IsNumber, + IsOptional, + IsString, + IsUrl, +} from 'class-validator'; +import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; +import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; +import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; +import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + RelationId, +} from 'typeorm'; + +@Entity() +export class Article extends CommonEntity { + @ApiProperty({ description: '게시글의 고유 아이디', type: Number }) + @PrimaryGeneratedColumn() + @IsNumber() + id: number; + + @ApiProperty({ description: '연결된 카테고리 fk', type: String }) + @Column({ name: 'post_category_id', nullable: true }) + @RelationId((article: Article) => article.postCategory) + @IsString() + @IsOptional() + postCategoryId: string; + + @IsString() + @ApiProperty({ description: '연결된 내지 fk', type: String }) + @Column({ name: 'post_background_id', nullable: true }) + @RelationId((article: Article) => article.postBackground) + @IsString() + @IsOptional() + postBackgroundId: string; + + @ApiProperty({ description: '작성한 유저 fk', type: Number }) + @Column({ name: 'user_id', nullable: false }) + @RelationId((article: Article) => article.user) + @IsNumber() + userId: number; + + @ApiProperty({ description: '제목(최대 100자)', type: String, default: '' }) + @Column({ length: 100, default: '' }) + @IsString() + title: string; + + @ApiProperty({ description: 'html 적용된 제목', type: String, default: '' }) + @Column({ name: 'html_title', default: '' }) + @IsString() + htmlTitle: string; + + @ApiProperty({ + description: '임시저장(false), 발행(true)', + type: Boolean, + default: false, + }) + @Column({ name: 'is_published', default: false }) + @IsBoolean() + isPublished: boolean; + + @ApiProperty({ description: '좋아요 카운트', type: Number, default: 0 }) + @Column({ name: 'like_count', default: 0 }) + @IsNumber() + likeCount: number; + + @ApiProperty({ description: '조회수 카운트', type: Number, default: 0 }) + @Column({ name: 'view_count', default: 0 }) + @IsNumber() + viewCount: number; + + @ApiProperty({ description: '댓글수 카운트', type: Number, default: 0 }) + @Column({ name: 'comment_count', default: 0 }) + @IsNumber() + commentCount: number; + + @ApiProperty({ description: '신고수 카운트', type: Number, default: 0 }) + @Column({ name: 'report_count', default: 0 }) + @IsNumber() + reportCount: number; + + @ApiProperty({ + description: '댓글 허용 여부(boolean)', + type: Boolean, + default: true, + }) + @Column({ name: 'allow_comment', default: true }) + @IsBoolean() + allowComment: boolean; + + @ApiProperty({ + description: + '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', + type: 'enum', + enum: OpenScope, + default: OpenScope.PUBLIC, + }) + @Column({ default: OpenScope.PUBLIC }) + @IsEnum(OpenScope) + scope: OpenScope; + + @ApiProperty({ description: '게시글 내용', type: String }) + @Column('longtext') + @IsString() + content: string; + + @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) + @Column({ name: 'main_description' }) + @IsString() + mainDescription: string; + + @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) + @Column({ name: 'image_url', default: '' }) + @IsUrl() + imageUrl: string; + + @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) + @Column({ name: 'main_image_url', default: '' }) + @IsUrl() + mainImageUrl: string; + + @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) + @ManyToOne(() => PostCategory, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + @JoinColumn() + postCategory: PostCategory; + + @ApiProperty({ description: '연결된 내지', type: PostBackground }) + @JoinColumn() + @ManyToOne(() => PostBackground, { + nullable: true, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + postBackground: PostBackground; + + @ApiProperty({ description: '작성자', type: User }) + @JoinColumn() + @ManyToOne(() => User, { + nullable: false, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + user: User; + + @ApiProperty({ + type: () => [Comment], + description: '연결된 댓글', + nullable: true, + }) + @OneToMany(() => Comment, (comment) => comment.articleId) + comments: Comment[]; + + @ApiProperty({ + type: () => [Notification], + description: '연결된 알림', + nullable: true, + }) + @OneToMany(() => Notification, (notification) => notification.articleId) + notifications: Notification[]; + + @ApiProperty({ + type: () => [Report], + description: '연결된 신고', + nullable: true, + }) + @OneToMany(() => Report, (report) => report.articleId) + reports: Report[]; + + @ApiProperty({ + type: () => [StickerBlock], + description: '연결된 스티커블럭', + nullable: true, + }) + @OneToMany(() => StickerBlock, (stickerBlock) => stickerBlock.articleId) + stickerBlocks: StickerBlock[]; +} diff --git a/src/APIs/posts/interfaces/posts.repository.interface.ts b/src/APIs/articles/interfaces/articles.repository.interface.ts similarity index 100% rename from src/APIs/posts/interfaces/posts.repository.interface.ts rename to src/APIs/articles/interfaces/articles.repository.interface.ts diff --git a/src/APIs/posts/interfaces/posts.service.interface.ts b/src/APIs/articles/interfaces/posts.service.interface.ts similarity index 95% rename from src/APIs/posts/interfaces/posts.service.interface.ts rename to src/APIs/articles/interfaces/posts.service.interface.ts index d54bb1b..bb6e527 100644 --- a/src/APIs/posts/interfaces/posts.service.interface.ts +++ b/src/APIs/articles/interfaces/posts.service.interface.ts @@ -2,7 +2,7 @@ import { CreatePostInput } from '../dtos/create-post.input'; import { CursorFetchPosts } from '../dtos/cursor-fetch-posts.dto'; import { FetchUserPostsInput } from '../dtos/fetch-user-posts.input'; import { PatchPostInput } from '../dtos/patch-post.dto'; -import { Posts } from '../entities/posts.entity'; +import { Posts } from '../entities/article.entity'; export interface IPostsServicePostId extends Pick {} diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 7982321..0bc6327 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -7,7 +7,7 @@ import { import { CreateCommentDto } from './dtos/create-comment.dto'; import { CommentsRepository } from './comments.repository'; import { DataSource, EntityManager, UpdateResult } from 'typeorm'; -import { Posts } from '../posts/entities/posts.entity'; +import { Posts } from '../articles/entities/article.entity'; import { ChildrenComment, FetchCommentDto, diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index e758e21..0acf78e 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { Column, diff --git a/src/APIs/likes/dtos/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts index 4bab4b4..cb8b40a 100644 --- a/src/APIs/likes/dtos/fetch-likes.dto.ts +++ b/src/APIs/likes/dtos/fetch-likes.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; import { Likes } from '../entities/like.entity'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; export class FetchLikesDto { @ApiProperty({ type: Number, description: 'post_id' }) diff --git a/src/APIs/likes/dtos/toggle-like-response.dto.ts b/src/APIs/likes/dtos/toggle-like-response.dto.ts index f207fca..254837b 100644 --- a/src/APIs/likes/dtos/toggle-like-response.dto.ts +++ b/src/APIs/likes/dtos/toggle-like-response.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { Likes } from '../entities/like.entity'; export class ToggleLikeResponseDto extends OmitType(Posts, [ diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index 2f0138e..11b57d5 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { Column, diff --git a/src/APIs/likes/likes.module.ts b/src/APIs/likes/likes.module.ts index dcb09be..d87b6fd 100644 --- a/src/APIs/likes/likes.module.ts +++ b/src/APIs/likes/likes.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Posts } from '../posts/entities/posts.entity'; +import { Posts } from '../articles/entities/article.entity'; import { LikesController } from './likes.controller'; import { LikesService } from './likes.service'; import { Likes } from './entities/like.entity'; diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 273728e..2947789 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -1,7 +1,7 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Likes } from './entities/like.entity'; -import { Posts } from '../posts/entities/posts.entity'; +import { Posts } from '../articles/entities/article.entity'; import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; import { LikesRepository } from './likes.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 85bbdaa..b6769a6 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { NotType } from 'src/common/enums/not-type.enum'; import { diff --git a/src/APIs/postCategories/entities/postCategory.entity.ts b/src/APIs/postCategories/entities/postCategory.entity.ts index 14f144f..714617d 100644 --- a/src/APIs/postCategories/entities/postCategory.entity.ts +++ b/src/APIs/postCategories/entities/postCategory.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { Column, diff --git a/src/APIs/posts/entities/posts.entity.ts b/src/APIs/posts/entities/posts.entity.ts deleted file mode 100644 index 1c4ca2b..0000000 --- a/src/APIs/posts/entities/posts.entity.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; -import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; -import { User } from 'src/APIs/users/entities/user.entity'; -import { OpenScope } from 'src/common/enums/open-scope.enum'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - RelationId, - UpdateDateColumn, -} from 'typeorm'; - -@Entity() -export class Posts { - // 이름 충돌 때문에 posts 복수형으로 사용 - @ApiProperty({ description: '포스트의 고유 아이디', type: Number }) - @PrimaryGeneratedColumn() - id: number; - - @IsString() - @ApiProperty({ description: '연결된 카테고리 fk', type: String }) - @Column({ nullable: true }) - @RelationId((posts: Posts) => posts.postCategory) - postCategoryId: string; - - @IsString() - @ApiProperty({ description: '연결된 내지 fk', type: String }) - @Column({ nullable: true }) - @RelationId((posts: Posts) => posts.postBackground) - postBackgroundId: string; - - @ApiProperty({ description: '작성한 유저 fk', type: Number }) - @Column({ nullable: false }) - @RelationId((posts: Posts) => posts.user) - userKakaoId: number; - - @ApiProperty({ description: '제목(최대 100자)', type: String }) - @Column({ length: 100, default: '' }) - title: string; - - @ApiProperty({ description: '수정용 제목', type: String }) - @Column({ default: '' }) - title_html: string; - - @ApiProperty({ description: '임시저장(false), 발행(true)', type: Boolean }) - @Column({ default: false }) - isPublished: boolean; - - @ApiProperty({ description: '좋아요 카운트', type: Number }) - @Column({ default: 0 }) - like_count: number; - - @ApiProperty({ description: '조회수 카운트', type: Number }) - @Column({ default: 0 }) - view_count: number; - - @ApiProperty({ description: '댓글수 카운트', type: Number }) - @Column({ default: 0 }) - comment_count: number; - - @ApiProperty({ description: '신고수 카운트', type: Number }) - @Column({ default: 0 }) - report_count: number; - - @ApiProperty({ description: '댓글 허용 여부(boolean)', type: Boolean }) - @Column({ default: true }) - allow_comment: boolean; - - @ApiProperty({ - description: - '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', - type: 'enum', - enum: OpenScope, - }) - @Column({ default: 'PUBLIC' }) - scope: OpenScope; - - @Index() - @ApiProperty({ description: '생성된 날짜', type: Date }) - @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) - date_created: Date; - - @ApiProperty({ description: '수정된 날짜', type: Date }) - @UpdateDateColumn() - date_updated: Date; - - @ApiProperty({ description: 'soft delete column', type: Date }) - @DeleteDateColumn() - date_deleted: Date; - - @ApiProperty({ description: '게시글 내용', type: String }) - @Column('longtext') - content: string; - - @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) - @Column() - main_description: string; - - @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) - @Column({ default: '' }) - image_url: string; - - @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) - @Column({ default: '' }) - main_image_url: string; - - @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) - @ManyToOne(() => PostCategory, { - nullable: true, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - @JoinColumn() - postCategory: PostCategory; - - @ApiProperty({ description: '연결된 내지', type: PostBackground }) - @JoinColumn() - @ManyToOne(() => PostBackground, { - nullable: true, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - postBackground: PostBackground; - - @ApiProperty({ description: '작성자', type: User }) - @JoinColumn() - @ManyToOne(() => User, { - nullable: false, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) - user: User; -} diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 0f9a027..73bf38b 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum } from 'class-validator'; import { Comment } from 'src/APIs/comments/entities/comment.entity'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { ReportTarget } from 'src/common/enums/report-target.enum'; import { ReportType } from 'src/common/enums/report-type.enum'; diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index e2db30d..288194e 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -10,7 +10,7 @@ import { CreateReportDto } from './dtos/create-report.dto'; import { UsersService } from '../users/users.service'; import { FetchReportResponse } from './dtos/fetch-report.dto'; import { ReportTarget } from 'src/common/enums/report-target.enum'; -import { Posts } from '../posts/entities/posts.entity'; +import { Posts } from '../articles/entities/article.entity'; import { Comment } from '../comments/entities/comment.entity'; @Injectable() diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 5590e4f..4744a87 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/posts/entities/posts.entity'; +import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { Sticker } from 'src/APIs/stickers/entities/sticker.entity'; import { Column, diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 6839e00..930e803 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -21,7 +21,7 @@ import { UtilsService } from 'src/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; import { UsersRepository } from './users.repository'; import { DataSource, UpdateResult } from 'typeorm'; -import { Posts } from '../posts/entities/posts.entity'; +import { Posts } from '../articles/entities/article.entity'; import { Follow } from '../follows/entities/follow.entity'; import { User } from './entities/user.entity'; import { Comment } from '../comments/entities/comment.entity'; diff --git a/src/app.module.ts b/src/app.module.ts index b806250..b99d5c6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,7 +3,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsModule } from './APIs/comments/comments.module'; -import { PostsModule } from './APIs/posts/posts.module'; +import { PostsModule } from './APIs/articles/articles.module'; import { UsersModule } from './APIs/users/users.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; From 59b566bdecf1815425bc17c5331eeca5b8db4116 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 15:47:13 +0900 Subject: [PATCH 188/236] refactor(article): change layers name --- src/APIs/articles/articles.controller.ts | 144 +++++----- src/APIs/articles/articles.module.ts | 27 +- src/APIs/articles/articles.repository.ts | 96 +++---- src/APIs/articles/articles.service.ts | 250 ++++++++---------- .../articles.repository.interface.ts | 26 +- .../interfaces/articles.service.interface.ts | 44 +++ .../interfaces/posts.service.interface.ts | 48 ---- 7 files changed, 292 insertions(+), 343 deletions(-) create mode 100644 src/APIs/articles/interfaces/articles.service.interface.ts delete mode 100644 src/APIs/articles/interfaces/posts.service.interface.ts diff --git a/src/APIs/articles/articles.controller.ts b/src/APIs/articles/articles.controller.ts index 6a22fc8..04cc754 100644 --- a/src/APIs/articles/articles.controller.ts +++ b/src/APIs/articles/articles.controller.ts @@ -6,7 +6,7 @@ import { HttpCode, Param, Patch, - Post, + Article, Query, Req, UploadedFile, @@ -23,44 +23,47 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { PostsService } from './articles.service'; -import { FetchPostsDto } from './dtos/fetch-posts.dto'; -import { PublishPostDto } from './dtos/publish-post.dto'; -import { PagePostResponseDto } from './dtos/page-post-response.dto'; -import { CreatePostInput } from './dtos/create-post.input'; -import { PublishPostInput } from './dtos/publish-post.input'; +import { ArticlesService } from './articles.service'; +import { FetchArticlesDto } from './dtos/fetch-posts.dto'; +import { PublishArticleDto } from './dtos/publish-post.dto'; +import { PageArticleResponseDto } from './dtos/page-post-response.dto'; +import { CreateArticleInput } from './dtos/create-post.input'; +import { PublishArticleInput } from './dtos/publish-post.input'; import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { FetchUserPostsInput } from './dtos/fetch-user-posts.input'; +import { FetchUserArticlesInput } from './dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { PostOnlyResponseDto, PostResponseDto } from './dtos/post-response.dto'; import { - FetchPostForUpdateDto, - PostResponseDtoExceptCategory, + ArticleOnlyResponseDto, + ArticleResponseDto, +} from './dtos/post-response.dto'; +import { + FetchArticleForUpdateDto, + ArticleResponseDtoExceptCategory, } from './dtos/fetch-post-for-update.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { SortOption } from 'src/common/enums/sort-option'; -import { CursorFetchPosts } from './dtos/cursor-fetch-posts.dto'; -import { CursorPagePostResponseDto } from './dtos/cursor-page-post-response.dto'; -import { PatchPostInput } from './dtos/patch-post.dto'; -import { DeletePostInput } from './dtos/delete-post.dto'; +import { CursorFetchArticles } from './dtos/cursor-fetch-posts.dto'; +import { CursorPageArticleResponseDto } from './dtos/cursor-page-post-response.dto'; +import { PatchArticleInput } from './dtos/patch-post.dto'; +import { DeleteArticleInput } from './dtos/delete-post.dto'; @ApiTags('게시글 API') @Controller('posts') -export class PostsController { - constructor(private readonly postsService: PostsService) {} +export class ArticlesController { + constructor(private readonly postsService: ArticlesService) {} @ApiOperation({ summary: '게시글 등록', description: '게시글을 등록한다.', }) - @Post() + @Article() @ApiCookieAuth() - @ApiCreatedResponse({ description: '등록 성공', type: PublishPostDto }) + @ApiCreatedResponse({ description: '등록 성공', type: PublishArticleDto }) @UseGuards(AuthGuardV2) @HttpCode(201) - async publishPost(@Req() req: Request, @Body() body: PublishPostInput) { + async publishArticle(@Req() req: Request, @Body() body: PublishArticleInput) { const kakaoId = req.user.userId; console.log(body); const dto = { ...body, userKakaoId: kakaoId, isPublished: true }; @@ -78,7 +81,7 @@ export class PostsController { async softDelete( @Req() req: Request, @Param('postId') id: number, - @Body() body: DeletePostInput, + @Body() body: DeleteArticleInput, ) { const kakaoId = req.user.userId; if (body.isHardDelete === true) { @@ -91,12 +94,12 @@ export class PostsController { summary: '게시글 임시등록', description: '게시글을 임시등록한다.', }) - @Post('temp') + @Article('temp') @ApiCookieAuth() - @ApiCreatedResponse({ description: '임시등록 성공', type: PublishPostDto }) + @ApiCreatedResponse({ description: '임시등록 성공', type: PublishArticleDto }) @UseGuards(AuthGuardV2) @HttpCode(201) - async updatePost(@Req() req: Request, @Body() body: CreatePostInput) { + async updateArticle(@Req() req: Request, @Body() body: CreateArticleInput) { const kakaoId = req.user.userId; const dto = { ...body, userKakaoId: kakaoId, isPublished: false }; return await this.postsService.save(dto); @@ -104,17 +107,17 @@ export class PostsController { @ApiOperation({ summary: '게시글 patch' }) @ApiCookieAuth() - @ApiOkResponse({ type: PostOnlyResponseDto }) + @ApiOkResponse({ type: ArticleOnlyResponseDto }) @UseGuards(AuthGuardV2) @Patch(':postId') @HttpCode(200) - async patchPost( + async patchArticle( @Req() req: Request, - @Body() body: PatchPostInput, + @Body() body: PatchArticleInput, @Param('postId') id: number, - ): Promise { + ): Promise { const kakaoId = req.user.userId; - return await this.postsService.patchPost({ ...body, id, kakaoId }); + return await this.postsService.patchArticle({ ...body, id, kakaoId }); } @ApiOperation({ @@ -122,15 +125,15 @@ export class PostsController { description: '로그인된 유저의 임시작성 게시글을 조회한다.', }) @ApiCookieAuth() - @ApiOkResponse({ type: [PostResponseDtoExceptCategory] }) + @ApiOkResponse({ type: [ArticleResponseDtoExceptCategory] }) @UseGuards(AuthGuardV2) @Get('temp') - async fetchTempPosts( + async fetchTempArticles( @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; console.log(kakaoId); - return await this.postsService.fetchTempPosts({ kakaoId }); + return await this.postsService.fetchTempArticles({ kakaoId }); } @ApiOperation({ @@ -149,7 +152,7 @@ export class PostsController { }) @UseGuards(AuthGuardV2) @ApiCookieAuth() - @Post('image') + @Article('image') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async createPrivateSticker( @@ -178,11 +181,11 @@ export class PostsController { 'id에 해당하는 게시글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', }) @Get('detail/:postId') - @ApiOkResponse({ type: PostResponseDto }) - async fetchPostDetail( + @ApiOkResponse({ type: ArticleResponseDto }) + async fetchArticleDetail( @Param('postId') id: number, @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; return await this.postsService.fetchDetail({ kakaoId, id }); } @@ -193,46 +196,16 @@ export class PostsController { '본인 게시글 수정용으로 id에 해당하는 게시글에 조인된 스티커 블록들의 값과 게시글 세부 데이터를 모두 가져온다.', }) @ApiCookieAuth() - @ApiOkResponse({ type: FetchPostForUpdateDto }) + @ApiOkResponse({ type: FetchArticleForUpdateDto }) @UseGuards(AuthGuardV2) @HttpCode(200) @Get('update/:postId') - async fetchPost( + async fetchArticle( @Req() req: Request, @Param('postId') id: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchPostForUpdate({ id, kakaoId }); - } - - @ApiOperation({ - summary: '[offset]전체 게시글 조회 API', - description: - 'Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', - }) - @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @HttpCode(200) - @Get('offset') - async fetchPosts(@Query() post: FetchPostsDto): Promise { - return await this.postsService.fetchPosts(post); - } - - @ApiOperation({ - summary: '[offset]친구 게시글 조회', - description: - '친구의 게시글을 조회한다. Query를 통해 오프셋 페이지네이션 가능. default) pageNo: 1, pageSize: 10', - }) - @ApiCreatedResponse({ description: '조회 성공', type: PagePostResponseDto }) - @UseGuards(AuthGuardV2) - @HttpCode(200) - @ApiCookieAuth() - @Get('offset/friends') - async fetchFriendsPosts( - @Query() page: FetchPostsDto, - @Req() req: Request, - ): Promise { + ): Promise { const kakaoId = req.user.userId; - return await this.postsService.fetchFriendsPosts({ kakaoId, page }); + return await this.postsService.fetchArticleForUpdate({ id, kakaoId }); } @ApiOperation({ @@ -241,16 +214,16 @@ export class PostsController { '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다. PUBLIC 게시글만 조회한다.', }) @Get('cursor') - @ApiOkResponse({ type: CursorPagePostResponseDto }) + @ApiOkResponse({ type: CursorPageArticleResponseDto }) async fetchCursor( - @Query() cursorOption: CursorFetchPosts, - ): Promise> { + @Query() cursorOption: CursorFetchArticles, + ): Promise> { if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return this.postsService.fetchPostsCursor({ cursorOption }); + return this.postsService.fetchArticlesCursor({ cursorOption }); } @ApiOperation({ @@ -261,18 +234,21 @@ export class PostsController { @ApiCookieAuth() @UseGuards(AuthGuardV2) @Get('cursor/friends') - @ApiOkResponse({ type: CursorPagePostResponseDto }) + @ApiOkResponse({ type: CursorPageArticleResponseDto }) async fetchFriendsCursor( - @Query() cursorOption: CursorFetchPosts, + @Query() cursorOption: CursorFetchArticles, @Req() req: Request, - ): Promise> { + ): Promise> { const kakaoId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return this.postsService.fetchFriendsPostsCursor({ cursorOption, kakaoId }); + return this.postsService.fetchFriendsArticlesCursor({ + cursorOption, + kakaoId, + }); } @ApiOperation({ @@ -281,19 +257,19 @@ export class PostsController { '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', }) @Get('/cursor/user/:userId') - @ApiOkResponse({ type: CursorPagePostResponseDto }) - async fetchUserPosts( + @ApiOkResponse({ type: CursorPageArticleResponseDto }) + async fetchUserArticles( @Param('userId') targetKakaoId: number, @Req() req: Request, - @Query() cursorOption: FetchUserPostsInput, - ): Promise { + @Query() cursorOption: FetchUserArticlesInput, + ): Promise { const kakaoId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); } - return await this.postsService.fetchUserPostsCursor({ + return await this.postsService.fetchUserArticlesCursor({ kakaoId, targetKakaoId, cursorOption, diff --git a/src/APIs/articles/articles.module.ts b/src/APIs/articles/articles.module.ts index 0c05df2..09b5319 100644 --- a/src/APIs/articles/articles.module.ts +++ b/src/APIs/articles/articles.module.ts @@ -1,27 +1,32 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Posts } from './entities/article.entity'; import { User } from '../users/entities/user.entity'; -import { PostsController } from './articles.controller'; import { UtilsModule } from 'src/utils/utils.module'; -import { PostsService } from './articles.service'; -import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; -import { PostCategory } from '../postCategories/entities/postCategory.entity'; +import { ArticlesService } from './articles.service'; +import { ArticleBackground } from '../articleBackgrounds/entities/articleBackground.entity'; +import { ArticleCategory } from '../articleCategories/entities/articleCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; -import { PostsRepository } from './articles.repository'; +import { ArticlesRepository } from './articles.repository'; import { FollowsModule } from '../follows/follows.module'; import { AwsModule } from 'src/modules/aws/aws.module'; +import { ArticlesController } from './articles.controller'; +import { Article } from './entities/article.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Posts, User, PostBackground, PostCategory]), + TypeOrmModule.forFeature([ + Article, + User, + ArticleBackground, + ArticleCategory, + ]), UtilsModule, AwsModule, FollowsModule, StickerBlocksModule, ], - providers: [PostsService, PostsRepository], - controllers: [PostsController], - exports: [PostsService], + providers: [ArticlesService, ArticlesRepository], + controllers: [ArticlesController], + exports: [ArticlesService], }) -export class PostsModule {} +export class ArticlesModule {} diff --git a/src/APIs/articles/articles.repository.ts b/src/APIs/articles/articles.repository.ts index 068be33..cf49977 100644 --- a/src/APIs/articles/articles.repository.ts +++ b/src/APIs/articles/articles.repository.ts @@ -1,38 +1,38 @@ import { Brackets, DataSource, Repository } from 'typeorm'; -import { Posts } from './entities/article.entity'; +import { Article } from './entities/article.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/common/enums/open-scope.enum'; -import { PostResponseDto } from './dtos/post-response.dto'; -import { PostResponseDtoExceptCategory } from './dtos/fetch-post-for-update.dto'; -import { PostsOrderOption } from 'src/common/enums/posts-order-option'; -import { PostsFilterOption } from 'src/common/enums/posts-filter-option'; +import { PostResponseDto } from './dtos/article-response.dto'; +import { PostResponseDtoExceptCategory } from './dtos/fetch-article-for-update.dto'; +import { ArticlesOrderOption } from 'src/common/enums/articles-order-option'; +import { ArticlesFilterOption } from 'src/common/enums/articles-filter-option'; import { SortOption } from 'src/common/enums/sort-option'; import { - IPostsRepoFetchFriendsPostsCursor, - IPostsRepoFetchPostsCursor, - IPostsRepoFetchUserPostsCursor, - IPostsRepoGetCursorQuery, + IArticlesRepoFetchFriendsArticlesCursor, + IArticlesRepoFetchArticlesCursor, + IArticlesRepoFetchUserArticlesCursor, + IArticlesRepoGetCursorQuery, } from './interfaces/articles.repository.interface'; import { Follow } from '../follows/entities/follow.entity'; @Injectable() -export class PostsRepository extends Repository { +export class ArticlesRepository extends Repository
{ constructor(private dataSource: DataSource) { - super(Posts, dataSource.createEntityManager()); + super(Article, dataSource.createEntityManager()); } - async upsertPost(post) { + async upsertPost(article) { return await this.createQueryBuilder() .insert() - .into(Posts, Object.keys(post)) - .values(post) + .into(Article, Object.keys(article)) + .values(article) .execute(); } - async fetchPosts(page) { + async fetchArticles(page) { return ( this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -44,10 +44,10 @@ export class PostsRepository extends Repository { .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) .andWhere('p.date_deleted IS NULL') //sql injection 방지를 위해 반드시 enum 거칠 것 - .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { + .andWhere(`${ArticlesFilterOption[page.filter]} LIKE :search`, { search: `%${page.search}%`, }) - .orderBy(`p.${PostsOrderOption[page.order]}`, 'DESC') + .orderBy(`p.${ArticlesOrderOption[page.order]}`, 'DESC') .take(page.getLimit()) .skip(page.getOffset()) .getManyAndCount() @@ -60,8 +60,8 @@ export class PostsRepository extends Repository { }); return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -77,8 +77,8 @@ export class PostsRepository extends Repository { async fetchPostForUpdate(id) { return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -91,11 +91,11 @@ export class PostsRepository extends Repository { .getOne(); } - async fetchFriendsPosts(subQuery, page) { + async fetchFriendsArticles(subQuery, page) { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -108,10 +108,10 @@ export class PostsRepository extends Repository { .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC], }) //sql injection 방지를 위해 만드시 enum 거칠 것 - .andWhere(`${PostsFilterOption[page.filter]} LIKE :search`, { + .andWhere(`${ArticlesFilterOption[page.filter]} LIKE :search`, { search: `%${page.search}%`, }) - .orderBy(`p.${PostsOrderOption[page.order]}`, 'DESC') + .orderBy(`p.${ArticlesOrderOption[page.order]}`, 'DESC') .andWhere('p.isPublished = true') .orderBy('p.id', 'DESC') .take(page.getLimit()) @@ -119,13 +119,13 @@ export class PostsRepository extends Repository { .getManyAndCount(); } - async fetchTempPosts( + async fetchTempArticles( kakaoId: number, ): Promise { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -140,8 +140,8 @@ export class PostsRepository extends Repository { .getMany(); } - getCursorQuery({ order, sort, take, cursor }: IPostsRepoGetCursorQuery) { - const _order = PostsOrderOption[order]; + getCursorQuery({ order, sort, take, cursor }: IArticlesRepoGetCursorQuery) { + const _order = ArticlesOrderOption[order]; const queryBuilder = this.createQueryBuilder('p'); const queryByOrderSort = @@ -152,8 +152,8 @@ export class PostsRepository extends Repository { queryBuilder .take(take + 1) .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.postBackground', 'postBackground') - .leftJoinAndSelect('p.postCategory', 'postCategory') + .leftJoinAndSelect('p.articleBackground', 'articleBackground') + .leftJoinAndSelect('p.articleCategory', 'articleCategory') .addSelect([ 'user.handle', 'user.kakaoId', @@ -172,10 +172,10 @@ export class PostsRepository extends Repository { return queryBuilder; } - async fetchPostsCursor({ + async fetchArticlesCursor({ cursorOption, date_filter, - }: IPostsRepoFetchPostsCursor) { + }: IArticlesRepoFetchArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -189,16 +189,16 @@ export class PostsRepository extends Repository { }); } - const posts: Posts[] = await queryBuilder.getMany(); + const articles: Article[] = await queryBuilder.getMany(); - return { posts }; + return { articles }; } - async fetchFriendsPostsCursor({ + async fetchFriendsArticlesCursor({ cursorOption, kakaoId, date_filter, - }: IPostsRepoFetchFriendsPostsCursor) { + }: IArticlesRepoFetchFriendsArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -254,22 +254,22 @@ export class PostsRepository extends Repository { // }); // } - const posts: Posts[] = await queryBuilder.getMany(); + const articles: Article[] = await queryBuilder.getMany(); - return { posts }; + return { articles }; } - async fetchUserPosts({ + async fetchUserArticles({ cursorOption, scope, userKakaoId, date_filter, - }: IPostsRepoFetchUserPostsCursor) { + }: IArticlesRepoFetchUserArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); if (cursorOption.categoryId) { - queryBuilder.andWhere('postCategory.id = :categoryId', { + queryBuilder.andWhere('articleCategory.id = :categoryId', { categoryId: cursorOption.categoryId, }); } @@ -285,8 +285,8 @@ export class PostsRepository extends Repository { }); } - const posts: Posts[] = await queryBuilder.getMany(); + const articles: Article[] = await queryBuilder.getMany(); - return { posts }; + return { articles }; } } diff --git a/src/APIs/articles/articles.service.ts b/src/APIs/articles/articles.service.ts index 0c70026..a3af49e 100644 --- a/src/APIs/articles/articles.service.ts +++ b/src/APIs/articles/articles.service.ts @@ -9,52 +9,38 @@ import { import { UtilsService } from 'src/utils/utils.service'; import { DataSource } from 'typeorm'; -import { Posts } from './entities/article.entity'; -import { Page } from '../../utils/pages/page'; -import { FetchPostsDto } from './dtos/fetch-posts.dto'; -import { PagePostResponseDto } from './dtos/page-post-response.dto'; -import { FetchFriendsPostsDto } from './dtos/fetch-friends-posts.dto'; -import { PostCategory } from '../postCategories/entities/postCategory.entity'; import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; -import { PostsRepository } from './articles.repository'; -import { PostOnlyResponseDto, PostResponseDto } from './dtos/post-response.dto'; -import { - FetchPostForUpdateDto, - PostResponseDtoExceptCategory, -} from './dtos/fetch-post-for-update.dto'; +import { ArticlesRepository } from './articles.repository'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { PostsOrderOption } from 'src/common/enums/posts-order-option'; import { FollowsService } from '../follows/follows.service'; import { DateOption } from 'src/common/enums/date-option'; import { Follow } from '../follows/entities/follow.entity'; import { - IPostsServiceCreate, - IPostsServiceCreateCursorResponse, - IPostsServiceFetchFriendsPostsCursor, - IPostsServiceFetchPostForUpdate, - IPostsServiceFetchPostsCursor, - IPostsServiceFetchUserPostsCursor, - IPostsServicePatchPost, - IPostsServicePostId, - IPostsServicePostUserIdPair, -} from './interfaces/posts.service.interface'; + IArticlesServiceCreate, + IArticlesServiceCreateCursorResponse, + IArticlesServiceFetchFriendsArticlesCursor, + IArticlesServiceFetchArticleForUpdate, + IArticlesServiceFetchArticlesCursor, + IArticlesServiceFetchUserArticlesCursor, + IArticlesServicePatchArticle, + IArticlesServiceArticleId, + IArticlesServiceArticleUserIdPair, +} from './interfaces/articles.service.interface'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { AwsService } from 'src/modules/aws/aws.service'; -import { PostBackground } from '../postBackgrounds/entities/postBackground.entity'; -import { PublishPostDto } from './dtos/publish-post.dto'; @Injectable() -export class PostsService { +export class ArticlesService { constructor( private readonly awsService: AwsService, private readonly utilsService: UtilsService, private readonly dataSource: DataSource, private readonly stickerBlocksService: StickerBlocksService, - private readonly postsRepository: PostsRepository, + private readonly articlesRepository: ArticlesRepository, private readonly followsService: FollowsService, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} @@ -74,71 +60,73 @@ export class PostsService { return { image_url }; } - async findPostsById({ id }: IPostsServicePostId) { - return await this.postsRepository.findOne({ where: { id } }); + async findArticlesById({ id }: IArticlesServiceArticleId) { + return await this.articlesRepository.findOne({ where: { id } }); } - async existCheck({ id }: IPostsServicePostId) { - const data = await this.findPostsById({ id }); + async existCheck({ id }: IArticlesServiceArticleId) { + const data = await this.findArticlesById({ id }); if (!data) throw new NotFoundException('게시글을 찾을 수 없습니다.'); return data; } - async fkValidCheck({ posts, passNonEssentail }) { + async fkValidCheck({ articles, passNonEssentail }) { const pc = await this.dataSource - .getRepository(PostCategory) + .getRepository(ArticleCategory) .createQueryBuilder('pc') - .where('pc.id = :id', { id: posts.postCategoryId }) + .where('pc.id = :id', { id: articles.articleCategoryId }) .getOne(); if (!pc && !passNonEssentail) - throw new BadRequestException('존재하지 않는 post_category입니다.'); + throw new BadRequestException('존재하지 않는 article_category입니다.'); const pg = await this.dataSource - .getRepository(PostBackground) + .getRepository(ArticleBackground) .createQueryBuilder('pg') - .where('pg.id = :id', { id: posts.postBackgroundId }) + .where('pg.id = :id', { id: articles.articleBackgroundId }) .getOne(); - if (!pg && posts.postBackgroundId && !passNonEssentail) - throw new BadRequestException('존재하지 않는 post_background입니다.'); + if (!pg && articles.articleBackgroundId && !passNonEssentail) + throw new BadRequestException('존재하지 않는 article_background입니다.'); const us = await this.dataSource .getRepository(User) .createQueryBuilder('us') - .where('us.kakaoId = :id', { id: posts.userKakaoId }) + .where('us.kakaoId = :id', { id: articles.userKakaoId }) .getOne(); if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); } - async save(createPostDto: IPostsServiceCreate): Promise { + async save( + createArticleDto: IArticlesServiceCreate, + ): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - const post = {}; + const article = {}; try { - Object.keys(createPostDto).map((el) => { - const value = createPostDto[el]; - if (createPostDto[el] != null) { - post[el] = value; + Object.keys(createArticleDto).map((el) => { + const value = createArticleDto[el]; + if (createArticleDto[el] != null) { + article[el] = value; } }); await this.fkValidCheck({ - posts: post, - passNonEssentail: !createPostDto.isPublished, + articles: article, + passNonEssentail: !createArticleDto.isPublished, }); const data = await queryRunner.manager .createQueryBuilder() .insert() - .into(Posts, Object.keys(post)) - .values(post) + .into(Articles, Object.keys(article)) + .values(article) .execute(); await queryRunner.commitTransaction(); - const postData = await this.postsRepository.findOne({ + const articleData = await this.articlesRepository.findOne({ where: { id: data.identifiers[0].id }, }); const stickerBlockData = await this.stickerBlocksService.bulkInsert({ - postsId: postData.id, - kakaoId: createPostDto.userKakaoId, - stickerBlocks: createPostDto.stickerBlocks, + articlesId: articleData.id, + kakaoId: createArticleDto.userKakaoId, + stickerBlocks: createArticleDto.stickerBlocks, }); - return { postData, stickerBlockData }; + return { articleData, stickerBlockData }; } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -147,116 +135,99 @@ export class PostsService { } } - async patchPost({ + async patchArticle({ kakaoId, id, ...rest - }: IPostsServicePatchPost): Promise { - const postData = await this.existCheck({ id }); - if (postData.userKakaoId != kakaoId) + }: IArticlesServicePatchArticle): Promise { + const articleData = await this.existCheck({ id }); + if (articleData.userKakaoId != kakaoId) throw new ForbiddenException('게시글 작성자가 아닙니다.'); Object.keys(rest).forEach((value) => { - if (rest[value] != null) postData[value] = rest[value]; + if (rest[value] != null) articleData[value] = rest[value]; }); - await this.fkValidCheck({ posts: postData, passNonEssentail: false }); - return await this.postsRepository.save(postData); - } - - async fetchPosts(page: FetchPostsDto): Promise { - const postsAndCounts = await this.postsRepository.fetchPosts(page); - return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); + await this.fkValidCheck({ articles: articleData, passNonEssentail: false }); + return await this.articlesRepository.save(articleData); } - async fetchPostForUpdate({ + async fetchArticleForUpdate({ id, kakaoId, - }: IPostsServiceFetchPostForUpdate): Promise { + }: IArticlesServiceFetchArticleForUpdate): Promise { const data = await this.existCheck({ id }); - await this.fkValidCheck({ posts: data, passNonEssentail: true }); + await this.fkValidCheck({ articles: data, passNonEssentail: true }); if (data.userKakaoId !== kakaoId) throw new UnauthorizedException('본인이 아닙니다.'); - const post = await this.postsRepository.fetchPostForUpdate(id); + const article = await this.articlesRepository.fetchArticleForUpdate(id); const stickerBlocks = await this.stickerBlocksService.fetchBlocks({ - postsId: id, + articlesId: id, }); - return { post, stickerBlocks }; + return { article, stickerBlocks }; } - async fetchFriendsPosts({ + async fetchTempArticles({ kakaoId, - page, - }: FetchFriendsPostsDto): Promise { - const subQuery = await this.dataSource - .createQueryBuilder(Follow, 'n') - .select('n.toUserKakaoId') - .where(`n.fromUserKakaoId = ${kakaoId}`) - .getQuery(); - const postsAndCounts = await this.postsRepository.fetchFriendsPosts( - subQuery, - page, - ); - - return new Page(postsAndCounts[1], page.pageSize, postsAndCounts[0]); - } - - async fetchTempPosts({ kakaoId }): Promise { - return await this.postsRepository.fetchTempPosts(kakaoId); + }): Promise { + return await this.articlesRepository.fetchTempArticles(kakaoId); } async fetchDetail({ kakaoId, id, - }: IPostsServicePostUserIdPair): Promise { + }: IArticlesServiceArticleUserIdPair): Promise { const data = await this.existCheck({ id }); - await this.fkValidCheck({ posts: data, passNonEssentail: false }); + await this.fkValidCheck({ articles: data, passNonEssentail: false }); const scope = await this.followsService.getScope({ from_user: data.userKakaoId, to_user: kakaoId, }); - // const comments = await this.commentsService.fetchComments({ postsId: id }); - const post = await this.postsRepository.fetchPostDetail({ id, scope }); - console.log(data, post); - return post; + // const comments = await this.commentsService.fetchComments({ articlesId: id }); + const article = await this.articlesRepository.fetchArticleDetail({ + id, + scope, + }); + console.log(data, article); + return article; } - async softDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { - const data = await this.postsRepository.findOne({ + async softDelete({ kakaoId, id }: IArticlesServiceArticleUserIdPair) { + const data = await this.articlesRepository.findOne({ where: { user: { kakaoId }, id }, }); if (data) { await this.awsService.deleteImageFromS3({ url: data.image_url }); await this.awsService.deleteImageFromS3({ url: data.main_image_url }); - await this.stickerBlocksService.deleteBlocks({ kakaoId, postsId: id }); + await this.stickerBlocksService.deleteBlocks({ kakaoId, articlesId: id }); } - return await this.postsRepository.softDelete({ user: { kakaoId }, id }); + return await this.articlesRepository.softDelete({ user: { kakaoId }, id }); } - async hardDelete({ kakaoId, id }: IPostsServicePostUserIdPair) { - const data = await this.postsRepository.findOne({ + async hardDelete({ kakaoId, id }: IArticlesServiceArticleUserIdPair) { + const data = await this.articlesRepository.findOne({ where: { user: { kakaoId }, id }, }); if (data) { await this.awsService.deleteImageFromS3({ url: data.image_url }); await this.awsService.deleteImageFromS3({ url: data.main_image_url }); - await this.stickerBlocksService.deleteBlocks({ kakaoId, postsId: id }); + await this.stickerBlocksService.deleteBlocks({ kakaoId, articlesId: id }); } - return await this.postsRepository.delete({ user: { kakaoId }, id }); + return await this.articlesRepository.delete({ user: { kakaoId }, id }); } //cursor async createCursorResponse({ cursorOption, - posts, - }: IPostsServiceCreateCursorResponse): Promise< - CustomCursorPageDto + articles, + }: IArticlesServiceCreateCursorResponse): Promise< + CustomCursorPageDto > { - const order = PostsOrderOption[cursorOption.order]; + const order = ArticlesOrderOption[cursorOption.order]; let hasNextData: boolean = true; let customCursor: string; const takePerPage = cursorOption.take; - const isLastPage = posts.length <= takePerPage; - const responseData = posts.slice(0, takePerPage); + const isLastPage = articles.length <= takePerPage; + const responseData = articles.slice(0, takePerPage); const lastDataPerPage = responseData[responseData.length - 1]; if (isLastPage) { @@ -264,7 +235,7 @@ export class PostsService { customCursor = null; } else { customCursor = await this.createCustomCursor({ - post: lastDataPerPage, + article: lastDataPerPage, order, }); } @@ -278,57 +249,58 @@ export class PostsService { return new CustomCursorPageDto(responseData, customCursorPageMetaDto); } - async fetchPostsCursor({ + async fetchArticlesCursor({ cursorOption, - }: IPostsServiceFetchPostsCursor): Promise< - CustomCursorPageDto + }: IArticlesServiceFetchArticlesCursor): Promise< + CustomCursorPageDto > { - const cacheKey = `fetchPostsCursor_${JSON.stringify(cursorOption)}`; + const cacheKey = `fetchArticlesCursor_${JSON.stringify(cursorOption)}`; - const cachedPosts = - await this.cacheManager.get>( + const cachedArticles = + await this.cacheManager.get>( cacheKey, ); - if (cachedPosts) { - return cachedPosts; + if (cachedArticles) { + return cachedArticles; } let date_filter: Date; if (cursorOption.date_created) date_filter = this.getDate(cursorOption.date_created); - const { posts } = await this.postsRepository.fetchPostsCursor({ + const { articles } = await this.articlesRepository.fetchArticlesCursor({ cursorOption, date_filter, }); - const result = await this.createCursorResponse({ posts, cursorOption }); + const result = await this.createCursorResponse({ articles, cursorOption }); await this.cacheManager.set(cacheKey, result, 180000); return result; } - async fetchFriendsPostsCursor({ + async fetchFriendsArticlesCursor({ cursorOption, kakaoId, - }: IPostsServiceFetchFriendsPostsCursor): Promise< - CustomCursorPageDto + }: IArticlesServiceFetchFriendsArticlesCursor): Promise< + CustomCursorPageDto > { let date_filter: Date; if (cursorOption.date_created) date_filter = this.getDate(cursorOption.date_created); - const { posts } = await this.postsRepository.fetchFriendsPostsCursor({ - cursorOption, - kakaoId, - date_filter, - }); - return await this.createCursorResponse({ posts, cursorOption }); + const { articles } = + await this.articlesRepository.fetchFriendsArticlesCursor({ + cursorOption, + kakaoId, + date_filter, + }); + return await this.createCursorResponse({ articles, cursorOption }); } - async fetchUserPostsCursor({ + async fetchUserArticlesCursor({ kakaoId, targetKakaoId, cursorOption, - }: IPostsServiceFetchUserPostsCursor): Promise< - CustomCursorPageDto + }: IArticlesServiceFetchUserArticlesCursor): Promise< + CustomCursorPageDto > { let date_filter: Date; if (cursorOption.date_created) @@ -338,18 +310,18 @@ export class PostsService { from_user: targetKakaoId, to_user: kakaoId, }); - const { posts } = await this.postsRepository.fetchUserPosts({ + const { articles } = await this.articlesRepository.fetchUserArticles({ cursorOption, date_filter, scope, userKakaoId: targetKakaoId, }); - return await this.createCursorResponse({ posts, cursorOption }); + return await this.createCursorResponse({ articles, cursorOption }); } - async createCustomCursor({ post, order }): Promise { - const id = post.id; - const _order = post[order]; + async createCustomCursor({ article, order }): Promise { + const id = article.id; + const _order = article[order]; const customCursor: string = String(_order).padStart(7, '0') + String(id).padStart(7, '0'); diff --git a/src/APIs/articles/interfaces/articles.repository.interface.ts b/src/APIs/articles/interfaces/articles.repository.interface.ts index 8331abd..353bc5b 100644 --- a/src/APIs/articles/interfaces/articles.repository.interface.ts +++ b/src/APIs/articles/interfaces/articles.repository.interface.ts @@ -1,32 +1,32 @@ -import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; +import { ArticlesOrderOptionWrap } from 'src/common/enums/articles-order-option'; import { SortOption } from 'src/common/enums/sort-option'; import { - IPostsServiceFetchPostsCursor, - IPostsServiceFetchFriendsPostsCursor, - IPostsServiceFetchUserPostsCursor, -} from './posts.service.interface'; + IArticlesServiceFetchArticlesCursor, + IArticlesServiceFetchFriendsArticlesCursor, + IArticlesServiceFetchUserArticlesCursor, +} from './articles.service.interface'; import { OpenScope } from 'src/common/enums/open-scope.enum'; -export interface IPostsRepoGetCursorQuery { - order: PostsOrderOptionWrap; +export interface IArticlesRepoGetCursorQuery { + order: ArticlesOrderOptionWrap; sort: SortOption; take: number; cursor: string; } -export interface IPostsRepoFetchPostsCursor - extends IPostsServiceFetchPostsCursor { +export interface IArticlesRepoFetchArticlesCursor + extends IArticlesServiceFetchArticlesCursor { date_filter: Date; } -export interface IPostsRepoFetchFriendsPostsCursor - extends Pick { +export interface IArticlesRepoFetchFriendsArticlesCursor + extends Pick { date_filter: Date; kakaoId: number; } -export interface IPostsRepoFetchUserPostsCursor - extends Pick { +export interface IArticlesRepoFetchUserArticlesCursor + extends Pick { date_filter: Date; scope: OpenScope[]; userKakaoId: number; diff --git a/src/APIs/articles/interfaces/articles.service.interface.ts b/src/APIs/articles/interfaces/articles.service.interface.ts new file mode 100644 index 0000000..2c0339d --- /dev/null +++ b/src/APIs/articles/interfaces/articles.service.interface.ts @@ -0,0 +1,44 @@ +import { Article } from '../entities/article.entity'; + +export interface IArticlesServiceArticleId extends Pick {} + +export interface IArticlesServiceArticleUserIdPair { + id: number; + kakaoId: number; +} + +export interface IArticlesServiceCreate extends CreateArticleInput { + userKakaoId: number; + + isPublished: boolean; +} + +export interface IArticlesServiceFetchArticleForUpdate { + id: number; + kakaoId: number; +} + +export interface IArticlesServiceCreateCursorResponse { + cursorOption: CursorFetchArticles; + articles: Article[]; +} + +export interface IArticlesServiceFetchArticlesCursor { + cursorOption: CursorFetchArticles; +} + +export interface IArticlesServiceFetchFriendsArticlesCursor { + cursorOption: CursorFetchArticles; + kakaoId: number; +} + +export interface IArticlesServiceFetchUserArticlesCursor { + cursorOption: FetchUserArticlesInput; + targetKakaoId: number; + kakaoId: number; +} + +export interface IArticlesServicePatchArticle extends PatchArticleInput { + kakaoId: number; + id: number; +} diff --git a/src/APIs/articles/interfaces/posts.service.interface.ts b/src/APIs/articles/interfaces/posts.service.interface.ts deleted file mode 100644 index bb6e527..0000000 --- a/src/APIs/articles/interfaces/posts.service.interface.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CreatePostInput } from '../dtos/create-post.input'; -import { CursorFetchPosts } from '../dtos/cursor-fetch-posts.dto'; -import { FetchUserPostsInput } from '../dtos/fetch-user-posts.input'; -import { PatchPostInput } from '../dtos/patch-post.dto'; -import { Posts } from '../entities/article.entity'; - -export interface IPostsServicePostId extends Pick {} - -export interface IPostsServicePostUserIdPair { - id: number; - kakaoId: number; -} - -export interface IPostsServiceCreate extends CreatePostInput { - userKakaoId: number; - - isPublished: boolean; -} - -export interface IPostsServiceFetchPostForUpdate { - id: number; - kakaoId: number; -} - -export interface IPostsServiceCreateCursorResponse { - cursorOption: CursorFetchPosts; - posts: Posts[]; -} - -export interface IPostsServiceFetchPostsCursor { - cursorOption: CursorFetchPosts; -} - -export interface IPostsServiceFetchFriendsPostsCursor { - cursorOption: CursorFetchPosts; - kakaoId: number; -} - -export interface IPostsServiceFetchUserPostsCursor { - cursorOption: FetchUserPostsInput; - targetKakaoId: number; - kakaoId: number; -} - -export interface IPostsServicePatchPost extends PatchPostInput { - kakaoId: number; - id: number; -} From 1a779acfb5b854537d748220ca80edf98985e982 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 16:21:52 +0900 Subject: [PATCH 189/236] refactor(articleBackgrounds): change domain name postBackgrounds -> articleBackgrounds --- .../articleBackgrounds.controller.ts} | 24 ++++---- .../articleBackgrounds.module.ts | 19 ++++++ .../articleBackgrounds.service.ts} | 20 +++---- .../entities/articleBackground.entity.ts | 17 ++++++ .../PostCategories.controller.ts | 0 .../PostCategories.module.ts | 0 .../PostCategories.repository.ts | 0 .../PostCategories.service.ts | 0 .../dtos/create-post-category-response.dto.ts | 0 .../dtos/create-post-category.dto.ts | 0 .../dtos/fetch-post-category.dto.ts | 0 .../dtos/patch-post-category.dto.ts | 0 .../entities/postCategory.entity.ts | 0 src/APIs/articles/articles.module.ts | 4 +- src/APIs/articles/articles.service.ts | 2 +- .../{ => controllers}/articles.controller.ts | 26 ++++---- .../controllers/create-articles.controller.ts | 8 +++ .../controllers/delete-articles.controller.ts | 0 .../controllers/read-articles.controller.ts | 0 .../controllers/update-articles.controller.ts | 0 ...esponse.dto.ts => article-response.dto.ts} | 0 src/APIs/articles/dtos/article.dto.ts | 4 ++ src/APIs/articles/entities/article.entity.ts | 12 ++-- .../{dtos => olds}/create-post.input.ts | 0 .../{dtos => olds}/cursor-fetch-posts.dto.ts | 0 .../cursor-page-post-response.dto.ts | 0 .../{dtos => olds}/delete-post.dto.ts | 0 .../{dtos => olds}/fetch-friends-posts.dto.ts | 0 .../{dtos => olds}/fetch-post-detail.dto.ts | 0 .../fetch-post-for-update.dto.ts | 0 .../{dtos => olds}/fetch-posts.dto.ts | 0 .../{dtos => olds}/fetch-user-posts.dto.ts | 0 .../{dtos => olds}/fetch-user-posts.input.ts | 0 .../{dtos => olds}/page-post-response.dto.ts | 0 .../articles/{dtos => olds}/patch-post.dto.ts | 0 src/APIs/articles/olds/post-response.dto.ts | 15 +++++ .../{dtos => olds}/publish-post.dto.ts | 0 .../{dtos => olds}/publish-post.input.ts | 0 .../{ => repositories}/articles.repository.ts | 60 +------------------ .../create-articles.repository.ts | 17 ++++++ .../delete-articles.repository.ts | 10 ++++ .../repositories/read-articles.repository.ts | 30 ++++++++++ .../update-articles.repository.ts | 10 ++++ .../services/create-articles.service.ts | 6 ++ .../services/delete-articles.service.ts | 0 .../services/read-articles.service.ts | 0 .../services/update-articles.service.ts | 0 src/APIs/likes/entities/like.entity.ts | 12 ++-- .../entities/postBackground.entity.ts | 13 ---- .../postBackgrounds/postBackgrounds.module.ts | 15 ----- src/app.module.ts | 4 +- src/common/dto/image-upload-response.dto.ts | 2 +- src/common/entities/common.entity.ts | 2 +- 53 files changed, 192 insertions(+), 140 deletions(-) rename src/APIs/{postBackgrounds/postBackgrounds.controller.ts => articleBackgrounds/articleBackgrounds.controller.ts} (66%) create mode 100644 src/APIs/articleBackgrounds/articleBackgrounds.module.ts rename src/APIs/{postBackgrounds/postBackgrounds.service.ts => articleBackgrounds/articleBackgrounds.service.ts} (63%) create mode 100644 src/APIs/articleBackgrounds/entities/articleBackground.entity.ts rename src/APIs/{postCategories => articleCategories}/PostCategories.controller.ts (100%) rename src/APIs/{postCategories => articleCategories}/PostCategories.module.ts (100%) rename src/APIs/{postCategories => articleCategories}/PostCategories.repository.ts (100%) rename src/APIs/{postCategories => articleCategories}/PostCategories.service.ts (100%) rename src/APIs/{postCategories => articleCategories}/dtos/create-post-category-response.dto.ts (100%) rename src/APIs/{postCategories => articleCategories}/dtos/create-post-category.dto.ts (100%) rename src/APIs/{postCategories => articleCategories}/dtos/fetch-post-category.dto.ts (100%) rename src/APIs/{postCategories => articleCategories}/dtos/patch-post-category.dto.ts (100%) rename src/APIs/{postCategories => articleCategories}/entities/postCategory.entity.ts (100%) rename src/APIs/articles/{ => controllers}/articles.controller.ts (91%) create mode 100644 src/APIs/articles/controllers/create-articles.controller.ts create mode 100644 src/APIs/articles/controllers/delete-articles.controller.ts create mode 100644 src/APIs/articles/controllers/read-articles.controller.ts create mode 100644 src/APIs/articles/controllers/update-articles.controller.ts rename src/APIs/articles/dtos/{post-response.dto.ts => article-response.dto.ts} (100%) create mode 100644 src/APIs/articles/dtos/article.dto.ts rename src/APIs/articles/{dtos => olds}/create-post.input.ts (100%) rename src/APIs/articles/{dtos => olds}/cursor-fetch-posts.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/cursor-page-post-response.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/delete-post.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-friends-posts.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-post-detail.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-post-for-update.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-posts.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-user-posts.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/fetch-user-posts.input.ts (100%) rename src/APIs/articles/{dtos => olds}/page-post-response.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/patch-post.dto.ts (100%) create mode 100644 src/APIs/articles/olds/post-response.dto.ts rename src/APIs/articles/{dtos => olds}/publish-post.dto.ts (100%) rename src/APIs/articles/{dtos => olds}/publish-post.input.ts (100%) rename src/APIs/articles/{ => repositories}/articles.repository.ts (79%) create mode 100644 src/APIs/articles/repositories/create-articles.repository.ts create mode 100644 src/APIs/articles/repositories/delete-articles.repository.ts create mode 100644 src/APIs/articles/repositories/read-articles.repository.ts create mode 100644 src/APIs/articles/repositories/update-articles.repository.ts create mode 100644 src/APIs/articles/services/create-articles.service.ts create mode 100644 src/APIs/articles/services/delete-articles.service.ts create mode 100644 src/APIs/articles/services/read-articles.service.ts create mode 100644 src/APIs/articles/services/update-articles.service.ts delete mode 100644 src/APIs/postBackgrounds/entities/postBackground.entity.ts delete mode 100644 src/APIs/postBackgrounds/postBackgrounds.module.ts diff --git a/src/APIs/postBackgrounds/postBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts similarity index 66% rename from src/APIs/postBackgrounds/postBackgrounds.controller.ts rename to src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index cf66922..05c4bd8 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -8,7 +8,7 @@ import { UploadedFile, UseInterceptors, } from '@nestjs/common'; -import { PostBackgroundsService } from './postBackgrounds.service'; +import { ArticleBackgroundsService } from './articleBackgrounds.service'; import { ApiBody, ApiConsumes, @@ -19,13 +19,13 @@ import { } from '@nestjs/swagger'; import { ImageUploadDto } from '../../common/dto/image-upload.dto'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { PostBackground } from './entities/postBackground.entity'; +import { ArticleBackground } from './entities/articleBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; @Controller('') -export class PostBackgroundsController { +export class ArticleBackgroundsController { constructor( - private readonly postBackgroundsService: PostBackgroundsService, + private readonly articleBackgroundsService: ArticleBackgroundsService, ) {} @ApiTags('어드민 API') @@ -39,13 +39,13 @@ export class PostBackgroundsController { description: '이미지 서버에 파일 업로드 완료', type: ImageUploadResponseDto, }) - @Post('users/admin/posts/background') + @Post('users/admin/article/background') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async uploadImage( @UploadedFile() file: Express.Multer.File, ): Promise { - const url = await this.postBackgroundsService.imageUpload(file); + const url = await this.articleBackgroundsService.imageUpload(file); return url; } @@ -53,17 +53,17 @@ export class PostBackgroundsController { @ApiOperation({ summary: '내지 모두 불러오기' }) @ApiOkResponse({ description: '모든 내지 fetch 완료', - type: [PostBackground], + type: [ArticleBackground], }) - @Get('posts/backgrounds') - async fetchAll(): Promise { - return await this.postBackgroundsService.fetchAll(); + @Get('article/backgrounds') + async fetchAll(): Promise { + return await this.articleBackgroundsService.fetchAll(); } @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) - @Delete('users/admin/posts/background/:id') + @Delete('users/admin/article/background/:id') async delete(@Param('id') id: string) { - return await this.postBackgroundsService.delete({ id }); + return await this.articleBackgroundsService.delete({ id }); } } diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts new file mode 100644 index 0000000..93ab54e --- /dev/null +++ b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { JwtStrategy } from '../auth/strategies/jwt.strategy'; +import { UtilsModule } from 'src/utils/utils.module'; +import { ArticleBackgroundsController } from './articleBackgrounds.controller'; +import { AwsModule } from 'src/modules/aws/aws.module'; +import { ArticleBackgroundsService } from './articleBackgrounds.service'; +import { ArticleBackground } from './entities/articleBackground.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ArticleBackground]), + UtilsModule, + AwsModule, + ], + providers: [JwtStrategy, ArticleBackgroundsService], + controllers: [ArticleBackgroundsController], +}) +export class ArticleBackgroundsModule {} diff --git a/src/APIs/postBackgrounds/postBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts similarity index 63% rename from src/APIs/postBackgrounds/postBackgrounds.service.ts rename to src/APIs/articleBackgrounds/articleBackgrounds.service.ts index 9324353..6baee64 100644 --- a/src/APIs/postBackgrounds/postBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -2,18 +2,18 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { UtilsService } from 'src/utils/utils.service'; -import { PostBackground } from './entities/postBackground.entity'; +import { ArticleBackground } from './entities/articleBackground.entity'; import { Repository } from 'typeorm'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() -export class PostBackgroundsService { +export class ArticleBackgroundsService { constructor( private readonly awsService: AwsService, private readonly utilsService: UtilsService, - @InjectRepository(PostBackground) - private readonly postBackgroundsRepository: Repository, + @InjectRepository(ArticleBackground) + private readonly articleBackgroundsRepository: Repository, ) {} async saveImage(file: Express.Multer.File): Promise { return await this.imageUpload(file); @@ -25,22 +25,22 @@ export class PostBackgroundsService { const imageName = this.utilsService.getUUID(); const ext = file.originalname.split('.').pop(); - const image_url = await this.awsService.imageUploadToS3( + const imageUrl = await this.awsService.imageUploadToS3( `${imageName}.${ext}`, file, ext, 2000, ); - await this.postBackgroundsRepository.save({ image_url }); - return { image_url }; + await this.articleBackgroundsRepository.save({ imageUrl }); + return { imageUrl }; } - async fetchAll(): Promise { - return await this.postBackgroundsRepository.find(); + async fetchAll(): Promise { + return await this.articleBackgroundsRepository.find(); } async delete({ id }) { // s3 서버에서 이미지 삭제하는 것까지 구현하기!! - return await this.postBackgroundsRepository.delete({ id }); + return await this.articleBackgroundsRepository.delete({ id }); } } diff --git a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts new file mode 100644 index 0000000..c41a8e7 --- /dev/null +++ b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Article } from 'src/APIs/articles/entities/article.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class ArticleBackground { + @ApiProperty({ description: 'PK: A_I_', type: Number }) + @PrimaryGeneratedColumn() + id: number; + + @ApiProperty({ type: String, description: '이미지가 저장된 url' }) + @Column({ nullable: false }) + imageUrl: string; + + @OneToMany(() => Article, (article) => article.articleBackgroundId) + articles: Article[]; +} diff --git a/src/APIs/postCategories/PostCategories.controller.ts b/src/APIs/articleCategories/PostCategories.controller.ts similarity index 100% rename from src/APIs/postCategories/PostCategories.controller.ts rename to src/APIs/articleCategories/PostCategories.controller.ts diff --git a/src/APIs/postCategories/PostCategories.module.ts b/src/APIs/articleCategories/PostCategories.module.ts similarity index 100% rename from src/APIs/postCategories/PostCategories.module.ts rename to src/APIs/articleCategories/PostCategories.module.ts diff --git a/src/APIs/postCategories/PostCategories.repository.ts b/src/APIs/articleCategories/PostCategories.repository.ts similarity index 100% rename from src/APIs/postCategories/PostCategories.repository.ts rename to src/APIs/articleCategories/PostCategories.repository.ts diff --git a/src/APIs/postCategories/PostCategories.service.ts b/src/APIs/articleCategories/PostCategories.service.ts similarity index 100% rename from src/APIs/postCategories/PostCategories.service.ts rename to src/APIs/articleCategories/PostCategories.service.ts diff --git a/src/APIs/postCategories/dtos/create-post-category-response.dto.ts b/src/APIs/articleCategories/dtos/create-post-category-response.dto.ts similarity index 100% rename from src/APIs/postCategories/dtos/create-post-category-response.dto.ts rename to src/APIs/articleCategories/dtos/create-post-category-response.dto.ts diff --git a/src/APIs/postCategories/dtos/create-post-category.dto.ts b/src/APIs/articleCategories/dtos/create-post-category.dto.ts similarity index 100% rename from src/APIs/postCategories/dtos/create-post-category.dto.ts rename to src/APIs/articleCategories/dtos/create-post-category.dto.ts diff --git a/src/APIs/postCategories/dtos/fetch-post-category.dto.ts b/src/APIs/articleCategories/dtos/fetch-post-category.dto.ts similarity index 100% rename from src/APIs/postCategories/dtos/fetch-post-category.dto.ts rename to src/APIs/articleCategories/dtos/fetch-post-category.dto.ts diff --git a/src/APIs/postCategories/dtos/patch-post-category.dto.ts b/src/APIs/articleCategories/dtos/patch-post-category.dto.ts similarity index 100% rename from src/APIs/postCategories/dtos/patch-post-category.dto.ts rename to src/APIs/articleCategories/dtos/patch-post-category.dto.ts diff --git a/src/APIs/postCategories/entities/postCategory.entity.ts b/src/APIs/articleCategories/entities/postCategory.entity.ts similarity index 100% rename from src/APIs/postCategories/entities/postCategory.entity.ts rename to src/APIs/articleCategories/entities/postCategory.entity.ts diff --git a/src/APIs/articles/articles.module.ts b/src/APIs/articles/articles.module.ts index 09b5319..6ef3802 100644 --- a/src/APIs/articles/articles.module.ts +++ b/src/APIs/articles/articles.module.ts @@ -6,10 +6,10 @@ import { ArticlesService } from './articles.service'; import { ArticleBackground } from '../articleBackgrounds/entities/articleBackground.entity'; import { ArticleCategory } from '../articleCategories/entities/articleCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; -import { ArticlesRepository } from './articles.repository'; +import { ArticlesRepository } from './repositories/articles.repository'; import { FollowsModule } from '../follows/follows.module'; import { AwsModule } from 'src/modules/aws/aws.module'; -import { ArticlesController } from './articles.controller'; +import { ArticlesController } from './controllers/articles.controller'; import { Article } from './entities/article.entity'; @Module({ diff --git a/src/APIs/articles/articles.service.ts b/src/APIs/articles/articles.service.ts index a3af49e..5aed0bd 100644 --- a/src/APIs/articles/articles.service.ts +++ b/src/APIs/articles/articles.service.ts @@ -12,7 +12,7 @@ import { DataSource } from 'typeorm'; import { User } from '../users/entities/user.entity'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; -import { ArticlesRepository } from './articles.repository'; +import { ArticlesRepository } from './repositories/articles.repository'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { FollowsService } from '../follows/follows.service'; diff --git a/src/APIs/articles/articles.controller.ts b/src/APIs/articles/controllers/articles.controller.ts similarity index 91% rename from src/APIs/articles/articles.controller.ts rename to src/APIs/articles/controllers/articles.controller.ts index 04cc754..3830169 100644 --- a/src/APIs/articles/articles.controller.ts +++ b/src/APIs/articles/controllers/articles.controller.ts @@ -23,31 +23,31 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { ArticlesService } from './articles.service'; -import { FetchArticlesDto } from './dtos/fetch-posts.dto'; -import { PublishArticleDto } from './dtos/publish-post.dto'; -import { PageArticleResponseDto } from './dtos/page-post-response.dto'; -import { CreateArticleInput } from './dtos/create-post.input'; -import { PublishArticleInput } from './dtos/publish-post.input'; +import { ArticlesService } from '../articles.service'; +import { FetchArticlesDto } from '../dtos/fetch-posts.dto'; +import { PublishArticleDto } from '../dtos/publish-post.dto'; +import { PageArticleResponseDto } from '../dtos/page-post-response.dto'; +import { CreateArticleInput } from '../dtos/create-post.input'; +import { PublishArticleInput } from '../dtos/publish-post.input'; import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { FetchUserArticlesInput } from './dtos/fetch-user-posts.input'; +import { FetchUserArticlesInput } from '../dtos/fetch-user-posts.input'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { ArticleOnlyResponseDto, ArticleResponseDto, -} from './dtos/post-response.dto'; +} from '../dtos/post-response.dto'; import { FetchArticleForUpdateDto, ArticleResponseDtoExceptCategory, -} from './dtos/fetch-post-for-update.dto'; +} from '../dtos/fetch-post-for-update.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { SortOption } from 'src/common/enums/sort-option'; -import { CursorFetchArticles } from './dtos/cursor-fetch-posts.dto'; -import { CursorPageArticleResponseDto } from './dtos/cursor-page-post-response.dto'; -import { PatchArticleInput } from './dtos/patch-post.dto'; -import { DeleteArticleInput } from './dtos/delete-post.dto'; +import { CursorFetchArticles } from '../dtos/cursor-fetch-posts.dto'; +import { CursorPageArticleResponseDto } from '../dtos/cursor-page-post-response.dto'; +import { PatchArticleInput } from '../dtos/patch-post.dto'; +import { DeleteArticleInput } from '../dtos/delete-post.dto'; @ApiTags('게시글 API') @Controller('posts') diff --git a/src/APIs/articles/controllers/create-articles.controller.ts b/src/APIs/articles/controllers/create-articles.controller.ts new file mode 100644 index 0000000..b99fd9e --- /dev/null +++ b/src/APIs/articles/controllers/create-articles.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('게시글 API') +@Controller('articles') +export class createArticlesController { + constructor(private readonly createArticlesService: createArticlesService) {} +} diff --git a/src/APIs/articles/controllers/delete-articles.controller.ts b/src/APIs/articles/controllers/delete-articles.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/articles/controllers/read-articles.controller.ts b/src/APIs/articles/controllers/read-articles.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/articles/controllers/update-articles.controller.ts b/src/APIs/articles/controllers/update-articles.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/articles/dtos/post-response.dto.ts b/src/APIs/articles/dtos/article-response.dto.ts similarity index 100% rename from src/APIs/articles/dtos/post-response.dto.ts rename to src/APIs/articles/dtos/article-response.dto.ts diff --git a/src/APIs/articles/dtos/article.dto.ts b/src/APIs/articles/dtos/article.dto.ts new file mode 100644 index 0000000..56a8820 --- /dev/null +++ b/src/APIs/articles/dtos/article.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Article } from '../entities/article.entity'; + +export class ArticleDto extends OmitType(Article, ['comments', '']) {} diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 1157abd..3f7fc15 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -7,8 +7,8 @@ import { IsString, IsUrl, } from 'class-validator'; -import { PostBackground } from 'src/APIs/postBackgrounds/entities/postBackground.entity'; -import { PostCategory } from 'src/APIs/postCategories/entities/postCategory.entity'; +import { PostBackground } from 'src/APIs/articleBackgrounds/entities/postBackground.entity'; +import { PostCategory } from 'src/APIs/articleCategories/entities/postCategory.entity'; import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { CommonEntity } from 'src/common/entities/common.entity'; @@ -31,19 +31,19 @@ export class Article extends CommonEntity { id: number; @ApiProperty({ description: '연결된 카테고리 fk', type: String }) - @Column({ name: 'post_category_id', nullable: true }) + @Column({ name: 'article_category_id', nullable: true }) @RelationId((article: Article) => article.postCategory) @IsString() @IsOptional() - postCategoryId: string; + articleCategoryId: string; @IsString() @ApiProperty({ description: '연결된 내지 fk', type: String }) - @Column({ name: 'post_background_id', nullable: true }) + @Column({ name: 'article_background_id', nullable: true }) @RelationId((article: Article) => article.postBackground) @IsString() @IsOptional() - postBackgroundId: string; + articleBackgroundId: string; @ApiProperty({ description: '작성한 유저 fk', type: Number }) @Column({ name: 'user_id', nullable: false }) diff --git a/src/APIs/articles/dtos/create-post.input.ts b/src/APIs/articles/olds/create-post.input.ts similarity index 100% rename from src/APIs/articles/dtos/create-post.input.ts rename to src/APIs/articles/olds/create-post.input.ts diff --git a/src/APIs/articles/dtos/cursor-fetch-posts.dto.ts b/src/APIs/articles/olds/cursor-fetch-posts.dto.ts similarity index 100% rename from src/APIs/articles/dtos/cursor-fetch-posts.dto.ts rename to src/APIs/articles/olds/cursor-fetch-posts.dto.ts diff --git a/src/APIs/articles/dtos/cursor-page-post-response.dto.ts b/src/APIs/articles/olds/cursor-page-post-response.dto.ts similarity index 100% rename from src/APIs/articles/dtos/cursor-page-post-response.dto.ts rename to src/APIs/articles/olds/cursor-page-post-response.dto.ts diff --git a/src/APIs/articles/dtos/delete-post.dto.ts b/src/APIs/articles/olds/delete-post.dto.ts similarity index 100% rename from src/APIs/articles/dtos/delete-post.dto.ts rename to src/APIs/articles/olds/delete-post.dto.ts diff --git a/src/APIs/articles/dtos/fetch-friends-posts.dto.ts b/src/APIs/articles/olds/fetch-friends-posts.dto.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-friends-posts.dto.ts rename to src/APIs/articles/olds/fetch-friends-posts.dto.ts diff --git a/src/APIs/articles/dtos/fetch-post-detail.dto.ts b/src/APIs/articles/olds/fetch-post-detail.dto.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-post-detail.dto.ts rename to src/APIs/articles/olds/fetch-post-detail.dto.ts diff --git a/src/APIs/articles/dtos/fetch-post-for-update.dto.ts b/src/APIs/articles/olds/fetch-post-for-update.dto.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-post-for-update.dto.ts rename to src/APIs/articles/olds/fetch-post-for-update.dto.ts diff --git a/src/APIs/articles/dtos/fetch-posts.dto.ts b/src/APIs/articles/olds/fetch-posts.dto.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-posts.dto.ts rename to src/APIs/articles/olds/fetch-posts.dto.ts diff --git a/src/APIs/articles/dtos/fetch-user-posts.dto.ts b/src/APIs/articles/olds/fetch-user-posts.dto.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-user-posts.dto.ts rename to src/APIs/articles/olds/fetch-user-posts.dto.ts diff --git a/src/APIs/articles/dtos/fetch-user-posts.input.ts b/src/APIs/articles/olds/fetch-user-posts.input.ts similarity index 100% rename from src/APIs/articles/dtos/fetch-user-posts.input.ts rename to src/APIs/articles/olds/fetch-user-posts.input.ts diff --git a/src/APIs/articles/dtos/page-post-response.dto.ts b/src/APIs/articles/olds/page-post-response.dto.ts similarity index 100% rename from src/APIs/articles/dtos/page-post-response.dto.ts rename to src/APIs/articles/olds/page-post-response.dto.ts diff --git a/src/APIs/articles/dtos/patch-post.dto.ts b/src/APIs/articles/olds/patch-post.dto.ts similarity index 100% rename from src/APIs/articles/dtos/patch-post.dto.ts rename to src/APIs/articles/olds/patch-post.dto.ts diff --git a/src/APIs/articles/olds/post-response.dto.ts b/src/APIs/articles/olds/post-response.dto.ts new file mode 100644 index 0000000..e4d8760 --- /dev/null +++ b/src/APIs/articles/olds/post-response.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; + +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { Posts } from '../entities/article.entity'; + +export class PostResponseDto extends OmitType(Posts, ['user']) { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; +} + +export class PostOnlyResponseDto extends OmitType(Posts, [ + 'user', + 'postBackground', + 'postCategory', +]) {} diff --git a/src/APIs/articles/dtos/publish-post.dto.ts b/src/APIs/articles/olds/publish-post.dto.ts similarity index 100% rename from src/APIs/articles/dtos/publish-post.dto.ts rename to src/APIs/articles/olds/publish-post.dto.ts diff --git a/src/APIs/articles/dtos/publish-post.input.ts b/src/APIs/articles/olds/publish-post.input.ts similarity index 100% rename from src/APIs/articles/dtos/publish-post.input.ts rename to src/APIs/articles/olds/publish-post.input.ts diff --git a/src/APIs/articles/articles.repository.ts b/src/APIs/articles/repositories/articles.repository.ts similarity index 79% rename from src/APIs/articles/articles.repository.ts rename to src/APIs/articles/repositories/articles.repository.ts index cf49977..93ba5ee 100644 --- a/src/APIs/articles/articles.repository.ts +++ b/src/APIs/articles/repositories/articles.repository.ts @@ -1,5 +1,5 @@ import { Brackets, DataSource, Repository } from 'typeorm'; -import { Article } from './entities/article.entity'; +import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { PostResponseDto } from './dtos/article-response.dto'; @@ -12,68 +12,14 @@ import { IArticlesRepoFetchArticlesCursor, IArticlesRepoFetchUserArticlesCursor, IArticlesRepoGetCursorQuery, -} from './interfaces/articles.repository.interface'; -import { Follow } from '../follows/entities/follow.entity'; +} from '../interfaces/articles.repository.interface'; +import { Follow } from '../../follows/entities/follow.entity'; @Injectable() export class ArticlesRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } - async upsertPost(article) { - return await this.createQueryBuilder() - .insert() - .into(Article, Object.keys(article)) - .values(article) - .execute(); - } - - async fetchArticles(page) { - return ( - this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.isPublished = true') - .andWhere('p.scope IN (:...scopes)', { scopes: [OpenScope.PUBLIC] }) - .andWhere('p.date_deleted IS NULL') - //sql injection 방지를 위해 반드시 enum 거칠 것 - .andWhere(`${ArticlesFilterOption[page.filter]} LIKE :search`, { - search: `%${page.search}%`, - }) - .orderBy(`p.${ArticlesOrderOption[page.order]}`, 'DESC') - .take(page.getLimit()) - .skip(page.getOffset()) - .getManyAndCount() - ); - } - async fetchPostDetail({ id, scope }): Promise { - await this.update(id, { - view_count: () => 'view_count +1', - }); - return await this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.id = :id', { id }) - .andWhere('p.scope IN (:scope)', { scope }) - .andWhere('p.date_deleted IS NULL') - .getOne(); - } async fetchPostForUpdate(id) { return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') diff --git a/src/APIs/articles/repositories/create-articles.repository.ts b/src/APIs/articles/repositories/create-articles.repository.ts new file mode 100644 index 0000000..b1e463c --- /dev/null +++ b/src/APIs/articles/repositories/create-articles.repository.ts @@ -0,0 +1,17 @@ +import { DataSource, Repository } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class CreateArticlesRepository extends Repository
{ + constructor(private dataSource: DataSource) { + super(Article, dataSource.createEntityManager()); + } + async insertPost(article) { + return await this.createQueryBuilder() + .insert() + .into(Article, Object.keys(article)) + .values(article) + .execute(); + } +} diff --git a/src/APIs/articles/repositories/delete-articles.repository.ts b/src/APIs/articles/repositories/delete-articles.repository.ts new file mode 100644 index 0000000..c30c6d1 --- /dev/null +++ b/src/APIs/articles/repositories/delete-articles.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DeleteArticlesRepository extends Repository
{ + constructor(private dataSource: DataSource) { + super(Article, dataSource.createEntityManager()); + } +} diff --git a/src/APIs/articles/repositories/read-articles.repository.ts b/src/APIs/articles/repositories/read-articles.repository.ts new file mode 100644 index 0000000..25abff2 --- /dev/null +++ b/src/APIs/articles/repositories/read-articles.repository.ts @@ -0,0 +1,30 @@ +import { DataSource, Repository } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ReadArticlesRepository extends Repository
{ + constructor(private dataSource: DataSource) { + super(Article, dataSource.createEntityManager()); + } + async fetchArticlesDetail({ id, scope }): Promise { + await this.update(id, { + viewCount: () => 'view_count +1', + }); + return await this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .leftJoinAndSelect('p.article_background', 'article_background') + .leftJoinAndSelect('p.article_category', 'article_category') + .addSelect([ + 'user.handle', + 'user.id', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.id = :id', { id }) + .andWhere('p.scope IN (:scope)', { scope }) + .andWhere('p.date_deleted IS NULL') + .getOne(); + } +} diff --git a/src/APIs/articles/repositories/update-articles.repository.ts b/src/APIs/articles/repositories/update-articles.repository.ts new file mode 100644 index 0000000..f9c1a12 --- /dev/null +++ b/src/APIs/articles/repositories/update-articles.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UpdateArticlesRepository extends Repository
{ + constructor(private dataSource: DataSource) { + super(Article, dataSource.createEntityManager()); + } +} diff --git a/src/APIs/articles/services/create-articles.service.ts b/src/APIs/articles/services/create-articles.service.ts new file mode 100644 index 0000000..7dab38c --- /dev/null +++ b/src/APIs/articles/services/create-articles.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class CreateArticlesService { + constructor() {} +} diff --git a/src/APIs/articles/services/delete-articles.service.ts b/src/APIs/articles/services/delete-articles.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/articles/services/read-articles.service.ts b/src/APIs/articles/services/read-articles.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/articles/services/update-articles.service.ts b/src/APIs/articles/services/update-articles.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index 11b57d5..7494700 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { Column, @@ -11,11 +10,10 @@ import { } from 'typeorm'; @Entity() -export class Likes { - // refactoring => pk 예측가능 값이어도 상관 없는 경우 A_I_로 하기 - @ApiProperty({ description: 'PK: uuid', type: String }) +export class Like { + @ApiProperty({ description: 'PK: uuid', type: Number }) @PrimaryGeneratedColumn('uuid') - id: string; + id: number; @ApiProperty({ description: '좋아요를 누른 유저', type: User }) @JoinColumn() @@ -24,7 +22,7 @@ export class Likes { @ApiProperty({ description: '유저 아이디', type: Number }) @Column() - @RelationId((like: Likes) => like.user) + @RelationId((like: Like) => like.user) userKakaoId: number; @ApiProperty({ @@ -44,6 +42,6 @@ export class Likes { description: '게시글 아이디', }) @Column() - @RelationId((like: Likes) => like.posts) + @RelationId((like: Like) => like.posts) postsId: number; } diff --git a/src/APIs/postBackgrounds/entities/postBackground.entity.ts b/src/APIs/postBackgrounds/entities/postBackground.entity.ts deleted file mode 100644 index eeed43b..0000000 --- a/src/APIs/postBackgrounds/entities/postBackground.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity() -export class PostBackground { - @ApiProperty({ description: 'PK: uuid', type: String }) - @PrimaryGeneratedColumn('uuid') - id: string; - - @ApiProperty({ type: String, description: '이미지가 저장된 url' }) - @Column({ nullable: false }) - image_url: string; -} diff --git a/src/APIs/postBackgrounds/postBackgrounds.module.ts b/src/APIs/postBackgrounds/postBackgrounds.module.ts deleted file mode 100644 index c5d0389..0000000 --- a/src/APIs/postBackgrounds/postBackgrounds.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/strategies/jwt.strategy'; -import { UtilsModule } from 'src/utils/utils.module'; -import { PostBackground } from './entities/postBackground.entity'; -import { PostBackgroundsService } from './postBackgrounds.service'; -import { PostBackgroundsController } from './postBackgrounds.controller'; -import { AwsModule } from 'src/modules/aws/aws.module'; - -@Module({ - imports: [TypeOrmModule.forFeature([PostBackground]), UtilsModule, AwsModule], - providers: [JwtStrategy, PostBackgroundsService], - controllers: [PostBackgroundsController], -}) -export class PostBackgroundsModule {} diff --git a/src/app.module.ts b/src/app.module.ts index b99d5c6..eff5813 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,8 +8,8 @@ import { UsersModule } from './APIs/users/users.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; import { FollowsModule } from './APIs/follows/follows.module'; -import { PostBackgroundsModule } from './APIs/postBackgrounds/postBackgrounds.module'; -import { PostCategoriesModule } from './APIs/postCategories/PostCategories.module'; +import { PostBackgroundsModule } from './APIs/articleBackgrounds/postBackgrounds.module'; +import { PostCategoriesModule } from './APIs/articleCategories/PostCategories.module'; import { LikesModule } from './APIs/likes/likes.module'; import { StickersModule } from './APIs/stickers/stickers.module'; import { StickerCategoriesModule } from './APIs/stickerCategories/stickerCategories.module'; diff --git a/src/common/dto/image-upload-response.dto.ts b/src/common/dto/image-upload-response.dto.ts index 085041d..685fbfe 100644 --- a/src/common/dto/image-upload-response.dto.ts +++ b/src/common/dto/image-upload-response.dto.ts @@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger'; export class ImageUploadResponseDto { @ApiProperty({ type: String, description: '이미지가 저장된 url' }) - image_url: string; + imageUrl: string; } diff --git a/src/common/entities/common.entity.ts b/src/common/entities/common.entity.ts index 13523b2..495239e 100644 --- a/src/common/entities/common.entity.ts +++ b/src/common/entities/common.entity.ts @@ -12,5 +12,5 @@ export abstract class CommonEntity { @ApiProperty({ type: Date, description: '삭제된 날짜' }) @DeleteDateColumn({ name: 'date_deleted' }) - date_deleted: Date; + dateDeleted: Date; } From 207f4d54ab7c33ce2433d5688c80dea22e90ac8b Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 16:44:28 +0900 Subject: [PATCH 190/236] refactor(articleCategories): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../entities/articleBackground.entity.ts | 5 ++ .../PostCategories.module.ts | 14 ---- ...ler.ts => articleCategories.controller.ts} | 82 ++++++++----------- .../articleCategories.module.ts | 15 ++++ ...ory.ts => articleCategories.repository.ts} | 6 +- ...ervice.ts => articleCategories.service.ts} | 39 +++++---- .../dtos/articleCategory.dto.ts | 7 ++ .../dtos/create-articleCategory.dto.ts | 8 ++ .../dtos/create-post-category-response.dto.ts | 28 ------- .../dtos/create-post-category.dto.ts | 11 --- .../dtos/fetch-articleCategory.dto.ts | 14 ++++ .../dtos/fetch-post-category.dto.ts | 14 ---- .../dtos/patch-articleCategory.dto.ts | 6 ++ .../dtos/patch-post-category.dto.ts | 4 - .../entities/articleCategory.entity.ts | 44 ++++++++++ .../entities/postCategory.entity.ts | 35 -------- 16 files changed, 153 insertions(+), 179 deletions(-) delete mode 100644 src/APIs/articleCategories/PostCategories.module.ts rename src/APIs/articleCategories/{PostCategories.controller.ts => articleCategories.controller.ts} (52%) create mode 100644 src/APIs/articleCategories/articleCategories.module.ts rename src/APIs/articleCategories/{PostCategories.repository.ts => articleCategories.repository.ts} (81%) rename src/APIs/articleCategories/{PostCategories.service.ts => articleCategories.service.ts} (53%) create mode 100644 src/APIs/articleCategories/dtos/articleCategory.dto.ts create mode 100644 src/APIs/articleCategories/dtos/create-articleCategory.dto.ts delete mode 100644 src/APIs/articleCategories/dtos/create-post-category-response.dto.ts delete mode 100644 src/APIs/articleCategories/dtos/create-post-category.dto.ts create mode 100644 src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts delete mode 100644 src/APIs/articleCategories/dtos/fetch-post-category.dto.ts create mode 100644 src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts delete mode 100644 src/APIs/articleCategories/dtos/patch-post-category.dto.ts create mode 100644 src/APIs/articleCategories/entities/articleCategory.entity.ts delete mode 100644 src/APIs/articleCategories/entities/postCategory.entity.ts diff --git a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts index c41a8e7..df15110 100644 --- a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts +++ b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts @@ -12,6 +12,11 @@ export class ArticleBackground { @Column({ nullable: false }) imageUrl: string; + @ApiProperty({ + type: () => [Article], + description: '연결된 게시글', + nullable: true, + }) @OneToMany(() => Article, (article) => article.articleBackgroundId) articles: Article[]; } diff --git a/src/APIs/articleCategories/PostCategories.module.ts b/src/APIs/articleCategories/PostCategories.module.ts deleted file mode 100644 index 84f4a7e..0000000 --- a/src/APIs/articleCategories/PostCategories.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { PostCategory } from './entities/postCategory.entity'; -import { PostCategoriesService } from './PostCategories.service'; -import { PostCategoriesController } from './PostCategories.controller'; -import { PostCategoriesRepository } from './PostCategories.repository'; -import { FollowsModule } from '../follows/follows.module'; - -@Module({ - imports: [TypeOrmModule.forFeature([PostCategory]), FollowsModule], - providers: [PostCategoriesService, PostCategoriesRepository], - controllers: [PostCategoriesController], -}) -export class PostCategoriesModule {} diff --git a/src/APIs/articleCategories/PostCategories.controller.ts b/src/APIs/articleCategories/articleCategories.controller.ts similarity index 52% rename from src/APIs/articleCategories/PostCategories.controller.ts rename to src/APIs/articleCategories/articleCategories.controller.ts index 01daf82..36f5ade 100644 --- a/src/APIs/articleCategories/PostCategories.controller.ts +++ b/src/APIs/articleCategories/articleCategories.controller.ts @@ -17,21 +17,25 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { PostCategoriesService } from './PostCategories.service'; import { Request } from 'express'; -import { CreatePostCategoryDto } from './dtos/create-post-category.dto'; -import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { - FetchPostCategoryDto, - FetchPostCategoriesDto, -} from './dtos/fetch-post-category.dto'; -import { PatchPostCategoryDto } from './dtos/patch-post-category.dto'; + FetchArticleCategoriesResponse, + FetchArticleCategoryResponse, +} from './dtos/fetch-articleCategory.dto'; +import { + CreateArticleCategoryInput, + CreateArticleCategoryResponse, +} from './dtos/create-articleCategory.dto'; +import { ArticleCategoriesService } from './articleCategories.service'; +import { PatchArticleCategoryInput } from './dtos/patch-articleCategory.dto'; @ApiTags('유저 API') @Controller('users') -export class PostCategoriesController { - constructor(private readonly postCategoriesService: PostCategoriesService) {} +export class ArticleCategoriesController { + constructor( + private readonly articleCategoriesService: ArticleCategoriesService, + ) {} @ApiOperation({ summary: '특정 유저의 카테고리 전체 조회', @@ -39,16 +43,16 @@ export class PostCategoriesController { '특정 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', }) @ApiOkResponse({ - type: [FetchPostCategoriesDto], + type: [FetchArticleCategoriesResponse], }) @Get(':userId/categories') @HttpCode(200) - async fetchPostCategories( + async fetchArticleCategories( @Req() req: Request, @Param('userId') targetKakaoId: number, - ): Promise { + ): Promise { const kakaoId = req.user.userId; - return await this.postCategoriesService.fetchAll({ + return await this.articleCategoriesService.fetchAll({ kakaoId, targetKakaoId, }); @@ -58,13 +62,13 @@ export class PostCategoriesController { summary: '특정 카테고리 조회', description: 'id에 해당하는 카테고리를 조회한다.', }) - @ApiOkResponse({ type: FetchPostCategoryDto }) + @ApiOkResponse({ type: FetchArticleCategoryResponse }) @Get('categories/:categoryId') async fetchMyCategory( @Req() req: Request, @Param('categoryId') id: string, - ): Promise { - return await this.postCategoriesService.findWithId({ id }); + ): Promise { + return await this.articleCategoriesService.findWithId({ id }); } @ApiOperation({ @@ -74,54 +78,32 @@ export class PostCategoriesController { @ApiCookieAuth() @ApiCreatedResponse({ description: '카테고리 생성 완료', - type: CreatePostCategoryResponseDto, + type: CreateArticleCategoryResponse, }) @UseGuards(AuthGuardV2) @Post('me/categories') @HttpCode(201) - async createPostCategory( + async createArticleCategory( @Req() req: Request, - @Body() body: CreatePostCategoryDto, - ): Promise { + @Body() body: CreateArticleCategoryInput, + ): Promise { const kakaoId = req.user.userId; const name = body.name; - return await this.postCategoriesService.create({ kakaoId, name }); + return await this.articleCategoriesService.create({ kakaoId, name }); } - // @ApiOperation({ - // summary: '로그인된 유저의 카테고리 전체 조회', - // description: - // '로그인된 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', - // }) - // @ApiCookieAuth() - // @ApiOkResponse({ - // type: [FetchPostCategoriesDto], - // }) - // @UseGuards(AuthGuardV2) - // @Get('me/categories') - // async fetchMyCategories( - // @Req() req: Request, - // ): Promise { - // const kakaoId = req.user.userId; - // console.log('kakaoId: ', kakaoId); - // return await this.postCategoriesService.fetchAll({ - // kakaoId, - // targetKakaoId: kakaoId, - // }); - // } - @ApiOperation({ summary: '로그인된 유저의 특정 카테고리 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: FetchPostCategoryDto }) + @ApiOkResponse({ type: FetchArticleCategoryResponse }) @UseGuards(AuthGuardV2) @Patch('me/categories/:categoryId') - async patchCategory( + async patchArticleCategory( @Req() req: Request, @Param('categoryId') id: string, - @Body() body: PatchPostCategoryDto, - ): Promise { + @Body() body: PatchArticleCategoryInput, + ): Promise { const kakaoId = req.user.userId; - return await this.postCategoriesService.patch({ + return await this.articleCategoriesService.patch({ kakaoId, id, ...body, @@ -136,11 +118,11 @@ export class PostCategoriesController { @ApiCookieAuth() @Delete('me/categories/:categoryId') @UseGuards(AuthGuardV2) - async deletePostCategory( + async deleteArticleCategory( @Req() req: Request, @Param('categoryId') id: string, ) { const kakaoId = req.user.userId; - return await this.postCategoriesService.delete({ kakaoId, id }); + return await this.articleCategoriesService.delete({ kakaoId, id }); } } diff --git a/src/APIs/articleCategories/articleCategories.module.ts b/src/APIs/articleCategories/articleCategories.module.ts new file mode 100644 index 0000000..b3e84dd --- /dev/null +++ b/src/APIs/articleCategories/articleCategories.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FollowsModule } from '../follows/follows.module'; +import { ArticleCategory } from './entities/articleCategory.entity'; +import { ArticleCategoriesRepository } from './articleCategories.repository'; +import { ArticleCategoriesService } from './articleCategories.service'; +import { ArticleCategoriesController } from './articleCategories.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([ArticleCategory]), FollowsModule], + providers: [ArticleCategoriesService, ArticleCategoriesRepository], + controllers: [ArticleCategoriesController], +}) +export class ArticleCategoriesModule {} diff --git a/src/APIs/articleCategories/PostCategories.repository.ts b/src/APIs/articleCategories/articleCategories.repository.ts similarity index 81% rename from src/APIs/articleCategories/PostCategories.repository.ts rename to src/APIs/articleCategories/articleCategories.repository.ts index 33b4d9c..d09fa4b 100644 --- a/src/APIs/articleCategories/PostCategories.repository.ts +++ b/src/APIs/articleCategories/articleCategories.repository.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { PostCategory } from './entities/postCategory.entity'; import { DataSource, Repository } from 'typeorm'; +import { ArticleCategory } from './entities/articleCategory.entity'; @Injectable() -export class PostCategoriesRepository extends Repository { +export class ArticleCategoriesRepository extends Repository { constructor(private dataSource: DataSource) { - super(PostCategory, dataSource.createEntityManager()); + super(ArticleCategory, dataSource.createEntityManager()); } async fetchUserCategory({ scope, userKakaoId }) { diff --git a/src/APIs/articleCategories/PostCategories.service.ts b/src/APIs/articleCategories/articleCategories.service.ts similarity index 53% rename from src/APIs/articleCategories/PostCategories.service.ts rename to src/APIs/articleCategories/articleCategories.service.ts index 27aee71..5416ee7 100644 --- a/src/APIs/articleCategories/PostCategories.service.ts +++ b/src/APIs/articleCategories/articleCategories.service.ts @@ -4,50 +4,49 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { CreatePostCategoryResponseDto } from './dtos/create-post-category-response.dto'; -import { PostCategoriesRepository } from './PostCategories.repository'; - -import { - FetchPostCategoriesDto, - FetchPostCategoryDto, -} from './dtos/fetch-post-category.dto'; import { FollowsService } from '../follows/follows.service'; +import { ArticleCategoriesRepository } from './articleCategories.repository'; +import { CreateArticleCategoryResponse } from './dtos/create-articleCategory.dto'; +import { + FetchArticleCategoriesResponse, + FetchArticleCategoryResponse, +} from './dtos/fetch-articleCategory.dto'; @Injectable() -export class PostCategoriesService { +export class ArticleCategoriesService { constructor( private readonly followsService: FollowsService, - private readonly postCategoriesRepository: PostCategoriesRepository, + private readonly articleCategoryRepository: ArticleCategoriesRepository, ) {} async findWithName({ kakaoId, name }) { - return await this.postCategoriesRepository.find({ + return await this.articleCategoryRepository.find({ where: { user: { kakaoId }, name }, }); } - async create({ kakaoId, name }): Promise { + async create({ kakaoId, name }): Promise { const data = await this.findWithName({ kakaoId, name }); if (data.length > 0) { throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); } - const result = await this.postCategoriesRepository.save({ + const result = await this.articleCategoryRepository.save({ user: { kakaoId }, name, }); return result; } - async patch({ kakaoId, id, name }): Promise { + async patch({ kakaoId, id, name }): Promise { const data = await this.findWithId({ id }); if (!data) throw new NotFoundException('카테고리를 찾을 수 없습니다.'); - if (data.userKakaoId != kakaoId) + if (data.userId != kakaoId) throw new ForbiddenException('카테고리를 수정할 권한이 없습니다.'); data.name = name; - return await this.postCategoriesRepository.save(data); + return await this.articleCategoryRepository.save(data); } - async findWithId({ id }): Promise { - return await this.postCategoriesRepository.findOne({ + async findWithId({ id }): Promise { + return await this.articleCategoryRepository.findOne({ where: { id }, }); } @@ -55,19 +54,19 @@ export class PostCategoriesService { async fetchAll({ kakaoId, targetKakaoId, - }): Promise { + }): Promise { const scope = await this.followsService.getScope({ from_user: targetKakaoId, to_user: kakaoId, }); - return await this.postCategoriesRepository.fetchUserCategory({ + return await this.articleCategoryRepository.fetchUserCategory({ userKakaoId: targetKakaoId, scope, }); } delete({ kakaoId, id }) { - return this.postCategoriesRepository.delete({ + return this.articleCategoryRepository.delete({ id, user: { kakaoId }, }); diff --git a/src/APIs/articleCategories/dtos/articleCategory.dto.ts b/src/APIs/articleCategories/dtos/articleCategory.dto.ts new file mode 100644 index 0000000..fdb543f --- /dev/null +++ b/src/APIs/articleCategories/dtos/articleCategory.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { ArticleCategory } from '../entities/articleCategory.entity'; + +export class ArticleCategoryDto extends OmitType(ArticleCategory, [ + 'user', + 'articles', +]) {} diff --git a/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts new file mode 100644 index 0000000..45cc12f --- /dev/null +++ b/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts @@ -0,0 +1,8 @@ +import { PickType } from '@nestjs/swagger'; +import { ArticleCategoryDto } from './articleCategory.dto'; + +export class CreateArticleCategoryInput extends PickType(ArticleCategoryDto, [ + 'name', +]) {} + +export class CreateArticleCategoryResponse extends ArticleCategoryDto {} diff --git a/src/APIs/articleCategories/dtos/create-post-category-response.dto.ts b/src/APIs/articleCategories/dtos/create-post-category-response.dto.ts deleted file mode 100644 index 77694ef..0000000 --- a/src/APIs/articleCategories/dtos/create-post-category-response.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ApiProperty, PickType } from '@nestjs/swagger'; -import { User } from 'src/APIs/users/entities/user.entity'; - -export class CreatePostCategoryResponseDto { - @ApiProperty({ - type: String, - description: 'PK: uuid', - }) - id: string; - - @ApiProperty({ - type: String, - description: '카테고리 이름', - }) - name: string; - - @ApiProperty({ - type: Number, - description: '유저 kakaoId', - }) - userKakaoId: number; - - @ApiProperty({ - type: PickType(User, ['kakaoId']), - description: '유저의 picktype', - }) - user: User; -} diff --git a/src/APIs/articleCategories/dtos/create-post-category.dto.ts b/src/APIs/articleCategories/dtos/create-post-category.dto.ts deleted file mode 100644 index e2650be..0000000 --- a/src/APIs/articleCategories/dtos/create-post-category.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -export class CreatePostCategoryDto { - @ApiProperty({ - type: String, - description: '카테고리 이름', - }) - @IsNotEmpty() - name: string; -} diff --git a/src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts new file mode 100644 index 0000000..71c582a --- /dev/null +++ b/src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArticleCategoryDto } from './articleCategory.dto'; +export class FetchArticleCategoryResponse extends ArticleCategoryDto {} + +export class FetchArticleCategoriesResponse { + @ApiProperty({ type: Number }) + postCount: number; + + @ApiProperty({ type: String }) + categoryId: string; + + @ApiProperty({ type: String }) + categoryName: string; +} diff --git a/src/APIs/articleCategories/dtos/fetch-post-category.dto.ts b/src/APIs/articleCategories/dtos/fetch-post-category.dto.ts deleted file mode 100644 index e7bbdf8..0000000 --- a/src/APIs/articleCategories/dtos/fetch-post-category.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { PostCategory } from '../entities/postCategory.entity'; -export class FetchPostCategoryDto extends OmitType(PostCategory, ['user']) {} - -export class FetchPostCategoriesDto { - @ApiProperty({ type: Number }) - postCount: number; - - @ApiProperty({ type: String }) - categoryId: string; - - @ApiProperty({ type: String }) - categoryName: string; -} diff --git a/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts new file mode 100644 index 0000000..57c52be --- /dev/null +++ b/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { ArticleCategoryDto } from './articleCategory.dto'; + +export class PatchArticleCategoryInput extends PickType(ArticleCategoryDto, [ + 'name', +]) {} diff --git a/src/APIs/articleCategories/dtos/patch-post-category.dto.ts b/src/APIs/articleCategories/dtos/patch-post-category.dto.ts deleted file mode 100644 index 4c125ef..0000000 --- a/src/APIs/articleCategories/dtos/patch-post-category.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { PostCategory } from '../entities/postCategory.entity'; - -export class PatchPostCategoryDto extends PickType(PostCategory, ['name']) {} diff --git a/src/APIs/articleCategories/entities/articleCategory.entity.ts b/src/APIs/articleCategories/entities/articleCategory.entity.ts new file mode 100644 index 0000000..62a64c7 --- /dev/null +++ b/src/APIs/articleCategories/entities/articleCategory.entity.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsString } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; +import { User } from 'src/APIs/users/entities/user.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + RelationId, +} from 'typeorm'; + +@Entity() +export class ArticleCategory { + @ApiProperty({ type: String, description: 'PK: A_I_' }) + @PrimaryGeneratedColumn() + @IsNumber() + id: number; + + @ApiProperty({ type: String, description: '카테고리 이름' }) + @Column({ nullable: false }) + @IsString() + name: string; + + @ApiProperty({ type: User, description: '연결된 유저' }) + @JoinColumn() + @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) + user: User; + + @ApiProperty({ type: Number, description: '유저 아이디' }) + @Column({ name: 'user_id' }) + @RelationId((articleCategory: ArticleCategory) => articleCategory.user) + userId: number; + + @ApiProperty({ + type: () => [Article], + description: '연결된 게시글', + nullable: true, + }) + @OneToMany(() => Article, (article) => article.articleCategoryId) + articles: Article[]; +} diff --git a/src/APIs/articleCategories/entities/postCategory.entity.ts b/src/APIs/articleCategories/entities/postCategory.entity.ts deleted file mode 100644 index 714617d..0000000 --- a/src/APIs/articleCategories/entities/postCategory.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; -import { User } from 'src/APIs/users/entities/user.entity'; -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - RelationId, -} from 'typeorm'; - -@Entity() -export class PostCategory { - @ApiProperty({ type: String, description: 'PK: uuid' }) - @PrimaryGeneratedColumn('uuid') - id: string; - - @ApiProperty({ type: String, description: '카테고리 이름' }) - @Column({ nullable: false }) - name: string; - - @JoinColumn() - @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) - user: User; - - @OneToMany(() => Posts, (posts) => posts.postCategory) - posts: Posts; - - @ApiProperty({ type: Number, description: '유저 아이디' }) - @Column() - @RelationId((postCategory: PostCategory) => postCategory.user) - userKakaoId: number; -} From 10d71365ef5908e011e7a812076dece72312f51d Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:25:29 +0900 Subject: [PATCH 191/236] refactor(user): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../entities/articleBackground.entity.ts | 5 +- .../entities/articleCategory.entity.ts | 5 +- src/APIs/articles/entities/article.entity.ts | 20 +- src/APIs/comments/entities/comment.entity.ts | 2 +- src/APIs/reports/entities/report.entity.ts | 9 + src/APIs/users/entities/user.entity.ts | 183 ++++++++++++++---- src/app.module.ts | 4 +- 7 files changed, 175 insertions(+), 53 deletions(-) diff --git a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts index df15110..248e50e 100644 --- a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts +++ b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Article } from 'src/APIs/articles/entities/article.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; @Entity() -export class ArticleBackground { +export class ArticleBackground extends CommonEntity { @ApiProperty({ description: 'PK: A_I_', type: Number }) @PrimaryGeneratedColumn() id: number; @@ -17,6 +18,6 @@ export class ArticleBackground { description: '연결된 게시글', nullable: true, }) - @OneToMany(() => Article, (article) => article.articleBackgroundId) + @OneToMany(() => Article, (article) => article.articleBackground) articles: Article[]; } diff --git a/src/APIs/articleCategories/entities/articleCategory.entity.ts b/src/APIs/articleCategories/entities/articleCategory.entity.ts index 62a64c7..6a2183c 100644 --- a/src/APIs/articleCategories/entities/articleCategory.entity.ts +++ b/src/APIs/articleCategories/entities/articleCategory.entity.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNumber, IsString } from 'class-validator'; import { Article } from 'src/APIs/articles/entities/article.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, @@ -13,7 +14,7 @@ import { } from 'typeorm'; @Entity() -export class ArticleCategory { +export class ArticleCategory extends CommonEntity { @ApiProperty({ type: String, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() @IsNumber() @@ -39,6 +40,6 @@ export class ArticleCategory { description: '연결된 게시글', nullable: true, }) - @OneToMany(() => Article, (article) => article.articleCategoryId) + @OneToMany(() => Article, (article) => article.articleCategory) articles: Article[]; } diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 3f7fc15..b058849 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -7,8 +7,8 @@ import { IsString, IsUrl, } from 'class-validator'; -import { PostBackground } from 'src/APIs/articleBackgrounds/entities/postBackground.entity'; -import { PostCategory } from 'src/APIs/articleCategories/entities/postCategory.entity'; +import { ArticleBackground } from 'src/APIs/articleBackgrounds/entities/articleBackground.entity'; +import { ArticleCategory } from 'src/APIs/articleCategories/entities/articleCategory.entity'; import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { CommonEntity } from 'src/common/entities/common.entity'; @@ -137,16 +137,16 @@ export class Article extends CommonEntity { onDelete: 'CASCADE', }) @JoinColumn() - postCategory: PostCategory; + articleCategory: ArticleCategory; @ApiProperty({ description: '연결된 내지', type: PostBackground }) @JoinColumn() @ManyToOne(() => PostBackground, { nullable: true, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', + onUpdate: 'SET NULL', + onDelete: 'SET NULL', }) - postBackground: PostBackground; + articleBackground: ArticleBackground; @ApiProperty({ description: '작성자', type: User }) @JoinColumn() @@ -162,7 +162,7 @@ export class Article extends CommonEntity { description: '연결된 댓글', nullable: true, }) - @OneToMany(() => Comment, (comment) => comment.articleId) + @OneToMany(() => Comment, (comment) => comment.article) comments: Comment[]; @ApiProperty({ @@ -170,7 +170,7 @@ export class Article extends CommonEntity { description: '연결된 알림', nullable: true, }) - @OneToMany(() => Notification, (notification) => notification.articleId) + @OneToMany(() => Notification, (notification) => notification.article) notifications: Notification[]; @ApiProperty({ @@ -178,7 +178,7 @@ export class Article extends CommonEntity { description: '연결된 신고', nullable: true, }) - @OneToMany(() => Report, (report) => report.articleId) + @OneToMany(() => Report, (report) => report.article) reports: Report[]; @ApiProperty({ @@ -186,6 +186,6 @@ export class Article extends CommonEntity { description: '연결된 스티커블럭', nullable: true, }) - @OneToMany(() => StickerBlock, (stickerBlock) => stickerBlock.articleId) + @OneToMany(() => StickerBlock, (stickerBlock) => stickerBlock.article) stickerBlocks: StickerBlock[]; } diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 0acf78e..b6c4001 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -27,7 +27,7 @@ export class Comment { userKakaoId: number; @ApiProperty({ type: User, description: '사용자 정보' }) - @ManyToOne(() => User, (users) => users.kakaoId, { nullable: false }) + @ManyToOne(() => User, (users) => users.id, { nullable: false }) @JoinColumn() user: User; diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 73bf38b..272cfb8 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -38,6 +38,15 @@ export class Report { @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; + @ApiProperty({ type: Number, description: '신고당한 유저 id' }) + @Column() + @RelationId((report: Report) => report.targetUser) + targetUserId: number; + + @JoinColumn() + @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + targetUser: User; + @IsEnum(ReportType) @ApiProperty({ type: 'enum', enum: ReportType, description: '신고 유형' }) @Column() diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 57cb6bb..3528173 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -1,75 +1,186 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - // PrimaryColumn, - // PrimaryGeneratedColumn, -} from 'typeorm'; +import { IsBoolean, IsNumber, IsString, IsUrl } from 'class-validator'; +import { Agreement } from 'src/APIs/agreements/entities/agreement.entity'; +import { ArticleCategory } from 'src/APIs/articleCategories/entities/articleCategory.entity'; +import { Article } from 'src/APIs/articles/entities/article.entity'; +import { Comment } from 'src/APIs/comments/entities/comment.entity'; +import { Feedback } from 'src/APIs/feedbacks/entities/feedback.entity'; +import { Follow } from 'src/APIs/follows/entities/follow.entity'; +import { Like } from 'src/APIs/likes/entities/like.entity'; +import { Notification } from 'src/APIs/notifications/entities/notification.entity'; +import { Report } from 'src/APIs/reports/entities/report.entity'; +import { Sticker } from 'src/APIs/stickers/entities/sticker.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; +import { Column, Entity, Index, OneToMany } from 'typeorm'; @Index('ngramUser', ['username'], { fulltext: true, parser: 'ngram' }) @Entity() -export class User { +export class User extends CommonEntity { + @ApiProperty({ description: 'PK: id', type: Number }) @Column({ type: 'bigint', primary: true }) - @ApiProperty({ description: '카카오 id', type: Number }) - kakaoId: number; + @IsNumber() + id: number; - @Column({ unique: true }) @ApiProperty({ description: '유저 핸들러', type: String }) + @Column({ unique: true }) + @IsString() handle: string; - @Column({ default: '' }) @ApiProperty({ description: 'crypted refresh token', type: String }) - current_refresh_token: string; + @Column({ name: 'current_refresh_token', default: '' }) + @IsString() + currentRefreshToken: string; - @Column({ default: false }) @ApiProperty({ description: '어드민 유저 여부', type: Boolean }) + @Column({ name: 'is_admin', default: false }) + @IsBoolean() isAdmin: boolean; - @Column({ default: 0 }) @ApiProperty({ description: '팔로잉 수', type: Number, required: false, default: 0, }) - following_count: number; + @Column({ name: 'following_count', default: 0 }) + @IsNumber() + followingCount: number; - @Column({ default: 0 }) @ApiProperty({ description: '팔로워 수', type: Number, required: false, default: 0, }) - follower_count: number; + @Column({ name: 'follower_count', default: 0 }) + @IsNumber() + followerCount: number; + @ApiProperty({ description: '유저 이름', type: String, uniqueItems: true }) @Column({ unique: true }) - @ApiProperty({ description: '유저 이름', type: String }) + @IsString() username: string; + @ApiProperty({ description: '유저 설명', type: String, default: '' }) @Column({ default: '' }) - @ApiProperty({ description: '유저 설명', type: String }) + @IsString() description: string; - @Column({ default: '' }) - @ApiProperty({ description: '프로필 이미지 url', type: String }) - profile_image: string; + @ApiProperty({ description: '프로필 이미지 url', type: String, default: '' }) + @Column({ name: 'profile_image', default: '' }) + @IsUrl() + profileImage: string; - @Column({ default: '' }) - @ApiProperty({ description: '프로필 배경 이미지 url', type: String }) - background_image: string; + @ApiProperty({ + description: '프로필 배경 이미지 url', + type: String, + default: '', + }) + @Column({ name: 'background_image', default: '' }) + @IsUrl() + backgroundImage: string; + + @ApiProperty({ + type: () => [Agreement], + description: '연결된 동의내역', + nullable: true, + }) + @OneToMany(() => Agreement, (agreement) => agreement.user) + agreements: Agreement[]; + + @ApiProperty({ + type: () => [Article], + description: '연결된 게시글', + nullable: true, + }) + @OneToMany(() => Article, (article) => article.user) + articles: Article[]; - @CreateDateColumn() - @ApiProperty({ description: '생성된 날짜', type: Date }) - date_created: Date; + @ApiProperty({ + type: () => [ArticleCategory], + description: '연결된 게시글 카테고리', + nullable: true, + }) + @OneToMany(() => ArticleCategory, (articleCategory) => articleCategory.user) + articleCategories: ArticleCategory[]; - @DeleteDateColumn() - @ApiProperty({ description: '삭제된 날짜', type: Date }) - date_deleted: Date; + @ApiProperty({ + type: () => [Comment], + description: '연결된 댓글', + nullable: true, + }) + @OneToMany(() => Comment, (comment) => comment.user) + comments: Comment[]; - // @OneToMany(() => Agreement, (agreement) => agreement.user) - // agreements: Agreement[]; + @ApiProperty({ + type: () => [Feedback], + description: '연결된 피드백', + nullable: true, + }) + @OneToMany(() => Feedback, (feedback) => feedback.user) + feedbacks: Feedback[]; + + @ApiProperty({ + type: () => [Follow], + description: '연결된 팔로잉', + nullable: true, + }) + @OneToMany(() => Follow, (follow) => follow.from_user) + followings: Follow[]; + + @ApiProperty({ + type: () => [Follow], + description: '연결된 팔로워', + nullable: true, + }) + @OneToMany(() => Follow, (follow) => follow.to_user) + followers: Follow[]; + + @ApiProperty({ + type: () => [Like], + description: '연결된 좋아요', + nullable: true, + }) + @OneToMany(() => Like, (like) => like.user) + likes: Like[]; + + @ApiProperty({ + type: () => [Notification], + description: '받은 알림', + nullable: true, + }) + @OneToMany(() => Notification, (notification) => notification.targetUser) + receivedNotifications: Notification[]; + + @ApiProperty({ + type: () => [Notification], + description: '보낸 알림', + nullable: true, + }) + @OneToMany(() => Notification, (notification) => notification.user) + sentNotifications: Notification[]; + + @ApiProperty({ + type: () => [Report], + description: '받은 신고', + nullable: true, + }) + @OneToMany(() => Report, (report) => report.targetUser) + receivedReports: Report[]; + + @ApiProperty({ + type: () => [Report], + description: '보낸 신고', + nullable: true, + }) + @OneToMany(() => Report, (report) => report.user) + sentReports: Report[]; + + @ApiProperty({ + type: () => [Sticker], + description: '연결된 스티커', + nullable: true, + }) + @OneToMany(() => Sticker, (sticker) => sticker.user) + stickers: Sticker[]; } diff --git a/src/app.module.ts b/src/app.module.ts index eff5813..a08df95 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,8 +8,8 @@ import { UsersModule } from './APIs/users/users.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; import { FollowsModule } from './APIs/follows/follows.module'; -import { PostBackgroundsModule } from './APIs/articleBackgrounds/postBackgrounds.module'; -import { PostCategoriesModule } from './APIs/articleCategories/PostCategories.module'; +import { PostBackgroundsModule } from './APIs/articleBackgrounds/articleBackgrounds.module'; +import { PostCategoriesModule } from './APIs/articleCategories/articleCategories.module'; import { LikesModule } from './APIs/likes/likes.module'; import { StickersModule } from './APIs/stickers/stickers.module'; import { StickerCategoriesModule } from './APIs/stickerCategories/stickerCategories.module'; From 5edcd56660349743ff556ad9de2fb767b2ecf090 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:33:22 +0900 Subject: [PATCH 192/236] refactor(comment): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- src/APIs/articles/entities/article.entity.ts | 6 +- src/APIs/comments/entities/comment.entity.ts | 61 ++++++++++---------- src/common/entities/indexed-common.entity.ts | 22 +++++++ 3 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 src/common/entities/indexed-common.entity.ts diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index b058849..060965a 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -9,9 +9,11 @@ import { } from 'class-validator'; import { ArticleBackground } from 'src/APIs/articleBackgrounds/entities/articleBackground.entity'; import { ArticleCategory } from 'src/APIs/articleCategories/entities/articleCategory.entity'; +import { Comment } from 'src/APIs/comments/entities/comment.entity'; +import { Notification } from 'src/APIs/notifications/entities/notification.entity'; import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; import { User } from 'src/APIs/users/entities/user.entity'; -import { CommonEntity } from 'src/common/entities/common.entity'; +import { IndexedCommonEntity } from 'src/common/entities/indexed-common.entity'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { Column, @@ -24,7 +26,7 @@ import { } from 'typeorm'; @Entity() -export class Article extends CommonEntity { +export class Article extends IndexedCommonEntity { @ApiProperty({ description: '게시글의 고유 아이디', type: Number }) @PrimaryGeneratedColumn() @IsNumber() diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index b6c4001..2b09e62 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -1,30 +1,31 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; +import { IsNumber, IsString } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; +import { Report } from 'src/APIs/reports/entities/report.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { IndexedCommonEntity } from 'src/common/entities/indexed-common.entity'; import { Column, - CreateDateColumn, - DeleteDateColumn, Entity, - Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, RelationId, - UpdateDateColumn, } from 'typeorm'; @Entity() -export class Comment { +export class Comment extends IndexedCommonEntity { @ApiProperty({ type: Number, description: '댓글 id' }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ type: Number, description: '작성자 유저 아이디' }) - @Column() + @Column({ name: 'user_id' }) @RelationId((comment: Comment) => comment.user) - userKakaoId: number; + @IsNumber() + userId: number; @ApiProperty({ type: User, description: '사용자 정보' }) @ManyToOne(() => User, (users) => users.id, { nullable: false }) @@ -32,26 +33,29 @@ export class Comment { user: User; @ApiProperty({ type: Number, description: '게시글 id' }) - @Column() - @RelationId((comment: Comment) => comment.posts) - postsId: number; + @Column({ name: 'article_id' }) + @RelationId((comment: Comment) => comment.article) + @IsNumber() + articleId: number; - @ApiProperty({ type: Posts, description: '게시글 정보' }) - @ManyToOne(() => Posts, (posts) => posts.id, { + @ApiProperty({ type: Article, description: '게시글 정보' }) + @ManyToOne(() => Article, (article) => article.id, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) @JoinColumn() - posts: Posts; + article: Article; - @ApiProperty({ type: String, description: '내용 정보' }) + @ApiProperty({ type: String, description: '내용 정보', maxLength: 1500 }) @Column({ length: 1500 }) + @IsString() content: string; - @ApiProperty({ type: Number, description: '신고 당한 횟수' }) - @Column({ default: 0 }) - report_count: number; + @ApiProperty({ type: Number, description: '신고 당한 횟수', default: 0 }) + @Column({ name: 'report_count', default: 0 }) + @IsNumber() + reportCount: number; @ApiProperty({ type: Comment, description: '루트 댓글 정보' }) @ManyToOne(() => Comment, (comment) => comment.children, { @@ -63,7 +67,7 @@ export class Comment { parent: Comment; @ApiProperty({ type: Number, description: '루트 댓글 아이디' }) - @Column({ nullable: true }) + @Column({ name: 'parent_id', nullable: true }) @RelationId((comment: Comment) => comment.parent) parentId: number; @@ -71,16 +75,11 @@ export class Comment { @OneToMany(() => Comment, (comment) => comment.parent) children: Comment[]; - @Index() - @ApiProperty({ type: Date, description: '생성 날짜' }) - @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) - date_created: Date; - - @ApiProperty({ type: Date, description: '수정 날짜' }) - @UpdateDateColumn() - date_updated: Date; - - @ApiProperty({ type: Date, description: '논리 삭제 칼럼', nullable: true }) - @DeleteDateColumn() - date_deleted: Date; + @ApiProperty({ + type: () => [Report], + description: '연결된 신고', + nullable: true, + }) + @OneToMany(() => Report, (report) => report.comment) + reports: Report[]; } diff --git a/src/common/entities/indexed-common.entity.ts b/src/common/entities/indexed-common.entity.ts new file mode 100644 index 0000000..f0794bc --- /dev/null +++ b/src/common/entities/indexed-common.entity.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + CreateDateColumn, + DeleteDateColumn, + Index, + UpdateDateColumn, +} from 'typeorm'; + +export abstract class IndexedCommonEntity { + @Index() + @ApiProperty({ type: Date, description: '생성된 날짜' }) + @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) + dateCreated: Date; + + @ApiProperty({ type: Date, description: '수정된 날짜' }) + @UpdateDateColumn({ name: 'date_updated' }) + dateUpdated: Date; + + @ApiProperty({ type: Date, description: '삭제된 날짜' }) + @DeleteDateColumn({ name: 'date_deleted' }) + dateDeleted: Date; +} From 91b04e6a2110c7462490ed194ef6633d22f0dde2 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:36:32 +0900 Subject: [PATCH 193/236] refactor(feedback): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase --- .../feedbacks/entities/feedback.entity.ts | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/APIs/feedbacks/entities/feedback.entity.ts b/src/APIs/feedbacks/entities/feedback.entity.ts index 5c3b148..704006f 100644 --- a/src/APIs/feedbacks/entities/feedback.entity.ts +++ b/src/APIs/feedbacks/entities/feedback.entity.ts @@ -1,11 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNumber, IsString } from 'class-validator'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { FeedbackType } from 'src/common/enums/feedback-type.enum'; import { Column, - CreateDateColumn, - DeleteDateColumn, Entity, JoinColumn, ManyToOne, @@ -14,45 +13,37 @@ import { } from 'typeorm'; @Entity() -export class Feedback { - @IsNumber() +export class Feedback extends CommonEntity { @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() + @IsNumber() id: number; - @IsString() @ApiProperty({ type: String, description: '피드백 내용' }) @Column() + @IsString() content: string; - @ApiProperty() - @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { + @ApiProperty({ + type: Number, + description: '피드백 보낸 유저의 카카오 아이디', + }) + @Column({ name: 'user_id', nullable: true }) + @RelationId((feedback: Feedback) => feedback.user) + @IsNumber() + userId: number; + + @ApiProperty({ type: User, description: '사용자 정보' }) + @ManyToOne(() => User, (users) => users.id, { nullable: true, onUpdate: 'NO ACTION', onDelete: 'SET NULL', }) + @JoinColumn() user: User; @ApiProperty({ description: '피드백 종류', type: 'enum', enum: FeedbackType }) - @IsEnum(FeedbackType) @Column() + @IsEnum(FeedbackType) type: FeedbackType; - - @IsNumber() - @ApiProperty({ - type: Number, - description: '피드백 보낸 유저의 카카오 아이디', - }) - @Column({ nullable: true }) - @RelationId((feedback: Feedback) => feedback.user) - userKakaoId: number; - - @ApiProperty({ type: Date, description: '생성한 날짜' }) - @CreateDateColumn() - date_created: Date; - - @ApiProperty({ type: Date, description: '삭제한 날짜' }) - @DeleteDateColumn() - date_deleted: Date; } From ea0cd2fef02d7d9578d259d0852c38b26c42dff8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:39:37 +0900 Subject: [PATCH 194/236] refactor(follow): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- src/APIs/articles/entities/article.entity.ts | 12 ++++---- src/APIs/follows/entities/follow.entity.ts | 31 +++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 060965a..f6766e9 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -32,20 +32,20 @@ export class Article extends IndexedCommonEntity { @IsNumber() id: number; - @ApiProperty({ description: '연결된 카테고리 fk', type: String }) + @ApiProperty({ description: '연결된 카테고리 fk', type: Number }) @Column({ name: 'article_category_id', nullable: true }) - @RelationId((article: Article) => article.postCategory) + @RelationId((article: Article) => article.articleCategory) @IsString() @IsOptional() - articleCategoryId: string; + articleCategoryId: number; @IsString() - @ApiProperty({ description: '연결된 내지 fk', type: String }) + @ApiProperty({ description: '연결된 내지 fk', type: Number }) @Column({ name: 'article_background_id', nullable: true }) - @RelationId((article: Article) => article.postBackground) + @RelationId((article: Article) => article.articleBackground) @IsString() @IsOptional() - articleBackgroundId: string; + articleBackgroundId: number; @ApiProperty({ description: '작성한 유저 fk', type: Number }) @Column({ name: 'user_id', nullable: false }) diff --git a/src/APIs/follows/entities/follow.entity.ts b/src/APIs/follows/entities/follow.entity.ts index ee61911..9e6d5e5 100644 --- a/src/APIs/follows/entities/follow.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, @@ -11,36 +13,37 @@ import { } from 'typeorm'; @Entity() -export class Follow { - @ApiProperty({ type: String, description: 'PK: uuid' }) - @PrimaryGeneratedColumn('uuid') - id: string; +export class Follow extends CommonEntity { + @ApiProperty({ type: String, description: 'PK: A_I_' }) + @PrimaryGeneratedColumn() + @IsNumber() + id: number; @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 받은 유저' }) @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { + @ManyToOne(() => User, (users) => users.id, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) - to_user: User; + toUser: User; @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 한 유저' }) @JoinColumn() - @ManyToOne(() => User, (users) => users.kakaoId, { + @ManyToOne(() => User, (users) => users.id, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) - from_user: User; + fromUser: User; @ApiProperty({ type: Number, description: '이웃 추가를 받은 유저' }) - @Column() - @RelationId((follow: Follow) => follow.to_user) - toUserKakaoId: number; + @Column({ name: 'to_user_id' }) + @RelationId((follow: Follow) => follow.toUser) + toUserId: number; @ApiProperty({ type: Number, description: '이웃 추가를 한 유저' }) - @Column() - @RelationId((follow: Follow) => follow.from_user) - fromUserKakaoId: number; + @Column({ name: 'from_user_id' }) + @RelationId((follow: Follow) => follow.fromUser) + fromUserId: number; } From 23bf281fb67166a15c08241090ecac7a728a9741 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:46:36 +0900 Subject: [PATCH 195/236] refactor(like): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- src/APIs/likes/entities/like.entity.ts | 30 +++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index 7494700..f8f988f 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -1,5 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, @@ -10,9 +13,10 @@ import { } from 'typeorm'; @Entity() -export class Like { - @ApiProperty({ description: 'PK: uuid', type: Number }) - @PrimaryGeneratedColumn('uuid') +export class Like extends CommonEntity { + @ApiProperty({ description: 'PK: number', type: Number }) + @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ description: '좋아요를 누른 유저', type: User }) @@ -21,27 +25,29 @@ export class Like { user: User; @ApiProperty({ description: '유저 아이디', type: Number }) - @Column() + @Column({ name: 'user_id' }) @RelationId((like: Like) => like.user) - userKakaoId: number; + @IsNumber() + userId: number; @ApiProperty({ - description: '좋아요를 누른 포스트', - type: Posts, + description: '좋아요를 누른 게시글', + type: Article, }) @JoinColumn() - @ManyToOne(() => Posts, { + @ManyToOne(() => Article, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) - posts: Posts; + article: Article; @ApiProperty({ type: Number, description: '게시글 아이디', }) - @Column() - @RelationId((like: Like) => like.posts) - postsId: number; + @Column({ name: 'article_id' }) + @RelationId((like: Like) => like.article) + @IsNumber() + articleId: number; } From c5e7e90fe7f8c8ccf3d7bb89e80a08372cbf9ed3 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:47:10 +0900 Subject: [PATCH 196/236] refactor(notification): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase --- .../entities/notification.entity.ts | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index b6769a6..9130da7 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; +import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { NotType } from 'src/common/enums/not-type.enum'; import { Column, - CreateDateColumn, - DeleteDateColumn, Entity, JoinColumn, ManyToOne, @@ -14,65 +14,63 @@ import { } from 'typeorm'; @Entity() -export class Notification { +export class Notification extends CommonEntity { @ApiProperty({ description: 'PK: A_I_', type: Number }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ description: '알림을 생성한 유저 정보', type: User }) @JoinColumn() - @ManyToOne(() => User) + @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @ApiProperty({ description: '알림을 생성한 유저 FK', type: Number }) - @Column() + @Column({ name: 'user_id' }) @RelationId((notification: Notification) => notification.user) - userKakaoId: number; + @IsNumber() + userId: number; @ApiProperty({ description: '알림을 받는 유저 정보', type: User }) @JoinColumn() - @ManyToOne(() => User) + @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @ApiProperty({ description: '알림을 받는 유저 FK', type: Number }) - @Column() + @Column({ name: 'target_user_id' }) @RelationId((notification: Notification) => notification.targetUser) - targetUserKakaoId: number; + @IsNumber() + targetUserId: number; @ApiProperty({ description: '알림의 유형', type: 'enum', enum: NotType }) @Column() + @IsEnum(NotType) type: NotType; @ApiProperty({ description: '알림 체크 여부', type: Boolean, default: false }) - @Column({ default: false }) - is_checked: boolean; - - @ApiProperty({ description: '생성된 날짜', type: Date }) - @CreateDateColumn() - date_created: Date; - - @ApiProperty({ description: '삭제된 날짜', type: Date }) - @DeleteDateColumn() - date_deleted: Date; + @Column({ name: 'is_checked', default: false }) + @IsBoolean() + isChecked: boolean; @ApiProperty({ type: Number, description: '알림이 발생한 게시글 id(nullable)', }) - @Column({ nullable: true }) - @RelationId((notification: Notification) => notification.post) - postId: number; + @Column({ name: 'article_id', nullable: true }) + @RelationId((notification: Notification) => notification.article) + @IsNumber() + articleId: number; @ApiProperty({ - type: Posts, + type: Article, description: '알림이 발생한 게시물', nullable: true, }) @JoinColumn() - @ManyToOne(() => Posts, { + @ManyToOne(() => Article, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL', }) // 게시물을 참조하는 경우 - post: Posts; + article: Article; } From b4674848cccfe55c3f99c49e46d058716473a71e Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:52:20 +0900 Subject: [PATCH 197/236] refactor(report): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- src/APIs/reports/entities/report.entity.ts | 47 +++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 272cfb8..4053caa 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -1,13 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; +import { IsEnum, IsNumber, IsString } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; import { Comment } from 'src/APIs/comments/entities/comment.entity'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { ReportTarget } from 'src/common/enums/report-target.enum'; import { ReportType } from 'src/common/enums/report-type.enum'; import { Column, - CreateDateColumn, Entity, JoinColumn, ManyToOne, @@ -16,33 +16,35 @@ import { } from 'typeorm'; @Entity() -export class Report { +export class Report extends CommonEntity { @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ type: String, description: '신고 내용' }) @Column() + @IsString() content: string; - @ApiProperty({ type: Date, description: '생성된 날짜' }) - @CreateDateColumn() - date_created: Date; - @ApiProperty({ type: Number, description: '신고한 유저 id' }) - @Column() + @Column({ name: 'user_id' }) @RelationId((report: Report) => report.user) - userKakaoId: number; + @IsNumber() + userId: number; + @ApiProperty({ description: '신고를 한 유저', type: User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @ApiProperty({ type: Number, description: '신고당한 유저 id' }) - @Column() + @Column({ name: 'target_user_id' }) @RelationId((report: Report) => report.targetUser) + @IsNumber() targetUserId: number; + @ApiProperty({ description: '신고를 당한 유저', type: User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @@ -50,34 +52,33 @@ export class Report { @IsEnum(ReportType) @ApiProperty({ type: 'enum', enum: ReportType, description: '신고 유형' }) @Column() + @IsEnum(ReportType) type: ReportType; - @IsEnum(ReportTarget) @ApiProperty({ type: 'enum', enum: ReportTarget, description: '신고 대상' }) @Column() + @IsEnum(ReportTarget) target: ReportTarget; - @ApiProperty({ type: String, description: '신고가 발생한 게시물의 url' }) - @Column() - url: string; - @ApiProperty({ type: Number, description: '신고당한 게시글 id' }) - @Column({ nullable: true }) - @RelationId((report: Report) => report.post) - postId: number; + @Column({ name: 'article_id', nullable: true }) + @RelationId((report: Report) => report.article) + @IsNumber() + articleId: number; - @ApiProperty({ type: Posts, description: '신고된 게시물', nullable: true }) + @ApiProperty({ type: Article, description: '신고된 게시물', nullable: true }) @JoinColumn() - @ManyToOne(() => Posts, { + @ManyToOne(() => Article, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL', }) // 게시물을 참조하는 경우 - post: Posts; + article: Article; @ApiProperty({ type: Number, description: '신고당한 댓글 id' }) - @Column({ nullable: true }) + @Column({ name: 'comment_id', nullable: true }) @RelationId((report: Report) => report.comment) + @IsNumber() commentId: number; @ApiProperty({ type: Comment, description: '신고된 댓글', nullable: true }) From 19a6aa63a895299a19ad2a28243bdcd0b2dd32ca Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:54:23 +0900 Subject: [PATCH 198/236] refactor(stickerBlock): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../entities/stickerblock.entity.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 4744a87..674c1d2 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; +import { IsNumber } from 'class-validator'; +import { Article } from 'src/APIs/articles/entities/article.entity'; import { Sticker } from 'src/APIs/stickers/entities/sticker.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, @@ -11,14 +13,16 @@ import { } from 'typeorm'; @Entity() -export class StickerBlock { +export class StickerBlock extends CommonEntity { @ApiProperty({ description: 'PK: A_I_', type: Number }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ description: '참조하는 스티커의 아이디', type: Number }) - @Column() + @Column({ name: 'sticker_id' }) @RelationId((stickerBlock: StickerBlock) => stickerBlock.sticker) + @IsNumber() stickerId: number; @ApiProperty({ description: '참조하는 스티커', type: Sticker }) @@ -30,17 +34,18 @@ export class StickerBlock { sticker: Sticker; @ApiProperty({ description: '참조하는 포스트 아이디', type: Number }) - @Column() + @Column({ name: 'article_id' }) @RelationId((stickerBlock: StickerBlock) => stickerBlock.posts) - postsId: number; + @IsNumber() + articleId: number; - @ApiProperty({ description: '참조하는 포스트', type: Posts }) + @ApiProperty({ description: '참조하는 포스트', type: Article }) @JoinColumn() - @ManyToOne(() => Posts, (posts) => posts.id, { + @ManyToOne(() => Article, (article) => article.id, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) - posts: Posts; + article: Article; @ApiProperty({ description: '스티커의 posX', type: Number }) @Column({ type: 'float' }) From 70cd400b24f60aa990d46f3013e95efdb8c24282 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:58:15 +0900 Subject: [PATCH 199/236] refactor(stickerCategory): unify naming convention - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../entities/stickerCategory.entity.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/APIs/stickerCategories/entities/stickerCategory.entity.ts b/src/APIs/stickerCategories/entities/stickerCategory.entity.ts index 76f5d86..5e78624 100644 --- a/src/APIs/stickerCategories/entities/stickerCategory.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategory.entity.ts @@ -1,13 +1,29 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { IsNumber, IsString } from 'class-validator'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { StickerCategoryMapper } from './stickerCategoryMapper.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; @Entity() -export class StickerCategory { +export class StickerCategory extends CommonEntity { @ApiProperty({ description: 'PK: A_I_', type: Number }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ description: '카테고리 이름', type: String }) @Column({ nullable: false, unique: true }) + @IsString() name: string; + + @ApiProperty({ + type: () => [StickerCategoryMapper], + description: '연결된 스티커 카테고리 매퍼', + nullable: true, + }) + @OneToMany( + () => StickerCategoryMapper, + (stickerCategoryMapper) => stickerCategoryMapper.stickerCategory, + ) + stickerCategoryMappers: StickerCategoryMapper[]; } From 2869e69f5477892b7249f51df8afceda725442ec Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 17:59:24 +0900 Subject: [PATCH 200/236] refactor(entity): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- .../entities/stickerCategoryMapper.entity.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts index da7cf9b..f97c6c2 100644 --- a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts @@ -8,15 +8,18 @@ import { } from 'typeorm'; import { StickerCategory } from './stickerCategory.entity'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; +import { CommonEntity } from 'src/common/entities/common.entity'; @Entity() -export class StickerCategoryMapper { +export class StickerCategoryMapper extends CommonEntity { @ApiProperty({ type: Number, description: '스티커 아이디' }) - @PrimaryColumn() + @PrimaryColumn({ name: 'sticker_id' }) @RelationId( (stickerCategoryMapper: StickerCategoryMapper) => stickerCategoryMapper.sticker, ) + @IsNumber() stickerId: number; @JoinColumn() @@ -27,11 +30,12 @@ export class StickerCategoryMapper { sticker: Sticker; @ApiProperty({ type: Number, description: '스티커 카테고리 아이디' }) - @PrimaryColumn() + @PrimaryColumn({ name: 'sticker_category_id' }) @RelationId( (stickerCategoryMapper: StickerCategoryMapper) => stickerCategoryMapper.stickerCategory, ) + @IsNumber() stickerCategoryId: number; @JoinColumn() From b4cb8b12ac78cbafc4c0514da8ea15a4023d2442 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 18:03:00 +0900 Subject: [PATCH 201/236] refactor(sticker): unify naming convention - Unified entity naming convention to CamelCase - Unified DB column naming convention to SnakeCase - Introduced OneToMany relationship - Extended CommonEntity to include date fields (dateCreated, dateUpdated, dateDeleted) --- src/APIs/articles/entities/article.entity.ts | 1 + src/APIs/stickers/entities/sticker.entity.ts | 35 +++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index f6766e9..9f79f1b 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -11,6 +11,7 @@ import { ArticleBackground } from 'src/APIs/articleBackgrounds/entities/articleB import { ArticleCategory } from 'src/APIs/articleCategories/entities/articleCategory.entity'; import { Comment } from 'src/APIs/comments/entities/comment.entity'; import { Notification } from 'src/APIs/notifications/entities/notification.entity'; +import { Report } from 'src/APIs/reports/entities/report.entity'; import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; import { User } from 'src/APIs/users/entities/user.entity'; import { IndexedCommonEntity } from 'src/common/entities/indexed-common.entity'; diff --git a/src/APIs/stickers/entities/sticker.entity.ts b/src/APIs/stickers/entities/sticker.entity.ts index 7a9317f..b502324 100644 --- a/src/APIs/stickers/entities/sticker.entity.ts +++ b/src/APIs/stickers/entities/sticker.entity.ts @@ -1,26 +1,32 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNumber, IsUrl } from 'class-validator'; +import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, JoinColumn, ManyToOne, + OneToMany, PrimaryGeneratedColumn, RelationId, } from 'typeorm'; @Entity() -export class Sticker { +export class Sticker extends CommonEntity { @ApiProperty({ description: 'PK: A_I', type: Number }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ description: '제작한 유저 fk', type: Number }) - @Column() + @Column({ name: 'user_id' }) @RelationId((sticker: Sticker) => sticker.user) - userKakaoId: number; + @IsNumber() + userId: number; - // @ApiProperty({ description: '제작한 유저', type: User }) + @ApiProperty({ description: '제작한 유저', type: User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', @@ -34,20 +40,33 @@ export class Sticker { type: String, nullable: false, }) - @Column({ nullable: false }) - image_url: string; + @Column({ name: 'image_url', nullable: false }) + @IsUrl() + imageUrl: string; @ApiProperty({ description: '블꾸 기본 제공 스티커 유무', type: Boolean, + default: false, }) - @Column({ nullable: false, default: false }) + @Column({ name: 'is_default', nullable: false, default: false }) + @IsBoolean() isDefault: boolean; @ApiProperty({ description: '재사용 가능 유무', type: Boolean, + default: false, }) - @Column({ nullable: false, default: false }) + @Column({ name: 'is_reusable', nullable: false, default: false }) + @IsBoolean() isReusable: boolean; + + @ApiProperty({ + type: () => [StickerBlock], + description: '연결된 스티커블럭', + nullable: true, + }) + @OneToMany(() => StickerBlock, (stickerBlock) => stickerBlock.sticker) + stickerBlocks: StickerBlock[]; } From 1fb8974c3100ca91e492e655b5e3c35f5aef91eb Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 4 Jul 2024 18:31:08 +0900 Subject: [PATCH 202/236] refactor(article): divide repository --- .../articles/dtos/article-response.dto.ts | 16 +- src/APIs/articles/dtos/article.dto.ts | 10 +- src/APIs/articles/entities/article.entity.ts | 8 +- .../articles.repository.interface.ts | 4 +- .../articles/olds/cursor-fetch-posts.dto.ts | 2 +- src/APIs/articles/olds/fetch-posts.dto.ts | 2 +- .../repositories/articles.repository.ts | 238 ------------------ .../create-articles.repository.ts | 2 +- .../paginate-articles.repository.ts.ts | 149 +++++++++++ .../repositories/read-articles.repository.ts | 39 ++- .../entities/stickerblock.entity.ts | 2 +- ...ter-option.ts => article-filter-option.ts} | 4 +- ...rder-option.ts => article-order-option.ts} | 4 +- 13 files changed, 218 insertions(+), 262 deletions(-) delete mode 100644 src/APIs/articles/repositories/articles.repository.ts create mode 100644 src/APIs/articles/repositories/paginate-articles.repository.ts.ts rename src/common/enums/{posts-filter-option.ts => article-filter-option.ts} (72%) rename src/common/enums/{posts-order-option.ts => article-order-option.ts} (76%) diff --git a/src/APIs/articles/dtos/article-response.dto.ts b/src/APIs/articles/dtos/article-response.dto.ts index e4d8760..cbd232a 100644 --- a/src/APIs/articles/dtos/article-response.dto.ts +++ b/src/APIs/articles/dtos/article-response.dto.ts @@ -1,15 +1,15 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Posts } from '../entities/article.entity'; +import { Article } from '../entities/article.entity'; -export class PostResponseDto extends OmitType(Posts, ['user']) { +export class ArticleDetailResponse extends OmitType(Article, [ + 'comments', + 'user', + 'stickerBlocks', + 'reports', + 'notifications', +]) { @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) user: UserPrimaryResponseDto; } - -export class PostOnlyResponseDto extends OmitType(Posts, [ - 'user', - 'postBackground', - 'postCategory', -]) {} diff --git a/src/APIs/articles/dtos/article.dto.ts b/src/APIs/articles/dtos/article.dto.ts index 56a8820..d6fa530 100644 --- a/src/APIs/articles/dtos/article.dto.ts +++ b/src/APIs/articles/dtos/article.dto.ts @@ -1,4 +1,12 @@ import { OmitType } from '@nestjs/swagger'; import { Article } from '../entities/article.entity'; -export class ArticleDto extends OmitType(Article, ['comments', '']) {} +export class ArticleDto extends OmitType(Article, [ + 'comments', + 'user', + 'stickerBlocks', + 'reports', + 'notifications', + 'articleCategory', + 'articleBackground', +]) {} diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 9f79f1b..31694ca 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -133,8 +133,8 @@ export class Article extends IndexedCommonEntity { @IsUrl() mainImageUrl: string; - @ApiProperty({ description: '연결된 카테고리', type: PostCategory }) - @ManyToOne(() => PostCategory, { + @ApiProperty({ description: '연결된 카테고리', type: ArticleCategory }) + @ManyToOne(() => ArticleCategory, { nullable: true, onUpdate: 'CASCADE', onDelete: 'CASCADE', @@ -142,9 +142,9 @@ export class Article extends IndexedCommonEntity { @JoinColumn() articleCategory: ArticleCategory; - @ApiProperty({ description: '연결된 내지', type: PostBackground }) + @ApiProperty({ description: '연결된 내지', type: ArticleBackground }) @JoinColumn() - @ManyToOne(() => PostBackground, { + @ManyToOne(() => ArticleBackground, { nullable: true, onUpdate: 'SET NULL', onDelete: 'SET NULL', diff --git a/src/APIs/articles/interfaces/articles.repository.interface.ts b/src/APIs/articles/interfaces/articles.repository.interface.ts index 353bc5b..82ccf9f 100644 --- a/src/APIs/articles/interfaces/articles.repository.interface.ts +++ b/src/APIs/articles/interfaces/articles.repository.interface.ts @@ -22,12 +22,12 @@ export interface IArticlesRepoFetchArticlesCursor export interface IArticlesRepoFetchFriendsArticlesCursor extends Pick { date_filter: Date; - kakaoId: number; + userId: number; } export interface IArticlesRepoFetchUserArticlesCursor extends Pick { date_filter: Date; scope: OpenScope[]; - userKakaoId: number; + userId: number; } diff --git a/src/APIs/articles/olds/cursor-fetch-posts.dto.ts b/src/APIs/articles/olds/cursor-fetch-posts.dto.ts index b165e6e..3ad3547 100644 --- a/src/APIs/articles/olds/cursor-fetch-posts.dto.ts +++ b/src/APIs/articles/olds/cursor-fetch-posts.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; import { DateOption } from 'src/common/enums/date-option'; -import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; +import { PostsOrderOptionWrap } from 'src/common/enums/article-order-option'; import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; export class CursorFetchPosts extends CustomCursorPageOptionsDto { diff --git a/src/APIs/articles/olds/fetch-posts.dto.ts b/src/APIs/articles/olds/fetch-posts.dto.ts index 6319efb..69143cd 100644 --- a/src/APIs/articles/olds/fetch-posts.dto.ts +++ b/src/APIs/articles/olds/fetch-posts.dto.ts @@ -1,7 +1,7 @@ import { IsEnum, IsOptional, IsString } from 'class-validator'; import { PageRequest } from '../../../utils/pages/page-request'; import { ApiProperty } from '@nestjs/swagger'; -import { PostsOrderOptionWrap } from 'src/common/enums/posts-order-option'; +import { PostsOrderOptionWrap } from 'src/common/enums/article-order-option'; import { PostsFilterOptionWrap } from 'src/common/enums/posts-filter-option'; export class FetchPostsDto extends PageRequest { diff --git a/src/APIs/articles/repositories/articles.repository.ts b/src/APIs/articles/repositories/articles.repository.ts deleted file mode 100644 index 93ba5ee..0000000 --- a/src/APIs/articles/repositories/articles.repository.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Brackets, DataSource, Repository } from 'typeorm'; -import { Article } from '../entities/article.entity'; -import { Injectable } from '@nestjs/common'; -import { OpenScope } from 'src/common/enums/open-scope.enum'; -import { PostResponseDto } from './dtos/article-response.dto'; -import { PostResponseDtoExceptCategory } from './dtos/fetch-article-for-update.dto'; -import { ArticlesOrderOption } from 'src/common/enums/articles-order-option'; -import { ArticlesFilterOption } from 'src/common/enums/articles-filter-option'; -import { SortOption } from 'src/common/enums/sort-option'; -import { - IArticlesRepoFetchFriendsArticlesCursor, - IArticlesRepoFetchArticlesCursor, - IArticlesRepoFetchUserArticlesCursor, - IArticlesRepoGetCursorQuery, -} from '../interfaces/articles.repository.interface'; -import { Follow } from '../../follows/entities/follow.entity'; -@Injectable() -export class ArticlesRepository extends Repository
{ - constructor(private dataSource: DataSource) { - super(Article, dataSource.createEntityManager()); - } - - async fetchPostForUpdate(id) { - return await this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.id = :id', { id }) - .andWhere('p.date_deleted IS NULL') - .getOne(); - } - - async fetchFriendsArticles(subQuery, page) { - return this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where(`p.userKakaoId = any(${subQuery})`) - .andWhere('p.date_deleted IS NULL') - .andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC], - }) //sql injection 방지를 위해 만드시 enum 거칠 것 - .andWhere(`${ArticlesFilterOption[page.filter]} LIKE :search`, { - search: `%${page.search}%`, - }) - .orderBy(`p.${ArticlesOrderOption[page.order]}`, 'DESC') - .andWhere('p.isPublished = true') - .orderBy('p.id', 'DESC') - .take(page.getLimit()) - .skip(page.getOffset()) - .getManyAndCount(); - } - - async fetchTempArticles( - kakaoId: number, - ): Promise { - return this.createQueryBuilder('p') - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.userKakaoId = :kakaoId', { kakaoId }) - .andWhere(`p.isPublished = false`) - .andWhere('p.date_deleted IS NULL') - .orderBy('p.id', 'DESC') - .getMany(); - } - - getCursorQuery({ order, sort, take, cursor }: IArticlesRepoGetCursorQuery) { - const _order = ArticlesOrderOption[order]; - - const queryBuilder = this.createQueryBuilder('p'); - const queryByOrderSort = - sort === SortOption.ASC - ? `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` - : `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; - - queryBuilder - .take(take + 1) - .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.articleBackground', 'articleBackground') - .leftJoinAndSelect('p.articleCategory', 'articleCategory') - .addSelect([ - 'user.handle', - 'user.kakaoId', - 'user.description', - 'user.profile_image', - 'user.username', - ]) - .where('p.isPublished = true') - .andWhere(queryByOrderSort, { - customCursor: cursor, - }) - .andWhere('p.date_deleted IS NULL') - .orderBy(`p.${_order}`, sort as any) - .addOrderBy('p.id', sort as any); - - return queryBuilder; - } - - async fetchArticlesCursor({ - cursorOption, - date_filter, - }: IArticlesRepoFetchArticlesCursor) { - const { order, cursor, take, sort } = cursorOption; - const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - - queryBuilder.andWhere('p.scope IN (:...scopes)', { - scopes: [OpenScope.PUBLIC], - }); - - if (date_filter) { - queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, - }); - } - - const articles: Article[] = await queryBuilder.getMany(); - - return { articles }; - } - - async fetchFriendsArticlesCursor({ - cursorOption, - kakaoId, - date_filter, - }: IArticlesRepoFetchFriendsArticlesCursor) { - const { order, cursor, take, sort } = cursorOption; - const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - - // const subQuery = await this.dataSource - // .createQueryBuilder(Follow, 'n') - // .select('n.toUserKakaoId') - // .where('n.fromUserKakaoId = :kakaoId', { kakaoId }) - // .getQuery(); - - const mutualFollows = await this.dataSource - .createQueryBuilder(Follow, 'f1') - .select('f1.fromUserKakaoId', 'user1') - .addSelect('f1.toUserKakaoId', 'user2') - .innerJoin( - Follow, - 'f2', - 'f1.fromUserKakaoId = f2.toUserKakaoId AND f1.toUserKakaoId = f2.fromUserKakaoId', - ); - - queryBuilder - .innerJoin(Follow, 'f', 'p.userKakaoId = f.toUserKakaoId') - .leftJoin( - `(${mutualFollows.getQuery()})`, - 'mf', - 'p.userKakaoId = mf.user1 AND f.fromUserKakaoId = mf.user2', - ) - .where('f.fromUserKakaoId = :kakaoId', { kakaoId }) - .andWhere( - new Brackets((qb) => { - qb.where('mf.user1 IS NOT NULL AND p.scope IN (:...scopes)', { - scopes: ['PUBLIC', 'PROTECTED'], - }).orWhere('mf.user1 IS NULL AND p.scope = :publicScope', { - publicScope: 'PUBLIC', - }); - }), - ); - - if (date_filter) { - queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, - }); - } - - // queryBuilder - // .andWhere(`p.userKakaoId = any(${subQuery})`) // 만약 서로이웃으로 scope하려면, 정반대 옵션으로 subQuery2를 만들고 andWhere()하나 추가하면 될듯 - // .andWhere('p.scope IN (:...scopes)', { - // scopes: [OpenScope.PUBLIC], - // }); //sql injection 방지를 위해 만드시 enum 거칠 것 - - // if (date_filter) { - // queryBuilder.andWhere('p.date_created > :date_filter', { - // date_filter: date_filter, - // }); - // } - - const articles: Article[] = await queryBuilder.getMany(); - - return { articles }; - } - - async fetchUserArticles({ - cursorOption, - scope, - userKakaoId, - date_filter, - }: IArticlesRepoFetchUserArticlesCursor) { - const { order, cursor, take, sort } = cursorOption; - const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - - if (cursorOption.categoryId) { - queryBuilder.andWhere('articleCategory.id = :categoryId', { - categoryId: cursorOption.categoryId, - }); - } - queryBuilder - .andWhere('p.userKakaoId = :userKakaoId', { - userKakaoId, - }) - .andWhere('p.scope IN (:scope)', { scope }); - - if (date_filter) { - queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, - }); - } - - const articles: Article[] = await queryBuilder.getMany(); - - return { articles }; - } -} diff --git a/src/APIs/articles/repositories/create-articles.repository.ts b/src/APIs/articles/repositories/create-articles.repository.ts index b1e463c..9972e7e 100644 --- a/src/APIs/articles/repositories/create-articles.repository.ts +++ b/src/APIs/articles/repositories/create-articles.repository.ts @@ -7,7 +7,7 @@ export class CreateArticlesRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } - async insertPost(article) { + async insert(article) { return await this.createQueryBuilder() .insert() .into(Article, Object.keys(article)) diff --git a/src/APIs/articles/repositories/paginate-articles.repository.ts.ts b/src/APIs/articles/repositories/paginate-articles.repository.ts.ts new file mode 100644 index 0000000..6958420 --- /dev/null +++ b/src/APIs/articles/repositories/paginate-articles.repository.ts.ts @@ -0,0 +1,149 @@ +import { Brackets, DataSource, Repository } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { SortOption } from 'src/common/enums/sort-option'; +import { ArticleOrderOption } from 'src/common/enums/article-order-option'; +import { OpenScope } from 'src/common/enums/open-scope.enum'; +import { + IArticlesRepoFetchArticlesCursor, + IArticlesRepoFetchFriendsArticlesCursor, + IArticlesRepoFetchUserArticlesCursor, + IArticlesRepoGetCursorQuery, +} from '../interfaces/articles.repository.interface'; +import { Follow } from 'src/APIs/follows/entities/follow.entity'; + +export class PaginateArticlesRepository extends Repository
{ + constructor(private dataSource: DataSource) { + super(Article, dataSource.createEntityManager()); + } + getCursorQuery({ order, sort, take, cursor }: IArticlesRepoGetCursorQuery) { + const _order = ArticleOrderOption[order]; + + const queryBuilder = this.createQueryBuilder('p'); + const queryByOrderSort = + sort === SortOption.ASC + ? `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` + : `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; + + queryBuilder + .take(take + 1) + .innerJoin('p.user', 'user') + .leftJoinAndSelect('p.article_background', 'article_background') + .leftJoinAndSelect('p.article_category', 'article_category') + .addSelect([ + 'user.handle', + 'user.id', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.is_published = true') + .andWhere(queryByOrderSort, { + customCursor: cursor, + }) + .andWhere('p.date_deleted IS NULL') + .orderBy(`p.${_order}`, sort as any) + .addOrderBy('p.id', sort as any); + + return queryBuilder; + } + + async fetchArticlesCursor({ + cursorOption, + date_filter, + }: IArticlesRepoFetchArticlesCursor) { + const { order, cursor, take, sort } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); + + queryBuilder.andWhere('p.scope IN (:...scopes)', { + scopes: [OpenScope.PUBLIC], + }); + + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, + }); + } + + const articles: Article[] = await queryBuilder.getMany(); + + return { articles }; + } + + async fetchFriendsArticlesCursor({ + cursorOption, + userId, + date_filter, + }: IArticlesRepoFetchFriendsArticlesCursor) { + const { order, cursor, take, sort } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); + + const mutualFollows = await this.dataSource + .createQueryBuilder(Follow, 'f1') + .select('f1.from_user_id', 'user1') + .addSelect('f1.to_user_id', 'user2') + .innerJoin( + Follow, + 'f2', + 'f1.from_user_id = f2.to_user_id AND f1.to_user_id = f2.from_user_id', + ); + + queryBuilder + .innerJoin(Follow, 'f', 'p.user_id = f.to_user_id') + .leftJoin( + `(${mutualFollows.getQuery()})`, + 'mf', + 'p.user_id = mf.user1 AND f.from_user_id = mf.user2', + ) + .where('f.from_user_id = :userId', { userId }) + .andWhere( + new Brackets((qb) => { + qb.where('mf.user1 IS NOT NULL AND p.scope IN (:...scopes)', { + scopes: ['PUBLIC', 'PROTECTED'], + }).orWhere('mf.user1 IS NULL AND p.scope = :publicScope', { + publicScope: 'PUBLIC', + }); + }), + ); + + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, + }); + } + + const articles: Article[] = await queryBuilder.getMany(); + + return { articles }; + } + + async fetchUserArticles({ + cursorOption, + scope, + userId, + date_filter, + }: IArticlesRepoFetchUserArticlesCursor) { + const { order, cursor, take, sort } = cursorOption; + const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); + + if (cursorOption.categoryId) { + queryBuilder.andWhere('article_category.id = :categoryId', { + categoryId: cursorOption.categoryId, + }); + } + queryBuilder + .andWhere('p.user_id = :user_id', { + userId, + }) + .andWhere('p.scope IN (:scope)', { scope }); + + if (date_filter) { + queryBuilder.andWhere('p.date_created > :date_filter', { + date_filter: date_filter, + }); + } + + const articles: Article[] = await queryBuilder.getMany(); + + return { articles }; + } +} diff --git a/src/APIs/articles/repositories/read-articles.repository.ts b/src/APIs/articles/repositories/read-articles.repository.ts index 25abff2..1b57708 100644 --- a/src/APIs/articles/repositories/read-articles.repository.ts +++ b/src/APIs/articles/repositories/read-articles.repository.ts @@ -1,13 +1,14 @@ import { DataSource, Repository } from 'typeorm'; import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; +import { ArticleDetailResponse } from '../dtos/article-response.dto'; @Injectable() export class ReadArticlesRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } - async fetchArticlesDetail({ id, scope }): Promise { + async readDetail({ id, scope }): Promise { await this.update(id, { viewCount: () => 'view_count +1', }); @@ -27,4 +28,40 @@ export class ReadArticlesRepository extends Repository
{ .andWhere('p.date_deleted IS NULL') .getOne(); } + + async readUpdateDetail(id) { + return await this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .leftJoinAndSelect('p.article_background', 'article_background') + .leftJoinAndSelect('p.article_category', 'article_category') + .addSelect([ + 'user.handle', + 'user.id', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.id = :id', { id }) + .andWhere('p.date_deleted IS NULL') + .getOne(); + } + + async fetchTempArticles(userId: number): Promise { + return this.createQueryBuilder('p') + .innerJoin('p.user', 'user') + .leftJoinAndSelect('p.article_background', 'article_background') + .leftJoinAndSelect('p.article_category', 'article_category') + .addSelect([ + 'user.handle', + 'user.id', + 'user.description', + 'user.profile_image', + 'user.username', + ]) + .where('p.user_id = :userId', { userId }) + .andWhere(`p.is_published = false`) + .andWhere('p.date_deleted IS NULL') + .orderBy('p.id', 'DESC') + .getMany(); + } } diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 674c1d2..02221de 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -35,7 +35,7 @@ export class StickerBlock extends CommonEntity { @ApiProperty({ description: '참조하는 포스트 아이디', type: Number }) @Column({ name: 'article_id' }) - @RelationId((stickerBlock: StickerBlock) => stickerBlock.posts) + @RelationId((stickerBlock: StickerBlock) => stickerBlock.article) @IsNumber() articleId: number; diff --git a/src/common/enums/posts-filter-option.ts b/src/common/enums/article-filter-option.ts similarity index 72% rename from src/common/enums/posts-filter-option.ts rename to src/common/enums/article-filter-option.ts index 6e72845..89786bf 100644 --- a/src/common/enums/posts-filter-option.ts +++ b/src/common/enums/article-filter-option.ts @@ -1,10 +1,10 @@ -export enum PostsFilterOption { +export enum ArticleFilterOption { TITLE = 'p.title', CONTENT = 'p.content', USER = 'user.username', } // client에게 key값을 받기 위한 wrapping enum -export enum PostsFilterOptionWrap { +export enum ArticleFilterOptionWrap { TITLE = 'TITLE', CONTENT = 'CONTENT', USER = 'USER', diff --git a/src/common/enums/posts-order-option.ts b/src/common/enums/article-order-option.ts similarity index 76% rename from src/common/enums/posts-order-option.ts rename to src/common/enums/article-order-option.ts index fb4c0de..146968b 100644 --- a/src/common/enums/posts-order-option.ts +++ b/src/common/enums/article-order-option.ts @@ -1,4 +1,4 @@ -export enum PostsOrderOption { +export enum ArticleOrderOption { LIKE = 'like_count', VIEW = 'view_count', COMMENT = 'comment_count', @@ -6,7 +6,7 @@ export enum PostsOrderOption { } // client에게 key값을 받기 위한 wrapping enum -export enum PostsOrderOptionWrap { +export enum ArticleOrderOptionWrap { LIKE = 'LIKE', VIEW = 'VIEW', COMMENT = 'COMMENT', From 4980c3d9fd531729c2750006837602fd8a71f3af Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 16:22:34 +0900 Subject: [PATCH 203/236] refactor(article): change dtos & divide layers --- .../articleBackgrounds.controller.ts | 4 +- .../articleBackgrounds.module.ts | 2 +- .../articleBackgrounds.service.ts | 4 +- src/APIs/articles/articles.module.ts | 41 +- src/APIs/articles/articles.service.ts | 359 ------------------ .../controllers/articles-create.controller.ts | 102 +++++ .../controllers/articles-delete.controller.ts | 39 ++ .../controllers/articles-read.controller.ts | 176 +++++++++ .../controllers/articles-update.controller.ts | 45 +++ .../controllers/articles.controller.ts | 278 -------------- .../controllers/create-articles.controller.ts | 8 - .../controllers/delete-articles.controller.ts | 0 .../controllers/read-articles.controller.ts | 0 .../controllers/update-articles.controller.ts | 0 .../articles/dtos/{ => common}/article.dto.ts | 2 +- .../article-create-draft-request.dto.ts | 9 + .../request/article-create-request.dto.ts | 24 ++ .../request/article-delete-request.dto.ts} | 2 +- .../dtos/request/article-patch-request.dto.ts | 6 + .../request/articles-get-request.dto.ts} | 14 +- .../request/articles-get-user-request.dto.ts} | 4 +- .../response/article-create-response.dto.ts | 11 + .../article-detail-for-update-response.dto.ts | 4 + .../article-detail-response.dto.ts} | 4 +- .../response/articles-get-response.dto.ts | 14 + src/APIs/articles/entities/article.entity.ts | 12 +- .../articles.repository.interface.ts | 10 +- .../interfaces/articles.service.interface.ts | 42 +- src/APIs/articles/olds/create-post.input.ts | 87 ----- .../olds/cursor-page-post-response.dto.ts | 14 - .../articles/olds/fetch-friends-posts.dto.ts | 6 - .../articles/olds/fetch-post-detail.dto.ts | 11 - .../olds/fetch-post-for-update.dto.ts | 17 - src/APIs/articles/olds/fetch-posts.dto.ts | 64 ---- .../articles/olds/fetch-user-posts.dto.ts | 7 - .../articles/olds/page-post-response.dto.ts | 16 - src/APIs/articles/olds/patch-post.dto.ts | 4 - src/APIs/articles/olds/post-response.dto.ts | 15 - src/APIs/articles/olds/publish-post.dto.ts | 17 - src/APIs/articles/olds/publish-post.input.ts | 79 ---- ...itory.ts => articles-create.repository.ts} | 2 +- ...itory.ts => articles-delete.repository.ts} | 2 +- ....ts => articles-paginate.repository.ts.ts} | 2 +- ...ository.ts => articles-read.repository.ts} | 16 +- ...itory.ts => articles-update.repository.ts} | 2 +- .../services/articles-create.service.ts | 81 ++++ .../services/articles-delete.service.ts | 49 +++ .../services/articles-paginate.service.ts | 151 ++++++++ .../services/articles-read.service.ts | 74 ++++ .../services/articles-update.service.ts | 37 ++ .../services/articles-validate.service.ts | 50 +++ .../services/create-articles.service.ts | 6 - .../services/delete-articles.service.ts | 0 .../services/read-articles.service.ts | 0 .../services/update-articles.service.ts | 0 .../dtos/common/stickerBlock.dto.ts | 7 + .../dtos/common/stickerBlocks.create.dto.ts | 10 + .../dtos/create-stickerBlock.dto.ts | 16 - .../dtos/create-stickerBlocks.dto.ts | 31 -- .../stickerBlock-create-request.dto.ts | 19 + .../stickerBlocks-create-request.dto.ts | 12 + .../stickerBlocks/stickerBlocks.service.ts | 8 +- src/APIs/stickers/stickers.controller.ts | 2 +- src/APIs/stickers/stickers.module.ts | 2 +- src/APIs/stickers/stickers.service.ts | 4 +- src/APIs/users/users.controller.ts | 4 +- src/APIs/users/users.module.ts | 2 +- src/APIs/users/users.service.ts | 4 +- .../image-upload-response.dto.ts | 0 src/common/{dto => dtos}/image-upload.dto.ts | 0 .../cursor-pages/dtos/cursor-page-meta.dto.ts | 0 .../dtos/cursor-page-option.dto.ts | 0 .../cursor-pages/dtos/cursor-page.dto.ts | 0 .../interfaces/cursor-page-meta-dto-params.ts | 0 src/{ => modules}/utils/pages/page-request.ts | 0 src/{ => modules}/utils/pages/page.ts | 0 src/{ => modules}/utils/utils.module.ts | 0 src/modules/utils/utils.service.ts | 28 ++ src/utils/utils.service.ts | 9 - 79 files changed, 1059 insertions(+), 1124 deletions(-) delete mode 100644 src/APIs/articles/articles.service.ts create mode 100644 src/APIs/articles/controllers/articles-create.controller.ts create mode 100644 src/APIs/articles/controllers/articles-delete.controller.ts create mode 100644 src/APIs/articles/controllers/articles-read.controller.ts create mode 100644 src/APIs/articles/controllers/articles-update.controller.ts delete mode 100644 src/APIs/articles/controllers/articles.controller.ts delete mode 100644 src/APIs/articles/controllers/create-articles.controller.ts delete mode 100644 src/APIs/articles/controllers/delete-articles.controller.ts delete mode 100644 src/APIs/articles/controllers/read-articles.controller.ts delete mode 100644 src/APIs/articles/controllers/update-articles.controller.ts rename src/APIs/articles/dtos/{ => common}/article.dto.ts (79%) create mode 100644 src/APIs/articles/dtos/request/article-create-draft-request.dto.ts create mode 100644 src/APIs/articles/dtos/request/article-create-request.dto.ts rename src/APIs/articles/{olds/delete-post.dto.ts => dtos/request/article-delete-request.dto.ts} (87%) create mode 100644 src/APIs/articles/dtos/request/article-patch-request.dto.ts rename src/APIs/articles/{olds/cursor-fetch-posts.dto.ts => dtos/request/articles-get-request.dto.ts} (53%) rename src/APIs/articles/{olds/fetch-user-posts.input.ts => dtos/request/articles-get-user-request.dto.ts} (69%) create mode 100644 src/APIs/articles/dtos/response/article-create-response.dto.ts create mode 100644 src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts rename src/APIs/articles/dtos/{article-response.dto.ts => response/article-detail-response.dto.ts} (73%) create mode 100644 src/APIs/articles/dtos/response/articles-get-response.dto.ts delete mode 100644 src/APIs/articles/olds/create-post.input.ts delete mode 100644 src/APIs/articles/olds/cursor-page-post-response.dto.ts delete mode 100644 src/APIs/articles/olds/fetch-friends-posts.dto.ts delete mode 100644 src/APIs/articles/olds/fetch-post-detail.dto.ts delete mode 100644 src/APIs/articles/olds/fetch-post-for-update.dto.ts delete mode 100644 src/APIs/articles/olds/fetch-posts.dto.ts delete mode 100644 src/APIs/articles/olds/fetch-user-posts.dto.ts delete mode 100644 src/APIs/articles/olds/page-post-response.dto.ts delete mode 100644 src/APIs/articles/olds/patch-post.dto.ts delete mode 100644 src/APIs/articles/olds/post-response.dto.ts delete mode 100644 src/APIs/articles/olds/publish-post.dto.ts delete mode 100644 src/APIs/articles/olds/publish-post.input.ts rename src/APIs/articles/repositories/{create-articles.repository.ts => articles-create.repository.ts} (87%) rename src/APIs/articles/repositories/{delete-articles.repository.ts => articles-delete.repository.ts} (81%) rename src/APIs/articles/repositories/{paginate-articles.repository.ts.ts => articles-paginate.repository.ts.ts} (98%) rename src/APIs/articles/repositories/{read-articles.repository.ts => articles-read.repository.ts} (79%) rename src/APIs/articles/repositories/{update-articles.repository.ts => articles-update.repository.ts} (81%) create mode 100644 src/APIs/articles/services/articles-create.service.ts create mode 100644 src/APIs/articles/services/articles-delete.service.ts create mode 100644 src/APIs/articles/services/articles-paginate.service.ts create mode 100644 src/APIs/articles/services/articles-read.service.ts create mode 100644 src/APIs/articles/services/articles-update.service.ts create mode 100644 src/APIs/articles/services/articles-validate.service.ts delete mode 100644 src/APIs/articles/services/create-articles.service.ts delete mode 100644 src/APIs/articles/services/delete-articles.service.ts delete mode 100644 src/APIs/articles/services/read-articles.service.ts delete mode 100644 src/APIs/articles/services/update-articles.service.ts create mode 100644 src/APIs/stickerBlocks/dtos/common/stickerBlock.dto.ts create mode 100644 src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts delete mode 100644 src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts delete mode 100644 src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts create mode 100644 src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts create mode 100644 src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts rename src/common/{dto => dtos}/image-upload-response.dto.ts (100%) rename src/common/{dto => dtos}/image-upload.dto.ts (100%) rename src/{ => modules}/utils/cursor-pages/dtos/cursor-page-meta.dto.ts (100%) rename src/{ => modules}/utils/cursor-pages/dtos/cursor-page-option.dto.ts (100%) rename src/{ => modules}/utils/cursor-pages/dtos/cursor-page.dto.ts (100%) rename src/{ => modules}/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts (100%) rename src/{ => modules}/utils/pages/page-request.ts (100%) rename src/{ => modules}/utils/pages/page.ts (100%) rename src/{ => modules}/utils/utils.module.ts (100%) create mode 100644 src/modules/utils/utils.service.ts delete mode 100644 src/utils/utils.service.ts diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index 05c4bd8..fd1454f 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -17,8 +17,8 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from '../../common/dto/image-upload.dto'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; +import { ImageUploadDto } from '../../common/dtos/image-upload.dto'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { ArticleBackground } from './entities/articleBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts index 93ab54e..3db029b 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtStrategy } from '../auth/strategies/jwt.strategy'; -import { UtilsModule } from 'src/utils/utils.module'; +import { UtilsModule } from 'src/modules/utils/utils.module'; import { ArticleBackgroundsController } from './articleBackgrounds.controller'; import { AwsModule } from 'src/modules/aws/aws.module'; import { ArticleBackgroundsService } from './articleBackgrounds.service'; diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts index 6baee64..d728da4 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { UtilsService } from 'src/utils/utils.service'; +import { UtilsService } from 'src/modules/utils/utils.service'; import { ArticleBackground } from './entities/articleBackground.entity'; import { Repository } from 'typeorm'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { AwsService } from 'src/modules/aws/aws.service'; @Injectable() diff --git a/src/APIs/articles/articles.module.ts b/src/APIs/articles/articles.module.ts index 6ef3802..7d23754 100644 --- a/src/APIs/articles/articles.module.ts +++ b/src/APIs/articles/articles.module.ts @@ -1,16 +1,27 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; -import { UtilsModule } from 'src/utils/utils.module'; -import { ArticlesService } from './articles.service'; +import { UtilsModule } from 'src/modules/utils/utils.module'; import { ArticleBackground } from '../articleBackgrounds/entities/articleBackground.entity'; import { ArticleCategory } from '../articleCategories/entities/articleCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; -import { ArticlesRepository } from './repositories/articles.repository'; import { FollowsModule } from '../follows/follows.module'; import { AwsModule } from 'src/modules/aws/aws.module'; -import { ArticlesController } from './controllers/articles.controller'; import { Article } from './entities/article.entity'; +import { ArticlesReadService } from './services/articles-read.service'; +import { ArticlesCreateService } from './services/articles-create.service'; +import { ArticlesDeleteService } from './services/articles-delete.service'; +import { ArticlesUpdateService } from './services/articles-update.service'; +import { ArticlesPaginateService } from './services/articles-paginate.service'; +import { ArticlesCreateRepository } from './repositories/articles-create.repository'; +import { ArticlesReadRepository } from './repositories/articles-read.repository'; +import { ArticlesUpdateRepository } from './repositories/articles-update.repository'; +import { ArticlesDeleteRepository } from './repositories/articles-delete.repository'; +import { ArticlesPaginateRepository } from './repositories/articles-paginate.repository.ts'; +import { ArticlesCreateController } from './controllers/articles-create.controller'; +import { ArticlesReadController } from './controllers/articles-read.controller'; +import { ArticlesUpdateController } from './controllers/articles-update.controller'; +import { ArticlesDeleteController } from './controllers/articles-delete.controller'; @Module({ imports: [ @@ -25,8 +36,24 @@ import { Article } from './entities/article.entity'; FollowsModule, StickerBlocksModule, ], - providers: [ArticlesService, ArticlesRepository], - controllers: [ArticlesController], - exports: [ArticlesService], + providers: [ + ArticlesCreateService, + ArticlesReadService, + ArticlesUpdateService, + ArticlesDeleteService, + ArticlesPaginateService, + ArticlesCreateRepository, + ArticlesReadRepository, + ArticlesUpdateRepository, + ArticlesDeleteRepository, + ArticlesPaginateRepository, + ], + controllers: [ + ArticlesCreateController, + ArticlesReadController, + ArticlesUpdateController, + ArticlesDeleteController, + ], + exports: [], }) export class ArticlesModule {} diff --git a/src/APIs/articles/articles.service.ts b/src/APIs/articles/articles.service.ts deleted file mode 100644 index 5aed0bd..0000000 --- a/src/APIs/articles/articles.service.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { - BadRequestException, - ForbiddenException, - Inject, - Injectable, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; - -import { UtilsService } from 'src/utils/utils.service'; -import { DataSource } from 'typeorm'; -import { User } from '../users/entities/user.entity'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { StickerBlocksService } from '../stickerBlocks/stickerBlocks.service'; -import { ArticlesRepository } from './repositories/articles.repository'; -import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; -import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { FollowsService } from '../follows/follows.service'; -import { DateOption } from 'src/common/enums/date-option'; -import { Follow } from '../follows/entities/follow.entity'; -import { - IArticlesServiceCreate, - IArticlesServiceCreateCursorResponse, - IArticlesServiceFetchFriendsArticlesCursor, - IArticlesServiceFetchArticleForUpdate, - IArticlesServiceFetchArticlesCursor, - IArticlesServiceFetchUserArticlesCursor, - IArticlesServicePatchArticle, - IArticlesServiceArticleId, - IArticlesServiceArticleUserIdPair, -} from './interfaces/articles.service.interface'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; -import { AwsService } from 'src/modules/aws/aws.service'; - -@Injectable() -export class ArticlesService { - constructor( - private readonly awsService: AwsService, - private readonly utilsService: UtilsService, - private readonly dataSource: DataSource, - private readonly stickerBlocksService: StickerBlocksService, - private readonly articlesRepository: ArticlesRepository, - private readonly followsService: FollowsService, - @Inject(CACHE_MANAGER) private cacheManager: Cache, - ) {} - - async imageUpload( - file: Express.Multer.File, - ): Promise { - const imageName = this.utilsService.getUUID(); - const ext = file.originalname.split('.').pop(); - - const image_url = await this.awsService.imageUploadToS3( - `${imageName}.${ext}`, - file, - ext, - 1280, - ); - - return { image_url }; - } - async findArticlesById({ id }: IArticlesServiceArticleId) { - return await this.articlesRepository.findOne({ where: { id } }); - } - - async existCheck({ id }: IArticlesServiceArticleId) { - const data = await this.findArticlesById({ id }); - if (!data) throw new NotFoundException('게시글을 찾을 수 없습니다.'); - return data; - } - - async fkValidCheck({ articles, passNonEssentail }) { - const pc = await this.dataSource - .getRepository(ArticleCategory) - .createQueryBuilder('pc') - .where('pc.id = :id', { id: articles.articleCategoryId }) - .getOne(); - if (!pc && !passNonEssentail) - throw new BadRequestException('존재하지 않는 article_category입니다.'); - const pg = await this.dataSource - .getRepository(ArticleBackground) - .createQueryBuilder('pg') - .where('pg.id = :id', { id: articles.articleBackgroundId }) - .getOne(); - if (!pg && articles.articleBackgroundId && !passNonEssentail) - throw new BadRequestException('존재하지 않는 article_background입니다.'); - const us = await this.dataSource - .getRepository(User) - .createQueryBuilder('us') - .where('us.kakaoId = :id', { id: articles.userKakaoId }) - .getOne(); - if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); - } - - async save( - createArticleDto: IArticlesServiceCreate, - ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - const article = {}; - try { - Object.keys(createArticleDto).map((el) => { - const value = createArticleDto[el]; - if (createArticleDto[el] != null) { - article[el] = value; - } - }); - await this.fkValidCheck({ - articles: article, - passNonEssentail: !createArticleDto.isPublished, - }); - const data = await queryRunner.manager - .createQueryBuilder() - .insert() - .into(Articles, Object.keys(article)) - .values(article) - .execute(); - await queryRunner.commitTransaction(); - const articleData = await this.articlesRepository.findOne({ - where: { id: data.identifiers[0].id }, - }); - const stickerBlockData = await this.stickerBlocksService.bulkInsert({ - articlesId: articleData.id, - kakaoId: createArticleDto.userKakaoId, - stickerBlocks: createArticleDto.stickerBlocks, - }); - return { articleData, stickerBlockData }; - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - } - - async patchArticle({ - kakaoId, - id, - ...rest - }: IArticlesServicePatchArticle): Promise { - const articleData = await this.existCheck({ id }); - if (articleData.userKakaoId != kakaoId) - throw new ForbiddenException('게시글 작성자가 아닙니다.'); - Object.keys(rest).forEach((value) => { - if (rest[value] != null) articleData[value] = rest[value]; - }); - await this.fkValidCheck({ articles: articleData, passNonEssentail: false }); - return await this.articlesRepository.save(articleData); - } - - async fetchArticleForUpdate({ - id, - kakaoId, - }: IArticlesServiceFetchArticleForUpdate): Promise { - const data = await this.existCheck({ id }); - await this.fkValidCheck({ articles: data, passNonEssentail: true }); - if (data.userKakaoId !== kakaoId) - throw new UnauthorizedException('본인이 아닙니다.'); - const article = await this.articlesRepository.fetchArticleForUpdate(id); - const stickerBlocks = await this.stickerBlocksService.fetchBlocks({ - articlesId: id, - }); - return { article, stickerBlocks }; - } - - async fetchTempArticles({ - kakaoId, - }): Promise { - return await this.articlesRepository.fetchTempArticles(kakaoId); - } - - async fetchDetail({ - kakaoId, - id, - }: IArticlesServiceArticleUserIdPair): Promise { - const data = await this.existCheck({ id }); - await this.fkValidCheck({ articles: data, passNonEssentail: false }); - const scope = await this.followsService.getScope({ - from_user: data.userKakaoId, - to_user: kakaoId, - }); - // const comments = await this.commentsService.fetchComments({ articlesId: id }); - const article = await this.articlesRepository.fetchArticleDetail({ - id, - scope, - }); - console.log(data, article); - return article; - } - - async softDelete({ kakaoId, id }: IArticlesServiceArticleUserIdPair) { - const data = await this.articlesRepository.findOne({ - where: { user: { kakaoId }, id }, - }); - if (data) { - await this.awsService.deleteImageFromS3({ url: data.image_url }); - await this.awsService.deleteImageFromS3({ url: data.main_image_url }); - await this.stickerBlocksService.deleteBlocks({ kakaoId, articlesId: id }); - } - return await this.articlesRepository.softDelete({ user: { kakaoId }, id }); - } - - async hardDelete({ kakaoId, id }: IArticlesServiceArticleUserIdPair) { - const data = await this.articlesRepository.findOne({ - where: { user: { kakaoId }, id }, - }); - if (data) { - await this.awsService.deleteImageFromS3({ url: data.image_url }); - await this.awsService.deleteImageFromS3({ url: data.main_image_url }); - await this.stickerBlocksService.deleteBlocks({ kakaoId, articlesId: id }); - } - return await this.articlesRepository.delete({ user: { kakaoId }, id }); - } - - //cursor - async createCursorResponse({ - cursorOption, - articles, - }: IArticlesServiceCreateCursorResponse): Promise< - CustomCursorPageDto - > { - const order = ArticlesOrderOption[cursorOption.order]; - let hasNextData: boolean = true; - let customCursor: string; - - const takePerPage = cursorOption.take; - const isLastPage = articles.length <= takePerPage; - const responseData = articles.slice(0, takePerPage); - const lastDataPerPage = responseData[responseData.length - 1]; - - if (isLastPage) { - hasNextData = false; - customCursor = null; - } else { - customCursor = await this.createCustomCursor({ - article: lastDataPerPage, - order, - }); - } - - const customCursorPageMetaDto = new CustomCursorPageMetaDto({ - customCursorPageOptionsDto: cursorOption, - hasNextData, - customCursor, - }); - - return new CustomCursorPageDto(responseData, customCursorPageMetaDto); - } - - async fetchArticlesCursor({ - cursorOption, - }: IArticlesServiceFetchArticlesCursor): Promise< - CustomCursorPageDto - > { - const cacheKey = `fetchArticlesCursor_${JSON.stringify(cursorOption)}`; - - const cachedArticles = - await this.cacheManager.get>( - cacheKey, - ); - if (cachedArticles) { - return cachedArticles; - } - - let date_filter: Date; - if (cursorOption.date_created) - date_filter = this.getDate(cursorOption.date_created); - const { articles } = await this.articlesRepository.fetchArticlesCursor({ - cursorOption, - date_filter, - }); - const result = await this.createCursorResponse({ articles, cursorOption }); - await this.cacheManager.set(cacheKey, result, 180000); - return result; - } - - async fetchFriendsArticlesCursor({ - cursorOption, - kakaoId, - }: IArticlesServiceFetchFriendsArticlesCursor): Promise< - CustomCursorPageDto - > { - let date_filter: Date; - if (cursorOption.date_created) - date_filter = this.getDate(cursorOption.date_created); - - const { articles } = - await this.articlesRepository.fetchFriendsArticlesCursor({ - cursorOption, - kakaoId, - date_filter, - }); - return await this.createCursorResponse({ articles, cursorOption }); - } - - async fetchUserArticlesCursor({ - kakaoId, - targetKakaoId, - cursorOption, - }: IArticlesServiceFetchUserArticlesCursor): Promise< - CustomCursorPageDto - > { - let date_filter: Date; - if (cursorOption.date_created) - date_filter = this.getDate(cursorOption.date_created); - - const scope = await this.followsService.getScope({ - from_user: targetKakaoId, - to_user: kakaoId, - }); - const { articles } = await this.articlesRepository.fetchUserArticles({ - cursorOption, - date_filter, - scope, - userKakaoId: targetKakaoId, - }); - return await this.createCursorResponse({ articles, cursorOption }); - } - - async createCustomCursor({ article, order }): Promise { - const id = article.id; - const _order = article[order]; - const customCursor: string = - String(_order).padStart(7, '0') + String(id).padStart(7, '0'); - - return customCursor; - } - - createDefaultCursor( - digitById: number, - digitByTargetColumn: number, - initialValue: string, - ) { - const defaultCustomCursor: string = - String().padStart(digitByTargetColumn, `${initialValue}`) + - String().padStart(digitById, `${initialValue}`); - return defaultCustomCursor; - } - - getDate(date_created: DateOption): Date { - let currentDate = new Date(); - switch (date_created) { - case DateOption.WEEK: - currentDate.setDate(currentDate.getDate() - 7); - break; - case DateOption.MONTH: - currentDate.setMonth(currentDate.getMonth() - 1); - break; - case DateOption.YEAR: - currentDate.setFullYear(currentDate.getFullYear() - 1); - break; - default: - currentDate = null; - } - return currentDate; - } -} diff --git a/src/APIs/articles/controllers/articles-create.controller.ts b/src/APIs/articles/controllers/articles-create.controller.ts new file mode 100644 index 0000000..46af0e1 --- /dev/null +++ b/src/APIs/articles/controllers/articles-create.controller.ts @@ -0,0 +1,102 @@ +import { + Body, + Controller, + HttpCode, + Post, + Req, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { Request } from 'express'; +import { + ApiBody, + ApiConsumes, + ApiCookieAuth, + ApiCreatedResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { ArticlesCreateService } from '../services/articles-create.service'; +import { ArticleCreateResponseDto } from '../dtos/response/article-create-response.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { ArticleCreateRequestDto } from '../dtos/request/article-create-request.dto'; +import { ArticleCreateDraftRequestDto } from '../dtos/request/article-create-draft-request.dto'; +import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; + +@ApiTags('게시글 API') +@Controller('articles') +export class ArticlesCreateController { + constructor(private readonly svc_articlesCreate: ArticlesCreateService) {} + + @ApiOperation({ + summary: '게시글 등록', + description: '게시글을 등록한다.', + }) + @Post() + @ApiCookieAuth() + @ApiCreatedResponse({ + description: '등록 성공', + type: ArticleCreateResponseDto, + }) + @UseGuards(AuthGuardV2) + @HttpCode(201) + async publishArticle( + @Req() req: Request, + @Body() body: ArticleCreateRequestDto, + ) { + const userId = req.user.userId; + const dto = { ...body, userId, isPublished: true }; + return await this.svc_articlesCreate.save(dto); + } + + @ApiOperation({ + summary: '게시글 임시등록', + description: '게시글을 임시등록한다.', + }) + @Post('temp') + @ApiCookieAuth() + @ApiCreatedResponse({ + description: '임시등록 성공', + type: ArticleCreateResponseDto, + }) + @UseGuards(AuthGuardV2) + @HttpCode(201) + async updateArticle( + @Req() req: Request, + @Body() body: ArticleCreateDraftRequestDto, + ) { + const userId = req.user.userId; + const dto = { ...body, userId, isPublished: false }; + return await this.svc_articlesCreate.save(dto); + } + + @ApiOperation({ + summary: '이미지 업로드', + description: + '이미지를 서버에 업로드한다. url을 반환 받는다. 게시글 내부 이미지 업로드 및 캡처 이미지 업로드용. max_width=1280px', + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: '업로드 할 파일', + type: ImageUploadDto, + }) + @ApiCreatedResponse({ + description: '이미지 서버에 파일 업로드 완료', + type: ImageUploadResponseDto, + }) + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @Post('image') + @UseInterceptors(FileInterceptor('file')) + @HttpCode(201) + async createPrivateSticker( + @Req() req: Request, + @UploadedFile() file: Express.Multer.File, + ): Promise { + return await this.svc_articlesCreate.imageUpload(file); + } +} +} diff --git a/src/APIs/articles/controllers/articles-delete.controller.ts b/src/APIs/articles/controllers/articles-delete.controller.ts new file mode 100644 index 0000000..9fcfe5b --- /dev/null +++ b/src/APIs/articles/controllers/articles-delete.controller.ts @@ -0,0 +1,39 @@ +import { + Body, + Controller, + Delete, + Param, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ArticlesDeleteService } from '../services/articles-delete.service'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request } from 'express'; +import { ArticleDeleteRequestDto } from '../dtos/request/article-delete-request.dto'; + +@ApiTags('게시글 API') +@Controller('articles') +export class ArticlesDeleteController { + constructor(private readonly svc_articlesDelete: ArticlesDeleteService) {} + + @ApiOperation({ + summary: '게시글 삭제', + description: + '로그인 된 유저의 postId에 해당하는 게시글을 삭제한다. isHardDelete(nullable)을 통해 삭제 방식 결정', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @Delete(':articleId') + async softDelete( + @Req() req: Request, + @Param('articleId') articleId: number, + @Body() body: ArticleDeleteRequestDto, + ) { + const userId = req.user.userId; + if (body.isHardDelete === true) { + return await this.svc_articlesDelete.hardDelete({ userId, articleId }); + } + return await this.svc_articlesDelete.softDelete({ userId, articleId }); + } +} diff --git a/src/APIs/articles/controllers/articles-read.controller.ts b/src/APIs/articles/controllers/articles-read.controller.ts new file mode 100644 index 0000000..cb5b84e --- /dev/null +++ b/src/APIs/articles/controllers/articles-read.controller.ts @@ -0,0 +1,176 @@ +import { + Controller, + Get, + HttpCode, + Param, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { ArticlesReadService } from '../services/articles-read.service'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request } from 'express'; +import { ArticleDto } from '../dtos/common/article.dto'; +import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; +import { ArticleDetailForUpdateResponseDto } from '../dtos/response/article-detail-for-update-response.dto'; +import { ArticlesGetResponseDto } from '../dtos/response/articles-get-response.dto'; +import { ArticlesGetRequestDto } from '../dtos/request/articles-get-request.dto'; +import { ArticlesPaginateService } from '../services/articles-paginate.service'; +import { SortOption } from 'src/common/enums/sort-option'; +import { CustomCursorPageDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page.dto'; + +@ApiTags('게시글 API') +@Controller('articles') +export class ArticlesReadController { + constructor( + private readonly svc_articlesRead: ArticlesReadService, + private readonly svc_articlesPaginate: ArticlesPaginateService, + ) {} + + @ApiOperation({ + summary: '임시작성 게시글 조회', + description: '로그인된 유저의 임시작성 게시글을 조회한다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: [ArticleDto] }) + @UseGuards(AuthGuardV2) + @Get('temp') + async fetchTempArticles(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.svc_articlesRead.readTempArticles({ userId }); + } + + @ApiOperation({ + summary: '게시글 디테일 뷰 fetch', + description: + 'id에 해당하는 게시글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', + }) + @Get('detail/:articleId') + @ApiOkResponse({ type: ArticleDetailResponseDto }) + async fetchArticleDetail( + @Param('articleId') articleId: number, + @Req() req: Request, + ): Promise { + const userId = req.user.userId; + return await this.svc_articlesRead.readArticleDetail({ userId, articleId }); + } + + @ApiOperation({ + summary: '[수정용] 게시글 및 스티커 상세 데이터 fetch', + description: + '본인 게시글 수정용으로 id에 해당하는 게시글에 조인된 스티커 블록들의 값과 게시글 세부 데이터를 모두 가져온다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ type: ArticleDetailForUpdateResponseDto }) + @UseGuards(AuthGuardV2) + @HttpCode(200) + @Get('update/:articleId') + async fetchArticle( + @Req() req: Request, + @Param('articleId') articleId: number, + ): Promise { + const userId = req.user.userId; + return await this.svc_articlesRead.readArticleUpdateDetail({ + articleId, + userId, + }); + } + + @ApiOperation({ + summary: '[cursor]전체 게시글 조회 API', + description: + '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다. PUBLIC 게시글만 조회한다.', + }) + @Get('cursor') + @ApiOkResponse({ type: ArticlesGetResponseDto }) + async fetchCursor( + @Query() cursorOption: ArticlesGetRequestDto, + ): Promise> { + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '0', + ); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '9', + ); + } + return this.svc_articlesPaginate.fetchArticlesCursor({ cursorOption }); + } + + @ApiOperation({ + summary: '[cursor]친구 게시글 조회 API', + description: + '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @Get('cursor/friends') + @ApiOkResponse({ type: ArticlesGetResponseDto }) + async fetchFriendsCursor( + @Query() cursorOption: ArticlesGetRequestDto, + @Req() req: Request, + ): Promise> { + const userId = req.user.userId; + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '0', + ); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '9', + ); + } + return this.svc_articlesPaginate.fetchFriendsArticlesCursor({ + cursorOption, + userId, + }); + } + + @ApiOperation({ + summary: '[cursor]특정 유저의 게시글 조회', + description: + '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', + }) + @Get('/cursor/user/:userId') + @ApiOkResponse({ type: ArticlesGetRequestDto }) + async fetchUserArticles( + @Param('userId') targetUserId: number, + @Req() req: Request, + @Query() cursorOption: ArticlesGetRequestDto, + ): Promise> { + const userId = req.user.userId; + if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '0', + ); + } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { + cursorOption.cursor = this.svc_articlesPaginate.createDefaultCursor( + 7, + 7, + '9', + ); + } + return await this.svc_articlesPaginate.fetchUserArticlesCursor({ + userId, + targetUserId, + cursorOption, + }); + } +} diff --git a/src/APIs/articles/controllers/articles-update.controller.ts b/src/APIs/articles/controllers/articles-update.controller.ts new file mode 100644 index 0000000..64503a3 --- /dev/null +++ b/src/APIs/articles/controllers/articles-update.controller.ts @@ -0,0 +1,45 @@ +import { + Body, + Controller, + HttpCode, + Param, + Patch, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { ArticlesUpdateService } from '../services/articles-update.service'; +import { ArticleDto } from '../dtos/common/article.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { ArticlePatchRequestDto } from '../dtos/request/article-patch-request.dto'; +import { Request } from 'express'; + +@ApiTags('게시글 API') +@Controller('articles') +export class ArticlesUpdateController { + constructor(private readonly svc_articlesUpdate: ArticlesUpdateService) {} + + @ApiOperation({ summary: '게시글 patch' }) + @ApiCookieAuth() + @ApiOkResponse({ type: ArticleDto }) + @UseGuards(AuthGuardV2) + @Patch(':articleId') + @HttpCode(200) + async patchArticle( + @Req() req: Request, + @Body() body: ArticlePatchRequestDto, + @Param('articleId') articleId: number, + ): Promise { + const userId = req.user.userId; + return await this.svc_articlesUpdate.patchArticle({ + ...body, + articleId, + userId, + }); + } +} diff --git a/src/APIs/articles/controllers/articles.controller.ts b/src/APIs/articles/controllers/articles.controller.ts deleted file mode 100644 index 3830169..0000000 --- a/src/APIs/articles/controllers/articles.controller.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - HttpCode, - Param, - Patch, - Article, - Query, - Req, - UploadedFile, - UseGuards, - UseInterceptors, -} from '@nestjs/common'; -import { - ApiBody, - ApiConsumes, - ApiCookieAuth, - ApiCreatedResponse, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { Request } from 'express'; -import { ArticlesService } from '../articles.service'; -import { FetchArticlesDto } from '../dtos/fetch-posts.dto'; -import { PublishArticleDto } from '../dtos/publish-post.dto'; -import { PageArticleResponseDto } from '../dtos/page-post-response.dto'; -import { CreateArticleInput } from '../dtos/create-post.input'; -import { PublishArticleInput } from '../dtos/publish-post.input'; -import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { FetchUserArticlesInput } from '../dtos/fetch-user-posts.input'; -import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { - ArticleOnlyResponseDto, - ArticleResponseDto, -} from '../dtos/post-response.dto'; -import { - FetchArticleForUpdateDto, - ArticleResponseDtoExceptCategory, -} from '../dtos/fetch-post-for-update.dto'; -import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; -import { SortOption } from 'src/common/enums/sort-option'; -import { CursorFetchArticles } from '../dtos/cursor-fetch-posts.dto'; -import { CursorPageArticleResponseDto } from '../dtos/cursor-page-post-response.dto'; -import { PatchArticleInput } from '../dtos/patch-post.dto'; -import { DeleteArticleInput } from '../dtos/delete-post.dto'; - -@ApiTags('게시글 API') -@Controller('posts') -export class ArticlesController { - constructor(private readonly postsService: ArticlesService) {} - - @ApiOperation({ - summary: '게시글 등록', - description: '게시글을 등록한다.', - }) - @Article() - @ApiCookieAuth() - @ApiCreatedResponse({ description: '등록 성공', type: PublishArticleDto }) - @UseGuards(AuthGuardV2) - @HttpCode(201) - async publishArticle(@Req() req: Request, @Body() body: PublishArticleInput) { - const kakaoId = req.user.userId; - console.log(body); - const dto = { ...body, userKakaoId: kakaoId, isPublished: true }; - return await this.postsService.save(dto); - } - - @ApiOperation({ - summary: '게시글 삭제', - description: - '로그인 된 유저의 postId에 해당하는 게시글을 삭제한다. isHardDelete(nullable)을 통해 삭제 방식 결정', - }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) - @Delete(':postId') - async softDelete( - @Req() req: Request, - @Param('postId') id: number, - @Body() body: DeleteArticleInput, - ) { - const kakaoId = req.user.userId; - if (body.isHardDelete === true) { - return await this.postsService.hardDelete({ kakaoId, id }); - } - return await this.postsService.softDelete({ kakaoId, id }); - } - - @ApiOperation({ - summary: '게시글 임시등록', - description: '게시글을 임시등록한다.', - }) - @Article('temp') - @ApiCookieAuth() - @ApiCreatedResponse({ description: '임시등록 성공', type: PublishArticleDto }) - @UseGuards(AuthGuardV2) - @HttpCode(201) - async updateArticle(@Req() req: Request, @Body() body: CreateArticleInput) { - const kakaoId = req.user.userId; - const dto = { ...body, userKakaoId: kakaoId, isPublished: false }; - return await this.postsService.save(dto); - } - - @ApiOperation({ summary: '게시글 patch' }) - @ApiCookieAuth() - @ApiOkResponse({ type: ArticleOnlyResponseDto }) - @UseGuards(AuthGuardV2) - @Patch(':postId') - @HttpCode(200) - async patchArticle( - @Req() req: Request, - @Body() body: PatchArticleInput, - @Param('postId') id: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.patchArticle({ ...body, id, kakaoId }); - } - - @ApiOperation({ - summary: '임시작성 게시글 조회', - description: '로그인된 유저의 임시작성 게시글을 조회한다.', - }) - @ApiCookieAuth() - @ApiOkResponse({ type: [ArticleResponseDtoExceptCategory] }) - @UseGuards(AuthGuardV2) - @Get('temp') - async fetchTempArticles( - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - console.log(kakaoId); - return await this.postsService.fetchTempArticles({ kakaoId }); - } - - @ApiOperation({ - summary: '이미지 업로드', - description: - '이미지를 서버에 업로드한다. url을 반환 받는다. 게시글 내부 이미지 업로드 및 캡처 이미지 업로드용. max_width=1280px', - }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: '업로드 할 파일', - type: ImageUploadDto, - }) - @ApiCreatedResponse({ - description: '이미지 서버에 파일 업로드 완료', - type: ImageUploadResponseDto, - }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @Article('image') - @UseInterceptors(FileInterceptor('file')) - @HttpCode(201) - async createPrivateSticker( - @Req() req: Request, - @UploadedFile() file: Express.Multer.File, - ): Promise { - return await this.postsService.imageUpload(file); - } - - // @ApiOperation({ - // summary: '게시글 hard delete', - // description: - // '로그인 된 유저의 {id}에 해당하는 게시글을 물리삭제한다. 임시 저장된 게시글에 사용을 권장', - // }) - // @ApiCookieAuth() - // @UseGuards(AuthGuardV2) - // @Delete('hard/:id') - // async hardDelete(@Req() req: Request, @Param('id') id: number) { - // const kakaoId = req.user.userId; - // return await this.postsService.hardDelete({ kakaoId, id }); - // } - - @ApiOperation({ - summary: '게시글 디테일 뷰 fetch', - description: - 'id에 해당하는 게시글을 가져온다. 조회수를 올린다. 보호된 게시글은 권한이 있는 사용자만 접근 가능하다.', - }) - @Get('detail/:postId') - @ApiOkResponse({ type: ArticleResponseDto }) - async fetchArticleDetail( - @Param('postId') id: number, - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchDetail({ kakaoId, id }); - } - - @ApiOperation({ - summary: '[수정용] 게시글 및 스티커 상세 데이터 fetch', - description: - '본인 게시글 수정용으로 id에 해당하는 게시글에 조인된 스티커 블록들의 값과 게시글 세부 데이터를 모두 가져온다.', - }) - @ApiCookieAuth() - @ApiOkResponse({ type: FetchArticleForUpdateDto }) - @UseGuards(AuthGuardV2) - @HttpCode(200) - @Get('update/:postId') - async fetchArticle( - @Req() req: Request, - @Param('postId') id: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.postsService.fetchArticleForUpdate({ id, kakaoId }); - } - - @ApiOperation({ - summary: '[cursor]전체 게시글 조회 API', - description: - '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다. PUBLIC 게시글만 조회한다.', - }) - @Get('cursor') - @ApiOkResponse({ type: CursorPageArticleResponseDto }) - async fetchCursor( - @Query() cursorOption: CursorFetchArticles, - ): Promise> { - if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); - } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); - } - return this.postsService.fetchArticlesCursor({ cursorOption }); - } - - @ApiOperation({ - summary: '[cursor]친구 게시글 조회 API', - description: - '커서 기반으로 게시글을 조회한다. 최초 조회 시 커서 값을 비워서 요청한다. 쿼리 옵션을 변경할 경우 기존의 커서 값을 쓸 수 없다.', - }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) - @Get('cursor/friends') - @ApiOkResponse({ type: CursorPageArticleResponseDto }) - async fetchFriendsCursor( - @Query() cursorOption: CursorFetchArticles, - @Req() req: Request, - ): Promise> { - const kakaoId = req.user.userId; - if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); - } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); - } - return this.postsService.fetchFriendsArticlesCursor({ - cursorOption, - kakaoId, - }); - } - - @ApiOperation({ - summary: '[cursor]특정 유저의 게시글 조회', - description: - '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', - }) - @Get('/cursor/user/:userId') - @ApiOkResponse({ type: CursorPageArticleResponseDto }) - async fetchUserArticles( - @Param('userId') targetKakaoId: number, - @Req() req: Request, - @Query() cursorOption: FetchUserArticlesInput, - ): Promise { - const kakaoId = req.user.userId; - if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '0'); - } else if (!cursorOption.cursor && cursorOption.sort === SortOption.DESC) { - cursorOption.cursor = this.postsService.createDefaultCursor(7, 7, '9'); - } - return await this.postsService.fetchUserArticlesCursor({ - kakaoId, - targetKakaoId, - cursorOption, - }); - } -} diff --git a/src/APIs/articles/controllers/create-articles.controller.ts b/src/APIs/articles/controllers/create-articles.controller.ts deleted file mode 100644 index b99fd9e..0000000 --- a/src/APIs/articles/controllers/create-articles.controller.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; - -@ApiTags('게시글 API') -@Controller('articles') -export class createArticlesController { - constructor(private readonly createArticlesService: createArticlesService) {} -} diff --git a/src/APIs/articles/controllers/delete-articles.controller.ts b/src/APIs/articles/controllers/delete-articles.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/articles/controllers/read-articles.controller.ts b/src/APIs/articles/controllers/read-articles.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/articles/controllers/update-articles.controller.ts b/src/APIs/articles/controllers/update-articles.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/articles/dtos/article.dto.ts b/src/APIs/articles/dtos/common/article.dto.ts similarity index 79% rename from src/APIs/articles/dtos/article.dto.ts rename to src/APIs/articles/dtos/common/article.dto.ts index d6fa530..458a208 100644 --- a/src/APIs/articles/dtos/article.dto.ts +++ b/src/APIs/articles/dtos/common/article.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { Article } from '../entities/article.entity'; +import { Article } from '../../entities/article.entity'; export class ArticleDto extends OmitType(Article, [ 'comments', diff --git a/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts b/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts new file mode 100644 index 0000000..e09df10 --- /dev/null +++ b/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts @@ -0,0 +1,9 @@ +import { PartialType } from '@nestjs/swagger'; +import { ArticleCreateRequestDto } from './article-create-request.dto'; +import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto'; + +export class ArticleCreateDraftRequestDto extends PartialType( + ArticleCreateRequestDto, +) { + stickerBlocks: StickerBlocksCreateDto[]; +} diff --git a/src/APIs/articles/dtos/request/article-create-request.dto.ts b/src/APIs/articles/dtos/request/article-create-request.dto.ts new file mode 100644 index 0000000..d19959c --- /dev/null +++ b/src/APIs/articles/dtos/request/article-create-request.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; +import { ArticleDto } from '../common/article.dto'; +import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto'; + +export class ArticleCreateRequestDto extends OmitType(ArticleDto, [ + 'id', + 'commentCount', + 'viewCount', + 'reportCount', + 'likeCount', + 'userId', + 'isPublished', + 'dateCreated', + 'dateDeleted', + 'dateUpdated', +]) { + @ApiProperty({ type: [StickerBlocksCreateDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => StickerBlocksCreateDto) + stickerBlocks: StickerBlocksCreateDto[]; +} diff --git a/src/APIs/articles/olds/delete-post.dto.ts b/src/APIs/articles/dtos/request/article-delete-request.dto.ts similarity index 87% rename from src/APIs/articles/olds/delete-post.dto.ts rename to src/APIs/articles/dtos/request/article-delete-request.dto.ts index d47109f..ac5797d 100644 --- a/src/APIs/articles/olds/delete-post.dto.ts +++ b/src/APIs/articles/dtos/request/article-delete-request.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsOptional } from 'class-validator'; -export class DeletePostInput { +export class ArticleDeleteRequestDto { @ApiProperty({ description: '물리 삭제 여부(nullable)', required: false, diff --git a/src/APIs/articles/dtos/request/article-patch-request.dto.ts b/src/APIs/articles/dtos/request/article-patch-request.dto.ts new file mode 100644 index 0000000..7c48058 --- /dev/null +++ b/src/APIs/articles/dtos/request/article-patch-request.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { ArticleCreateRequestDto } from './article-create-request.dto'; + +export class ArticlePatchRequestDto extends PartialType( + ArticleCreateRequestDto, +) {} diff --git a/src/APIs/articles/olds/cursor-fetch-posts.dto.ts b/src/APIs/articles/dtos/request/articles-get-request.dto.ts similarity index 53% rename from src/APIs/articles/olds/cursor-fetch-posts.dto.ts rename to src/APIs/articles/dtos/request/articles-get-request.dto.ts index 3ad3547..ef784a6 100644 --- a/src/APIs/articles/olds/cursor-fetch-posts.dto.ts +++ b/src/APIs/articles/dtos/request/articles-get-request.dto.ts @@ -1,18 +1,18 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; +import { ArticleOrderOptionWrap } from 'src/common/enums/article-order-option'; import { DateOption } from 'src/common/enums/date-option'; -import { PostsOrderOptionWrap } from 'src/common/enums/article-order-option'; -import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; +import { CustomCursorPageOptionsDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-option.dto'; -export class CursorFetchPosts extends CustomCursorPageOptionsDto { +export class ArticlesGetRequestDto extends CustomCursorPageOptionsDto { @ApiProperty({ description: '정렬 옵션', type: 'enum', - enum: PostsOrderOptionWrap, + enum: ArticleOrderOptionWrap, required: false, - default: PostsOrderOptionWrap.DATE, + default: ArticleOrderOptionWrap.DATE, }) - order?: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; + order?: ArticleOrderOptionWrap = ArticleOrderOptionWrap.DATE; @ApiProperty({ type: 'enun', @@ -23,5 +23,5 @@ export class CursorFetchPosts extends CustomCursorPageOptionsDto { }) @IsEnum(DateOption) @IsOptional() - date_created?: DateOption; + dateCreated?: DateOption; } diff --git a/src/APIs/articles/olds/fetch-user-posts.input.ts b/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts similarity index 69% rename from src/APIs/articles/olds/fetch-user-posts.input.ts rename to src/APIs/articles/dtos/request/articles-get-user-request.dto.ts index c424316..d2aabf3 100644 --- a/src/APIs/articles/olds/fetch-user-posts.input.ts +++ b/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts @@ -1,9 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsOptional } from 'class-validator'; -import { CursorFetchPosts } from './cursor-fetch-posts.dto'; +import { ArticlesGetRequestDto } from './articles-get-request.dto'; -export class FetchUserPostsInput extends CursorFetchPosts { +export class ArticlesGetUserRequestDto extends ArticlesGetRequestDto { @ApiProperty({ description: '필터링할 카테고리 아이디', type: String, diff --git a/src/APIs/articles/dtos/response/article-create-response.dto.ts b/src/APIs/articles/dtos/response/article-create-response.dto.ts new file mode 100644 index 0000000..e940536 --- /dev/null +++ b/src/APIs/articles/dtos/response/article-create-response.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArticleDto } from '../common/article.dto'; +import { StickerBlockDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlock.dto'; + +export class ArticleCreateResponseDto { + @ApiProperty({ type: ArticleDto }) + articleData: ArticleDto; + + @ApiProperty({ type: [StickerBlockDto] }) + stickerBlockData: StickerBlockDto[]; +} diff --git a/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts new file mode 100644 index 0000000..58fb9b7 --- /dev/null +++ b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts @@ -0,0 +1,4 @@ +export class ArticleDetailForUpdateResponseDto { + article; + stickerBlocks; +} diff --git a/src/APIs/articles/dtos/article-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-response.dto.ts similarity index 73% rename from src/APIs/articles/dtos/article-response.dto.ts rename to src/APIs/articles/dtos/response/article-detail-response.dto.ts index cbd232a..7450c60 100644 --- a/src/APIs/articles/dtos/article-response.dto.ts +++ b/src/APIs/articles/dtos/response/article-detail-response.dto.ts @@ -1,9 +1,9 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Article } from '../entities/article.entity'; +import { Article } from '../../entities/article.entity'; -export class ArticleDetailResponse extends OmitType(Article, [ +export class ArticleDetailResponseDto extends OmitType(Article, [ 'comments', 'user', 'stickerBlocks', diff --git a/src/APIs/articles/dtos/response/articles-get-response.dto.ts b/src/APIs/articles/dtos/response/articles-get-response.dto.ts new file mode 100644 index 0000000..a79bf3f --- /dev/null +++ b/src/APIs/articles/dtos/response/articles-get-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CustomCursorPageMetaDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { ArticleDto } from '../common/article.dto'; + +export class ArticlesGetResponseDto { + @ApiProperty({ description: '조회된 데이터', type: [ArticleDto] }) + readonly data: ArticleDto[]; + + @ApiProperty({ + description: '페이지네이션 메타 데이터', + type: CustomCursorPageMetaDto, + }) + readonly meta: CustomCursorPageMetaDto; +} diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 31694ca..597998d 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -113,13 +113,17 @@ export class Article extends IndexedCommonEntity { @IsEnum(OpenScope) scope: OpenScope; - @ApiProperty({ description: '게시글 내용', type: String }) - @Column('longtext') + @ApiProperty({ description: '게시글 내용', type: String, default: '' }) + @Column('longtext', { default: '' }) @IsString() content: string; - @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) - @Column({ name: 'main_description' }) + @ApiProperty({ + description: '게시글 설명(html 태그 제외)', + type: String, + default: '', + }) + @Column({ name: 'main_description', default: '' }) @IsString() mainDescription: string; diff --git a/src/APIs/articles/interfaces/articles.repository.interface.ts b/src/APIs/articles/interfaces/articles.repository.interface.ts index 82ccf9f..1c460bd 100644 --- a/src/APIs/articles/interfaces/articles.repository.interface.ts +++ b/src/APIs/articles/interfaces/articles.repository.interface.ts @@ -1,4 +1,3 @@ -import { ArticlesOrderOptionWrap } from 'src/common/enums/articles-order-option'; import { SortOption } from 'src/common/enums/sort-option'; import { IArticlesServiceFetchArticlesCursor, @@ -6,9 +5,10 @@ import { IArticlesServiceFetchUserArticlesCursor, } from './articles.service.interface'; import { OpenScope } from 'src/common/enums/open-scope.enum'; +import { ArticleOrderOptionWrap } from 'src/common/enums/article-order-option'; export interface IArticlesRepoGetCursorQuery { - order: ArticlesOrderOptionWrap; + order: ArticleOrderOptionWrap; sort: SortOption; take: number; cursor: string; @@ -16,18 +16,18 @@ export interface IArticlesRepoGetCursorQuery { export interface IArticlesRepoFetchArticlesCursor extends IArticlesServiceFetchArticlesCursor { - date_filter: Date; + dateFilter: Date; } export interface IArticlesRepoFetchFriendsArticlesCursor extends Pick { - date_filter: Date; + dateFilter: Date; userId: number; } export interface IArticlesRepoFetchUserArticlesCursor extends Pick { - date_filter: Date; + dateFilter: Date; scope: OpenScope[]; userId: number; } diff --git a/src/APIs/articles/interfaces/articles.service.interface.ts b/src/APIs/articles/interfaces/articles.service.interface.ts index 2c0339d..8e4676d 100644 --- a/src/APIs/articles/interfaces/articles.service.interface.ts +++ b/src/APIs/articles/interfaces/articles.service.interface.ts @@ -1,44 +1,44 @@ +import { ArticleCreateRequestDto } from '../dtos/request/article-create-request.dto'; +import { ArticlePatchRequestDto } from '../dtos/request/article-patch-request.dto'; +import { ArticlesGetRequestDto } from '../dtos/request/articles-get-request.dto'; +import { ArticlesGetUserRequestDto } from '../dtos/request/articles-get-user-request.dto'; import { Article } from '../entities/article.entity'; -export interface IArticlesServiceArticleId extends Pick {} +export interface IArticlesServiceArticleId { + articleId: number; +} export interface IArticlesServiceArticleUserIdPair { - id: number; - kakaoId: number; + articleId: number; + userId: number; } -export interface IArticlesServiceCreate extends CreateArticleInput { - userKakaoId: number; - +export interface IArticlesServiceCreate extends ArticleCreateRequestDto { + userId: number; isPublished: boolean; } -export interface IArticlesServiceFetchArticleForUpdate { - id: number; - kakaoId: number; -} - export interface IArticlesServiceCreateCursorResponse { - cursorOption: CursorFetchArticles; + cursorOption: ArticlesGetRequestDto; articles: Article[]; } export interface IArticlesServiceFetchArticlesCursor { - cursorOption: CursorFetchArticles; + cursorOption: ArticlesGetRequestDto; } export interface IArticlesServiceFetchFriendsArticlesCursor { - cursorOption: CursorFetchArticles; - kakaoId: number; + cursorOption: ArticlesGetRequestDto; + userId: number; } export interface IArticlesServiceFetchUserArticlesCursor { - cursorOption: FetchUserArticlesInput; - targetKakaoId: number; - kakaoId: number; + cursorOption: ArticlesGetUserRequestDto; + targetUserId: number; + userId: number; } -export interface IArticlesServicePatchArticle extends PatchArticleInput { - kakaoId: number; - id: number; +export interface IArticlesServicePatchArticle extends ArticlePatchRequestDto { + userId: number; + articleId: number; } diff --git a/src/APIs/articles/olds/create-post.input.ts b/src/APIs/articles/olds/create-post.input.ts deleted file mode 100644 index 4ab23ca..0000000 --- a/src/APIs/articles/olds/create-post.input.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsEnum, - IsOptional, - IsString, - ValidateNested, -} from 'class-validator'; -import { BulkInsertStickerInput } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; -import { OpenScope } from 'src/common/enums/open-scope.enum'; - -export class CreatePostInput { - @ApiProperty({ type: [BulkInsertStickerInput] }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => BulkInsertStickerInput) - stickerBlocks: BulkInsertStickerInput[]; - - @ApiProperty({ - description: '연결된 카테고리 fk', - type: String, - required: false, - }) - @IsString() - @IsOptional() - postCategoryId?: string; - - @ApiProperty({ description: '연결된 내지 fk', type: String, required: false }) - @IsString() - @IsOptional() - postBackgroundId?: string; - - @ApiProperty({ - description: '제목(최대 100자)', - type: String, - }) - @IsString() - title: string; - - @ApiProperty({ description: '수정용 제목', type: String }) - @IsString() - title_html: string; - - @ApiProperty({ - description: '댓글 허용 여부(boolean)', - type: Boolean, - required: false, - }) - @IsBoolean() - @IsOptional() - allow_comment?: boolean; - - @ApiProperty({ - description: - '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', - type: 'enum', - enum: OpenScope, - required: false, - }) - @IsEnum(OpenScope) - @IsOptional() - scope?: OpenScope; - - @ApiProperty({ description: '게시글 내용', type: String }) - @IsString() - content: string; - - @ApiProperty({ - description: '게시글 캡쳐 이미지 url', - type: String, - required: false, - }) - @IsString() - @IsOptional() - image_url?: string; - - @ApiProperty({ - description: '게시글 대표 이미지 url', - type: String, - required: false, - }) - @IsString() - @IsOptional() - main_image_url?: string; -} diff --git a/src/APIs/articles/olds/cursor-page-post-response.dto.ts b/src/APIs/articles/olds/cursor-page-post-response.dto.ts deleted file mode 100644 index 913b515..0000000 --- a/src/APIs/articles/olds/cursor-page-post-response.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; -import { PostResponseDto } from './post-response.dto'; - -export class CursorPagePostResponseDto { - @ApiProperty({ description: '조회된 데이터', type: [PostResponseDto] }) - readonly data: PostResponseDto[]; - - @ApiProperty({ - description: '페이지네이션 메타 데이터', - type: CustomCursorPageMetaDto, - }) - readonly meta: CustomCursorPageMetaDto; -} diff --git a/src/APIs/articles/olds/fetch-friends-posts.dto.ts b/src/APIs/articles/olds/fetch-friends-posts.dto.ts deleted file mode 100644 index ac04ba9..0000000 --- a/src/APIs/articles/olds/fetch-friends-posts.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FetchPostsDto } from './fetch-posts.dto'; - -export class FetchFriendsPostsDto { - kakaoId: string; - page: FetchPostsDto; -} diff --git a/src/APIs/articles/olds/fetch-post-detail.dto.ts b/src/APIs/articles/olds/fetch-post-detail.dto.ts deleted file mode 100644 index dfa6234..0000000 --- a/src/APIs/articles/olds/fetch-post-detail.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { PostResponseDto } from './post-response.dto'; -import { FetchCommentsDto } from 'src/APIs/comments/dtos/fetch-comments.dto'; - -export class fetchPostDetailDto { - @ApiProperty({ type: PostResponseDto, description: '게시글 정보' }) - post: PostResponseDto; - - @ApiProperty({ type: [FetchCommentsDto], description: '댓글 정보' }) - comments: FetchCommentsDto[]; -} diff --git a/src/APIs/articles/olds/fetch-post-for-update.dto.ts b/src/APIs/articles/olds/fetch-post-for-update.dto.ts deleted file mode 100644 index 59b4323..0000000 --- a/src/APIs/articles/olds/fetch-post-for-update.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { StickerBlock } from 'src/APIs/stickerBlocks/entities/stickerblock.entity'; -import { PostResponseDto } from './post-response.dto'; -import { ApiProperty, OmitType } from '@nestjs/swagger'; - -export class PostResponseDtoExceptCategory extends OmitType(PostResponseDto, [ - 'postCategory', -]) {} -export class FetchPostForUpdateDto { - @ApiProperty({ - description: '게시글 정보', - type: PostResponseDtoExceptCategory, - }) - post: PostResponseDtoExceptCategory; - - @ApiProperty({ description: '스티커 블록 배열', type: [StickerBlock] }) - stickerBlocks: StickerBlock[]; -} diff --git a/src/APIs/articles/olds/fetch-posts.dto.ts b/src/APIs/articles/olds/fetch-posts.dto.ts deleted file mode 100644 index 69143cd..0000000 --- a/src/APIs/articles/olds/fetch-posts.dto.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { IsEnum, IsOptional, IsString } from 'class-validator'; -import { PageRequest } from '../../../utils/pages/page-request'; -import { ApiProperty } from '@nestjs/swagger'; -import { PostsOrderOptionWrap } from 'src/common/enums/article-order-option'; -import { PostsFilterOptionWrap } from 'src/common/enums/posts-filter-option'; - -export class FetchPostsDto extends PageRequest { - @ApiProperty({ - description: '페이지 정렬 옵션(default = TIME)', - type: 'enum', - enum: PostsOrderOptionWrap, - required: false, - }) - @IsOptional() - @IsEnum(PostsOrderOptionWrap) - order: PostsOrderOptionWrap = PostsOrderOptionWrap.DATE; - - @ApiProperty({ - description: '페이지 검색 옵션(default = TITLE)', - type: 'enum', - enum: PostsFilterOptionWrap, - required: false, - }) - @IsOptional() - @IsEnum(PostsFilterOptionWrap) - filter: PostsFilterOptionWrap = PostsFilterOptionWrap.TITLE; - - @ApiProperty({ - description: '검색할 내용', - type: String, - required: false, - }) - @IsOptional() - @IsString() - search: string = '%'; -} - -export const FETCH_POST_OPTION = { - id: true, - postBackground: { id: true, image_url: true }, - postCategory: { id: true, name: true }, - user: { - kakaoId: true, - isAdmin: true, - username: true, - description: true, - profile_image: true, - date_created: true, - date_deleted: true, - }, - title: true, - title_html: true, - content: true, - main_description: true, - image_url: true, - main_image_url: true, - isPublished: true, - like_count: true, - report_count: true, - allow_comment: true, - scope: true, - date_created: true, - date_updated: true, -}; diff --git a/src/APIs/articles/olds/fetch-user-posts.dto.ts b/src/APIs/articles/olds/fetch-user-posts.dto.ts deleted file mode 100644 index 0312ccb..0000000 --- a/src/APIs/articles/olds/fetch-user-posts.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FetchUserPostsInput } from './fetch-user-posts.input'; - -export class FetchUserPostsDto extends FetchUserPostsInput { - kakaoId: number; - - targetKakaoId: number; -} diff --git a/src/APIs/articles/olds/page-post-response.dto.ts b/src/APIs/articles/olds/page-post-response.dto.ts deleted file mode 100644 index 36d67d3..0000000 --- a/src/APIs/articles/olds/page-post-response.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { PostResponseDto } from './post-response.dto'; - -export class PagePostResponseDto { - @ApiProperty({ description: '한 페이지 당 아이템 갯수', type: Number }) - pageSize: number; - - @ApiProperty({ description: '전체 아이템 갯수', type: Number }) - totalCount: number; - - @ApiProperty({ description: '요청할 페이지 번호', type: Number }) - totalPage: number; - - @ApiProperty({ description: '조회된 포스트', type: [PostResponseDto] }) - items: PostResponseDto[]; -} diff --git a/src/APIs/articles/olds/patch-post.dto.ts b/src/APIs/articles/olds/patch-post.dto.ts deleted file mode 100644 index 0c5ecba..0000000 --- a/src/APIs/articles/olds/patch-post.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreatePostInput } from './create-post.input'; - -export class PatchPostInput extends PartialType(CreatePostInput) {} diff --git a/src/APIs/articles/olds/post-response.dto.ts b/src/APIs/articles/olds/post-response.dto.ts deleted file mode 100644 index e4d8760..0000000 --- a/src/APIs/articles/olds/post-response.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; - -import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Posts } from '../entities/article.entity'; - -export class PostResponseDto extends OmitType(Posts, ['user']) { - @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) - user: UserPrimaryResponseDto; -} - -export class PostOnlyResponseDto extends OmitType(Posts, [ - 'user', - 'postBackground', - 'postCategory', -]) {} diff --git a/src/APIs/articles/olds/publish-post.dto.ts b/src/APIs/articles/olds/publish-post.dto.ts deleted file mode 100644 index cbbb323..0000000 --- a/src/APIs/articles/olds/publish-post.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Posts } from '../entities/article.entity'; -import { CreateStickerBlocksResponseDto } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; - -export class PublishPostResult extends OmitType(Posts, [ - 'postBackground', - 'user', - 'postCategory', -]) {} - -export class PublishPostDto { - @ApiProperty({ type: [PublishPostResult] }) - postData: PublishPostResult; - - @ApiProperty({ type: [CreateStickerBlocksResponseDto] }) - stickerBlockData: CreateStickerBlocksResponseDto[]; -} diff --git a/src/APIs/articles/olds/publish-post.input.ts b/src/APIs/articles/olds/publish-post.input.ts deleted file mode 100644 index e077376..0000000 --- a/src/APIs/articles/olds/publish-post.input.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - IsArray, - IsEnum, - IsOptional, - IsString, - ValidateNested, -} from 'class-validator'; -import { BulkInsertStickerInput } from 'src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto'; -import { OpenScope } from 'src/common/enums/open-scope.enum'; -import { IsBoolean } from 'src/common/validators/isBoolean'; - -export class PublishPostInput { - @ApiProperty({ type: [BulkInsertStickerInput] }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => BulkInsertStickerInput) - stickerBlocks: BulkInsertStickerInput[]; - - @ApiProperty({ - description: '연결된 카테고리 fk', - type: String, - }) - @IsString() - postCategoryId: string; - - @ApiProperty({ - description: '연결된 내지 fk', - type: String, - nullable: true, - required: false, - }) - @IsString() - @IsOptional() - postBackgroundId?: string; - - @ApiProperty({ - description: '제목(최대 100자)', - type: String, - }) - @IsString() - title: string; - - @ApiProperty({ description: '수정용 제목', type: String }) - @IsString() - title_html: string; - - @ApiProperty({ - description: '댓글 허용 여부(boolean)', - type: Boolean, - }) - @IsBoolean() - allow_comment: boolean; - - @ApiProperty({ - description: - '[공개 설정] PUBLIC: 전체공개, PROTECTED: 친구공개, PRIVATE: 비공개', - type: 'enum', - enum: OpenScope, - }) - @IsEnum(OpenScope) - scope: OpenScope; - - @ApiProperty({ description: '게시글 내용', type: String }) - @IsString() - content: string; - - @ApiProperty({ description: '게시글 설명(html 태그 제외)', type: String }) - main_description: string; - - @ApiProperty({ description: '게시글 캡쳐 이미지 url', type: String }) - @IsString() - image_url: string; - - @ApiProperty({ description: '게시글 대표 이미지 url', type: String }) - @IsString() - main_image_url: string; -} diff --git a/src/APIs/articles/repositories/create-articles.repository.ts b/src/APIs/articles/repositories/articles-create.repository.ts similarity index 87% rename from src/APIs/articles/repositories/create-articles.repository.ts rename to src/APIs/articles/repositories/articles-create.repository.ts index 9972e7e..ca7ea71 100644 --- a/src/APIs/articles/repositories/create-articles.repository.ts +++ b/src/APIs/articles/repositories/articles-create.repository.ts @@ -3,7 +3,7 @@ import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; @Injectable() -export class CreateArticlesRepository extends Repository
{ +export class ArticlesCreateRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } diff --git a/src/APIs/articles/repositories/delete-articles.repository.ts b/src/APIs/articles/repositories/articles-delete.repository.ts similarity index 81% rename from src/APIs/articles/repositories/delete-articles.repository.ts rename to src/APIs/articles/repositories/articles-delete.repository.ts index c30c6d1..14dbe45 100644 --- a/src/APIs/articles/repositories/delete-articles.repository.ts +++ b/src/APIs/articles/repositories/articles-delete.repository.ts @@ -3,7 +3,7 @@ import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; @Injectable() -export class DeleteArticlesRepository extends Repository
{ +export class ArticlesDeleteRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } diff --git a/src/APIs/articles/repositories/paginate-articles.repository.ts.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts.ts similarity index 98% rename from src/APIs/articles/repositories/paginate-articles.repository.ts.ts rename to src/APIs/articles/repositories/articles-paginate.repository.ts.ts index 6958420..f4b3957 100644 --- a/src/APIs/articles/repositories/paginate-articles.repository.ts.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts.ts @@ -11,7 +11,7 @@ import { } from '../interfaces/articles.repository.interface'; import { Follow } from 'src/APIs/follows/entities/follow.entity'; -export class PaginateArticlesRepository extends Repository
{ +export class ArticlesPaginateRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } diff --git a/src/APIs/articles/repositories/read-articles.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts similarity index 79% rename from src/APIs/articles/repositories/read-articles.repository.ts rename to src/APIs/articles/repositories/articles-read.repository.ts index 1b57708..26a3c66 100644 --- a/src/APIs/articles/repositories/read-articles.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -1,15 +1,15 @@ import { DataSource, Repository } from 'typeorm'; import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; -import { ArticleDetailResponse } from '../dtos/article-response.dto'; +import { ArticleDetailResponse } from '../dtos/response/article-detail-response.dto'; @Injectable() -export class ReadArticlesRepository extends Repository
{ +export class ArticlesReadRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } - async readDetail({ id, scope }): Promise { - await this.update(id, { + async readDetail({ articleId, scope }): Promise { + await this.update(articleId, { viewCount: () => 'view_count +1', }); return await this.createQueryBuilder('p') @@ -23,13 +23,13 @@ export class ReadArticlesRepository extends Repository
{ 'user.profile_image', 'user.username', ]) - .where('p.id = :id', { id }) + .where('p.id = :articleId', { articleId }) .andWhere('p.scope IN (:scope)', { scope }) .andWhere('p.date_deleted IS NULL') .getOne(); } - async readUpdateDetail(id) { + async readUpdateDetail({ articleId }) { return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') .leftJoinAndSelect('p.article_background', 'article_background') @@ -41,12 +41,12 @@ export class ReadArticlesRepository extends Repository
{ 'user.profile_image', 'user.username', ]) - .where('p.id = :id', { id }) + .where('p.id = :articleId', { articleId }) .andWhere('p.date_deleted IS NULL') .getOne(); } - async fetchTempArticles(userId: number): Promise { + async readTemp({ userId }): Promise { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') .leftJoinAndSelect('p.article_background', 'article_background') diff --git a/src/APIs/articles/repositories/update-articles.repository.ts b/src/APIs/articles/repositories/articles-update.repository.ts similarity index 81% rename from src/APIs/articles/repositories/update-articles.repository.ts rename to src/APIs/articles/repositories/articles-update.repository.ts index f9c1a12..5396f39 100644 --- a/src/APIs/articles/repositories/update-articles.repository.ts +++ b/src/APIs/articles/repositories/articles-update.repository.ts @@ -3,7 +3,7 @@ import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; @Injectable() -export class UpdateArticlesRepository extends Repository
{ +export class ArticlesUpdateRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts new file mode 100644 index 0000000..fb24da6 --- /dev/null +++ b/src/APIs/articles/services/articles-create.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@nestjs/common'; +import { IArticlesServiceCreate } from '../interfaces/articles.service.interface'; +import { DataSource } from 'typeorm'; +import { Article } from '../entities/article.entity'; +import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; +import { ArticlesValidateService } from './articles-validate.service'; +import { ArticlesReadRepository } from '../repositories/articles-read.repository'; +import { AwsService } from 'src/modules/aws/aws.service'; +import { UtilsService } from 'src/modules/utils/utils.service'; +import { ArticleCreateResponseDto } from '../dtos/response/article-create-response.dto'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; + +@Injectable() +export class ArticlesCreateService { + constructor( + private readonly db_dataSource: DataSource, + private readonly svc_articlesValidate: ArticlesValidateService, + private readonly svc_stickerBlocks: StickerBlocksService, + private readonly svc_aws: AwsService, + private readonly svc_utils: UtilsService, + private readonly repo_articlesRead: ArticlesReadRepository, + ) {} + + async save( + createArticleDto: IArticlesServiceCreate, + ): Promise { + const queryRunner = this.db_dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + const article = {}; + try { + Object.keys(createArticleDto).map((el) => { + const value = createArticleDto[el]; + if (createArticleDto[el] != null) { + article[el] = value; + } + }); + await this.svc_articlesValidate.fkValidCheck({ + articles: article, + passNonEssentail: !createArticleDto.isPublished, + }); + const queryResult = await queryRunner.manager + .createQueryBuilder() + .insert() + .into(Article, Object.keys(article)) + .values(article) + .execute(); + await queryRunner.commitTransaction(); + const articleData = await this.repo_articlesRead.findOne({ + where: { id: queryResult.identifiers[0].id }, + }); + const stickerBlockData = await this.svc_stickerBlocks.bulkInsert({ + articleId: articleData.id, + userId: createArticleDto.userId, + stickerBlocks: createArticleDto.stickerBlocks, + }); + return { articleData, stickerBlockData }; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } + + async imageUpload( + file: Express.Multer.File, + ): Promise { + const imageName = this.svc_utils.getUUID(); + const ext = file.originalname.split('.').pop(); + + const imageUrl = await this.svc_aws.imageUploadToS3( + `${imageName}.${ext}`, + file, + ext, + 1280, + ); + + return { imageUrl }; + } +} diff --git a/src/APIs/articles/services/articles-delete.service.ts b/src/APIs/articles/services/articles-delete.service.ts new file mode 100644 index 0000000..7b8bb9c --- /dev/null +++ b/src/APIs/articles/services/articles-delete.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { ArticlesValidateService } from './articles-validate.service'; +import { ArticlesDeleteRepository } from '../repositories/articles-delete.repository'; +import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; +import { AwsService } from 'src/modules/aws/aws.service'; +import { IArticlesServiceArticleUserIdPair } from '../interfaces/articles.service.interface'; +import { ArticlesReadRepository } from '../repositories/articles-read.repository'; + +@Injectable() +export class ArticlesDeleteService { + constructor( + // private readonly dataSource: DataSource, + private readonly svc_articlesValidate: ArticlesValidateService, + private readonly svc_stickerBlocks: StickerBlocksService, + private readonly svc_aws: AwsService, + private readonly repo_articlesRead: ArticlesReadRepository, + private readonly repo_articlesDelete: ArticlesDeleteRepository, + ) {} + + async softDelete({ userId, articleId }: IArticlesServiceArticleUserIdPair) { + const data = await this.repo_articlesRead.findOne({ + where: { user: { id: userId }, id: articleId }, + }); + if (data) { + await this.svc_aws.deleteImageFromS3({ url: data.imageUrl }); + await this.svc_aws.deleteImageFromS3({ url: data.mainImageUrl }); + await this.svc_stickerBlocks.deleteBlocks({ userId, articleId }); + } + return await this.repo_articlesDelete.softDelete({ + user: { id: userId }, + id: articleId, + }); + } + + async hardDelete({ userId, articleId }: IArticlesServiceArticleUserIdPair) { + const data = await this.repo_articlesRead.findOne({ + where: { user: { userId }, articleId }, + }); + if (data) { + await this.svc_aws.deleteImageFromS3({ url: data.imageUrl }); + await this.svc_aws.deleteImageFromS3({ url: data.mainImageUrl }); + await this.svc_stickerBlocks.deleteBlocks({ userId, articleId }); + } + return await this.repo_articlesDelete.delete({ + user: { id: userId }, + id: articleId, + }); + } +} diff --git a/src/APIs/articles/services/articles-paginate.service.ts b/src/APIs/articles/services/articles-paginate.service.ts new file mode 100644 index 0000000..36127ac --- /dev/null +++ b/src/APIs/articles/services/articles-paginate.service.ts @@ -0,0 +1,151 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ArticlesValidateService } from './articles-validate.service'; +import { ArticlesPaginateRepository } from '../repositories/articles-paginate.repository.ts'; +import { ArticleOrderOption } from 'src/common/enums/article-order-option'; +import { CustomCursorPageMetaDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { CustomCursorPageDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page.dto'; +import { FollowsService } from 'src/APIs/follows/follows.service'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { UtilsService } from 'src/modules/utils/utils.service'; +import { + IArticlesServiceCreateCursorResponse, + IArticlesServiceFetchArticlesCursor, + IArticlesServiceFetchFriendsArticlesCursor, + IArticlesServiceFetchUserArticlesCursor, +} from '../interfaces/articles.service.interface'; +import { ArticleDto } from '../dtos/common/article.dto'; + +@Injectable() +export class ArticlesPaginateService { + constructor( + // private readonly dataSource: DataSource, + private readonly svc_articlesValidate: ArticlesValidateService, + private readonly repo_articlesPaginate: ArticlesPaginateRepository, + private readonly svc_follow: FollowsService, + private readonly svc_utils: UtilsService, + @Inject(CACHE_MANAGER) private db_cacheManager: Cache, + ) {} + + createDefaultCursor( + digitById: number, + digitByTargetColumn: number, + initialValue: string, + ) { + const defaultCustomCursor: string = + String().padStart(digitByTargetColumn, `${initialValue}`) + + String().padStart(digitById, `${initialValue}`); + return defaultCustomCursor; + } + + async createCustomCursor({ article, order }): Promise { + const id = article.id; + const _order = article[order]; + const customCursor: string = + String(_order).padStart(7, '0') + String(id).padStart(7, '0'); + + return customCursor; + } + + async createCursorResponse({ + cursorOption, + articles, + }: IArticlesServiceCreateCursorResponse): Promise< + CustomCursorPageDto + > { + const order = ArticleOrderOption[cursorOption.order]; + let hasNextData: boolean = true; + let customCursor: string; + + const takePerPage = cursorOption.take; + const isLastPage = articles.length <= takePerPage; + const responseData = articles.slice(0, takePerPage); + const lastDataPerPage = responseData[responseData.length - 1]; + + if (isLastPage) { + hasNextData = false; + customCursor = null; + } else { + customCursor = await this.createCustomCursor({ + article: lastDataPerPage, + order, + }); + } + + const customCursorPageMetaDto = new CustomCursorPageMetaDto({ + customCursorPageOptionsDto: cursorOption, + hasNextData, + customCursor, + }); + + return new CustomCursorPageDto(responseData, customCursorPageMetaDto); + } + + async fetchArticlesCursor({ + cursorOption, + }: IArticlesServiceFetchArticlesCursor): Promise< + CustomCursorPageDto + > { + const cacheKey = `fetchArticlesCursor_${JSON.stringify(cursorOption)}`; + + const cachedArticles = + await this.db_cacheManager.get>(cacheKey); + if (cachedArticles) { + return cachedArticles; + } + + let dateFilter: Date; + if (cursorOption.dateCreated) + dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + const { articles } = await this.repo_articlesPaginate.fetchArticlesCursor({ + cursorOption, + dateFilter, + }); + const result = await this.createCursorResponse({ articles, cursorOption }); + await this.db_cacheManager.set(cacheKey, result, 180000); + return result; + } + + async fetchFriendsArticlesCursor({ + cursorOption, + userId, + }: IArticlesServiceFetchFriendsArticlesCursor): Promise< + CustomCursorPageDto + > { + let dateFilter: Date; + if (cursorOption.dateCreated) + dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + + const { articles } = + await this.repo_articlesPaginate.fetchFriendsArticlesCursor({ + cursorOption, + userId, + dateFilter, + }); + return await this.createCursorResponse({ articles, cursorOption }); + } + + async fetchUserArticlesCursor({ + userId, + targetUserId, + cursorOption, + }: IArticlesServiceFetchUserArticlesCursor): Promise< + CustomCursorPageDto + > { + let dateFilter: Date; + if (cursorOption.dateCreated) + dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + + const scope = await this.svc_follow.getScope({ + fromUser: targetUserId, + toUser: userId, + }); + const { articles } = await this.repo_articlesPaginate.fetchUserArticles({ + cursorOption, + dateFilter, + scope, + userId: targetUserId, + }); + return await this.createCursorResponse({ articles, cursorOption }); + } +} diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts new file mode 100644 index 0000000..8353198 --- /dev/null +++ b/src/APIs/articles/services/articles-read.service.ts @@ -0,0 +1,74 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { ArticlesValidateService } from './articles-validate.service'; +import { ArticlesReadRepository } from '../repositories/articles-read.repository'; +import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; +import { FollowsService } from 'src/APIs/follows/follows.service'; +import { + IArticlesServiceArticleId, + IArticlesServiceArticleUserIdPair, +} from '../interfaces/articles.service.interface'; +import { ArticleDetailForUpdateResponseDto } from '../dtos/response/article-detail-for-update-response.dto'; +import { ArticleDto } from '../dtos/common/article.dto'; +import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; + +@Injectable() +export class ArticlesReadService { + constructor( + private readonly dataSource: DataSource, + private readonly svc_articlesValidate: ArticlesValidateService, + private readonly svc_stickerBlocks: StickerBlocksService, + private readonly svc_follows: FollowsService, + private readonly repo_articlesRead: ArticlesReadRepository, + ) {} + + async findArticlesById({ articleId }: IArticlesServiceArticleId) { + return await this.repo_articlesRead.findOne({ where: { id: articleId } }); + } + + async readArticleUpdateDetail({ + articleId, + userId, + }: IArticlesServiceArticleUserIdPair): Promise { + const data = await this.svc_articlesValidate.existCheck({ articleId }); + await this.svc_articlesValidate.fkValidCheck({ + articles: data, + passNonEssentail: true, + }); + if (data.userId !== userId) + throw new UnauthorizedException('본인이 아닙니다.'); + const article = await this.repo_articlesRead.readUpdateDetail(id); + const stickerBlocks = await this.svc_stickerBlocks.fetchBlocks({ + articleId, + }); + return { article, stickerBlocks }; + } + + async readTempArticles({ + userId, + }): Promise { + return await this.repo_articlesRead.readTemp(userId); + } + + async readArticleDetail({ + userId, + articleId, + }: IArticlesServiceArticleUserIdPair): Promise { + const data = await this.svc_articlesValidate.existCheck({ articleId }); + await this.svc_articlesValidate.fkValidCheck({ + articles: data, + passNonEssentail: false, + }); + const scope = await this.svc_follows.getScope({ + fromUser: data.userId, + toUser: userId, + }); + // const comments = await this.commentsService.fetchComments({ articlesId: id }); + const article = await this.repo_articlesRead.readDetail({ + articleId, + scope, + }); + console.log(data, article); + return article; + } +} diff --git a/src/APIs/articles/services/articles-update.service.ts b/src/APIs/articles/services/articles-update.service.ts new file mode 100644 index 0000000..e58a8b6 --- /dev/null +++ b/src/APIs/articles/services/articles-update.service.ts @@ -0,0 +1,37 @@ +import { ForbiddenException } from '@nestjs/common'; +import { ArticlesValidateService } from './articles-validate.service'; +import { ArticlesReadRepository } from '../repositories/articles-read.repository'; +import { DataSource } from 'typeorm'; +import { ArticlesUpdateRepository } from '../repositories/articles-update.repository'; +import { ArticlesCreateRepository } from '../repositories/articles-create.repository'; +import { IArticlesServicePatchArticle } from '../interfaces/articles.service.interface'; +import { ArticleDto } from '../dtos/common/article.dto'; + +export class ArticlesUpdateService { + constructor( + private readonly dataSource: DataSource, + private readonly svc_articlesValidate: ArticlesValidateService, + private readonly repo_articlesRead: ArticlesReadRepository, + private readonly repo_articlesCreate: ArticlesCreateRepository, + private readonly repo_articlesUpdate: ArticlesUpdateRepository, + ) {} + async patchArticle({ + userId, + articleId, + ...rest + }: IArticlesServicePatchArticle): Promise { + const articleData = await this.svc_articlesValidate.existCheck({ + articleId, + }); + if (articleData.userId != userId) + throw new ForbiddenException('게시글 작성자가 아닙니다.'); + Object.keys(rest).forEach((value) => { + if (rest[value] != null) articleData[value] = rest[value]; + }); + await this.svc_articlesValidate.fkValidCheck({ + articles: articleData, + passNonEssentail: false, + }); + return await this.repo_articlesCreate.save(articleData); // 바꾸자로직 + } +} diff --git a/src/APIs/articles/services/articles-validate.service.ts b/src/APIs/articles/services/articles-validate.service.ts new file mode 100644 index 0000000..ca5224a --- /dev/null +++ b/src/APIs/articles/services/articles-validate.service.ts @@ -0,0 +1,50 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { IArticlesServiceArticleId } from '../interfaces/articles.service.interface'; +import { DataSource } from 'typeorm'; +import { ArticleCategory } from 'src/APIs/articleCategories/entities/articleCategory.entity'; +import { ArticleBackground } from 'src/APIs/articleBackgrounds/entities/articleBackground.entity'; +import { User } from 'src/APIs/users/entities/user.entity'; +import { ArticlesReadRepository } from '../repositories/articles-read.repository'; + +@Injectable() +export class ArticlesValidateService { + constructor( + private readonly dataSource: DataSource, + private readonly repo_articlesRead: ArticlesReadRepository, + ) {} + + async existCheck({ articleId }: IArticlesServiceArticleId) { + const data = await this.repo_articlesRead.findOne({ + where: { id: articleId }, + }); + if (!data) throw new NotFoundException('게시글을 찾을 수 없습니다.'); + return data; + } + + async fkValidCheck({ articles, passNonEssentail }) { + const pc = await this.dataSource + .getRepository(ArticleCategory) + .createQueryBuilder('pc') + .where('pc.id = :id', { id: articles.articleCategoryId }) + .getOne(); + if (!pc && !passNonEssentail) + throw new BadRequestException('존재하지 않는 article_category입니다.'); + const pg = await this.dataSource + .getRepository(ArticleBackground) + .createQueryBuilder('pg') + .where('pg.id = :id', { id: articles.articleBackgroundId }) + .getOne(); + if (!pg && articles.articleBackgroundId && !passNonEssentail) + throw new BadRequestException('존재하지 않는 article_background입니다.'); + const us = await this.dataSource + .getRepository(User) + .createQueryBuilder('us') + .where('us.id = :id', { id: articles.userId }) + .getOne(); + if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); + } +} diff --git a/src/APIs/articles/services/create-articles.service.ts b/src/APIs/articles/services/create-articles.service.ts deleted file mode 100644 index 7dab38c..0000000 --- a/src/APIs/articles/services/create-articles.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class CreateArticlesService { - constructor() {} -} diff --git a/src/APIs/articles/services/delete-articles.service.ts b/src/APIs/articles/services/delete-articles.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/articles/services/read-articles.service.ts b/src/APIs/articles/services/read-articles.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/articles/services/update-articles.service.ts b/src/APIs/articles/services/update-articles.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIs/stickerBlocks/dtos/common/stickerBlock.dto.ts b/src/APIs/stickerBlocks/dtos/common/stickerBlock.dto.ts new file mode 100644 index 0000000..272a0a0 --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/common/stickerBlock.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerBlock } from '../../entities/stickerblock.entity'; + +export class StickerBlockDto extends OmitType(StickerBlock, [ + 'sticker', + 'article', +]) {} diff --git a/src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts b/src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts new file mode 100644 index 0000000..f5b86b9 --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerBlockDto } from './stickerBlock.dto'; + +export class StickerBlocksCreateDto extends OmitType(StickerBlockDto, [ + 'id', + 'articleId', + 'dateCreated', + 'dateDeleted', + 'dateUpdated', +]) {} diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts deleted file mode 100644 index 8eb0b17..0000000 --- a/src/APIs/stickerBlocks/dtos/create-stickerBlock.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { StickerBlock } from '../entities/stickerblock.entity'; - -export class CreateStickerBlockInput extends OmitType(StickerBlock, [ - 'id', - 'posts', - 'postsId', - 'sticker', - 'stickerId', -]) {} - -export class CreateStickerBlockDto extends CreateStickerBlockInput { - postsId: number; - stickerId: number; - kakaoId: number; -} diff --git a/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts b/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts deleted file mode 100644 index 13d6f51..0000000 --- a/src/APIs/stickerBlocks/dtos/create-stickerBlocks.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, ValidateNested } from 'class-validator'; -import { CreateStickerBlockInput } from './create-stickerBlock.dto'; - -export class BulkInsertStickerInput extends CreateStickerBlockInput { - @ApiProperty({ type: Number, description: '스티커의 id' }) - stickerId: number; -} - -export class CreateStickerBlocksInput { - @ApiProperty({ type: [BulkInsertStickerInput] }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => BulkInsertStickerInput) - stickerBlocks: BulkInsertStickerInput[]; -} - -export class CreateStickerBlocksDto { - stickerBlocks: BulkInsertStickerInput[]; - postsId: number; - kakaoId: number; -} - -export class CreateStickerBlocksResponseDto extends BulkInsertStickerInput { - @ApiProperty({ type: Number, description: '게시글 아이디' }) - postsId: number; - - @ApiProperty({ type: Number, description: '스티커블록 아이디' }) - id: number; -} diff --git a/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts b/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts new file mode 100644 index 0000000..9a7df85 --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts @@ -0,0 +1,19 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerBlock } from '../entities/stickerblock.entity'; +import { StickerBlockDto } from '../common/stickerBlock.dto'; + +export class StickerBlockCreateRequestDto extends OmitType(StickerBlockDto, [ + 'id', + 'articleId', + 'stickerId', + 'dateCreated', + 'dateDeleted', + 'dateUpdated', +]) {} + +//인터페이스화? +export class CreateStickerBlockDto extends StickerBlockCreateRequestDto { + articleId: number; + stickerId: number; + userId: number; +} diff --git a/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts b/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts new file mode 100644 index 0000000..bd36715 --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StickerBlocksCreateDto } from '../common/stickerBlocks.create.dto'; +import { IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class StickerBlocksCreateRequestDto { + @ApiProperty({ type: [StickerBlocksCreateDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => StickerBlocksCreateDto) + stickerBlocks: StickerBlocksCreateDto[]; +} diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 110ae72..39030f8 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -68,16 +68,16 @@ export class StickerBlocksService { } async deleteBlocks({ - kakaoId, - postsId, + userId, + articleId, }: IStikcerBlocksServiceDeleteBlocks): Promise { const blocksToDelete = await this.stickerBlocksRepository.find({ relations: ['sticker'], - where: { postsId }, + where: { articleId }, }); for (const block of blocksToDelete) { if (block.sticker.isReusable === false) - await this.stickersService.delete({ kakaoId, id: block.id }); + await this.stickersService.delete({ userId, id: block.id }); await this.stickerBlocksRepository.remove(block); } return; diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index fc4d759..f192727 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -23,7 +23,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; +import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; import { Sticker } from './entities/sticker.entity'; diff --git a/src/APIs/stickers/stickers.module.ts b/src/APIs/stickers/stickers.module.ts index 28ca9ad..fca1f6a 100644 --- a/src/APIs/stickers/stickers.module.ts +++ b/src/APIs/stickers/stickers.module.ts @@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Sticker } from './entities/sticker.entity'; import { StickersController } from './stickers.controller'; import { StickersService } from './stickers.service'; -import { UtilsService } from 'src/utils/utils.service'; +import { UtilsService } from 'src/modules/utils/utils.service'; import { UsersModule } from '../users/users.module'; import { AwsService } from 'src/modules/aws/aws.service'; diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 8d4fa5c..3c14307 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -3,8 +3,8 @@ import { Repository } from 'typeorm'; import { Sticker } from './entities/sticker.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { CreateStickerDto } from './dtos/create-sticker.dto'; -import { UtilsService } from 'src/utils/utils.service'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; +import { UtilsService } from 'src/modules/utils/utils.service'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { UsersService } from '../users/users.service'; import { UpdateStickerDto } from './dtos/update-sticker.dto'; import { diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index d2d8780..a3b1621 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -30,8 +30,8 @@ import { UserResponseDtoWithFollowing, } from './dtos/user-response.dto'; import { PatchUserInput } from './dtos/patch-user.input'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { ImageUploadDto } from 'src/common/dto/image-upload.dto'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; +import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { DeleteUserInput } from './dtos/delete-user.dto'; diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index 5a83b65..7579667 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { UtilsService } from 'src/utils/utils.service'; +import { UtilsService } from 'src/modules/utils/utils.service'; import { UsersRepository } from './users.repository'; import { AwsService } from 'src/modules/aws/aws.service'; diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts index 930e803..cc17278 100644 --- a/src/APIs/users/users.service.ts +++ b/src/APIs/users/users.service.ts @@ -16,8 +16,8 @@ import { UserResponseDto, UserResponseDtoWithFollowing, } from './dtos/user-response.dto'; -import { ImageUploadResponseDto } from 'src/common/dto/image-upload-response.dto'; -import { UtilsService } from 'src/utils/utils.service'; +import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; +import { UtilsService } from 'src/modules/utils/utils.service'; import { UploadImageDto } from './dtos/upload-image.dto'; import { UsersRepository } from './users.repository'; import { DataSource, UpdateResult } from 'typeorm'; diff --git a/src/common/dto/image-upload-response.dto.ts b/src/common/dtos/image-upload-response.dto.ts similarity index 100% rename from src/common/dto/image-upload-response.dto.ts rename to src/common/dtos/image-upload-response.dto.ts diff --git a/src/common/dto/image-upload.dto.ts b/src/common/dtos/image-upload.dto.ts similarity index 100% rename from src/common/dto/image-upload.dto.ts rename to src/common/dtos/image-upload.dto.ts diff --git a/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts b/src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto.ts similarity index 100% rename from src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts rename to src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto.ts diff --git a/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/modules/utils/cursor-pages/dtos/cursor-page-option.dto.ts similarity index 100% rename from src/utils/cursor-pages/dtos/cursor-page-option.dto.ts rename to src/modules/utils/cursor-pages/dtos/cursor-page-option.dto.ts diff --git a/src/utils/cursor-pages/dtos/cursor-page.dto.ts b/src/modules/utils/cursor-pages/dtos/cursor-page.dto.ts similarity index 100% rename from src/utils/cursor-pages/dtos/cursor-page.dto.ts rename to src/modules/utils/cursor-pages/dtos/cursor-page.dto.ts diff --git a/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts b/src/modules/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts similarity index 100% rename from src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts rename to src/modules/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts diff --git a/src/utils/pages/page-request.ts b/src/modules/utils/pages/page-request.ts similarity index 100% rename from src/utils/pages/page-request.ts rename to src/modules/utils/pages/page-request.ts diff --git a/src/utils/pages/page.ts b/src/modules/utils/pages/page.ts similarity index 100% rename from src/utils/pages/page.ts rename to src/modules/utils/pages/page.ts diff --git a/src/utils/utils.module.ts b/src/modules/utils/utils.module.ts similarity index 100% rename from src/utils/utils.module.ts rename to src/modules/utils/utils.module.ts diff --git a/src/modules/utils/utils.service.ts b/src/modules/utils/utils.service.ts new file mode 100644 index 0000000..b7bb12e --- /dev/null +++ b/src/modules/utils/utils.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { DateOption } from 'src/common/enums/date-option'; +import { v4 } from 'uuid'; + +@Injectable() +export class UtilsService { + getDate(date_created: DateOption): Date { + let currentDate = new Date(); + switch (date_created) { + case DateOption.WEEK: + currentDate.setDate(currentDate.getDate() - 7); + break; + case DateOption.MONTH: + currentDate.setMonth(currentDate.getMonth() - 1); + break; + case DateOption.YEAR: + currentDate.setFullYear(currentDate.getFullYear() - 1); + break; + default: + currentDate = null; + } + return currentDate; + } + + getUUID(): string { + return v4(); + } +} diff --git a/src/utils/utils.service.ts b/src/utils/utils.service.ts deleted file mode 100644 index d090d17..0000000 --- a/src/utils/utils.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { v4 } from 'uuid'; - -@Injectable() -export class UtilsService { - getUUID(): string { - return v4(); - } -} From 38986d81373e2d4d5b3a2b6ac7cef2e39b66c501 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 16:57:06 +0900 Subject: [PATCH 204/236] refactor(utils): divide utils - modulize utils which need to access repos - functionize simple utils not needing DI --- .../controllers/articles-read.controller.ts | 2 +- .../dtos/request/articles-get-request.dto.ts | 2 +- .../response/articles-get-response.dto.ts | 2 +- .../services/articles-create.service.ts | 5 +- .../services/articles-paginate.service.ts | 13 +++-- src/APIs/users/dtos/common/user.dto.ts | 31 +++++++++++ src/APIs/users/dtos/create-user.input.ts | 8 --- .../user-delete-request.dto.ts} | 6 ++- .../user-patch-request.dto.ts} | 2 +- .../request/user-update-image-request.dto.ts | 8 +++ .../response/user-following-response.dto.ts | 7 +++ .../response/user-primary-response.dto.ts | 19 +++++++ src/APIs/users/dtos/upload-image.dto.ts | 8 --- src/APIs/users/dtos/user-response.dto.ts | 40 -------------- src/APIs/users/users.controller.ts | 11 ---- src/modules/utils/pages/page-request.ts | 53 ------------------- src/modules/utils/pages/page.ts | 21 -------- src/modules/utils/utils.service.ts | 26 +-------- src/utils/classUtils.ts | 8 +++ .../cursor-pages/dtos/cursor-page-meta.dto.ts | 0 .../dtos/cursor-page-option.dto.ts | 0 .../cursor-pages/dtos/cursor-page.dto.ts | 0 .../interfaces/cursor-page-meta-dto-params.ts | 0 src/utils/dateUtils.ts | 19 +++++++ src/utils/uuidUtils.ts | 5 ++ 25 files changed, 114 insertions(+), 182 deletions(-) create mode 100644 src/APIs/users/dtos/common/user.dto.ts delete mode 100644 src/APIs/users/dtos/create-user.input.ts rename src/APIs/users/dtos/{delete-user.dto.ts => request/user-delete-request.dto.ts} (55%) rename src/APIs/users/dtos/{patch-user.input.ts => request/user-patch-request.dto.ts} (95%) create mode 100644 src/APIs/users/dtos/request/user-update-image-request.dto.ts create mode 100644 src/APIs/users/dtos/response/user-following-response.dto.ts create mode 100644 src/APIs/users/dtos/response/user-primary-response.dto.ts delete mode 100644 src/APIs/users/dtos/upload-image.dto.ts delete mode 100644 src/modules/utils/pages/page-request.ts delete mode 100644 src/modules/utils/pages/page.ts create mode 100644 src/utils/classUtils.ts rename src/{modules => }/utils/cursor-pages/dtos/cursor-page-meta.dto.ts (100%) rename src/{modules => }/utils/cursor-pages/dtos/cursor-page-option.dto.ts (100%) rename src/{modules => }/utils/cursor-pages/dtos/cursor-page.dto.ts (100%) rename src/{modules => }/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts (100%) create mode 100644 src/utils/dateUtils.ts create mode 100644 src/utils/uuidUtils.ts diff --git a/src/APIs/articles/controllers/articles-read.controller.ts b/src/APIs/articles/controllers/articles-read.controller.ts index cb5b84e..f49daa6 100644 --- a/src/APIs/articles/controllers/articles-read.controller.ts +++ b/src/APIs/articles/controllers/articles-read.controller.ts @@ -23,7 +23,7 @@ import { ArticlesGetResponseDto } from '../dtos/response/articles-get-response.d import { ArticlesGetRequestDto } from '../dtos/request/articles-get-request.dto'; import { ArticlesPaginateService } from '../services/articles-paginate.service'; import { SortOption } from 'src/common/enums/sort-option'; -import { CustomCursorPageDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page.dto'; +import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; @ApiTags('게시글 API') @Controller('articles') diff --git a/src/APIs/articles/dtos/request/articles-get-request.dto.ts b/src/APIs/articles/dtos/request/articles-get-request.dto.ts index ef784a6..0fef9b0 100644 --- a/src/APIs/articles/dtos/request/articles-get-request.dto.ts +++ b/src/APIs/articles/dtos/request/articles-get-request.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; import { ArticleOrderOptionWrap } from 'src/common/enums/article-order-option'; import { DateOption } from 'src/common/enums/date-option'; -import { CustomCursorPageOptionsDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-option.dto'; +import { CustomCursorPageOptionsDto } from 'src/utils/cursor-pages/dtos/cursor-page-option.dto'; export class ArticlesGetRequestDto extends CustomCursorPageOptionsDto { @ApiProperty({ diff --git a/src/APIs/articles/dtos/response/articles-get-response.dto.ts b/src/APIs/articles/dtos/response/articles-get-response.dto.ts index a79bf3f..be8bab6 100644 --- a/src/APIs/articles/dtos/response/articles-get-response.dto.ts +++ b/src/APIs/articles/dtos/response/articles-get-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { CustomCursorPageMetaDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { ArticleDto } from '../common/article.dto'; export class ArticlesGetResponseDto { diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts index fb24da6..8bf4a96 100644 --- a/src/APIs/articles/services/articles-create.service.ts +++ b/src/APIs/articles/services/articles-create.service.ts @@ -6,9 +6,9 @@ import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.servi import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesReadRepository } from '../repositories/articles-read.repository'; import { AwsService } from 'src/modules/aws/aws.service'; -import { UtilsService } from 'src/modules/utils/utils.service'; import { ArticleCreateResponseDto } from '../dtos/response/article-create-response.dto'; import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; +import { getUUID } from 'src/utils/uuidUtils'; @Injectable() export class ArticlesCreateService { @@ -17,7 +17,6 @@ export class ArticlesCreateService { private readonly svc_articlesValidate: ArticlesValidateService, private readonly svc_stickerBlocks: StickerBlocksService, private readonly svc_aws: AwsService, - private readonly svc_utils: UtilsService, private readonly repo_articlesRead: ArticlesReadRepository, ) {} @@ -66,7 +65,7 @@ export class ArticlesCreateService { async imageUpload( file: Express.Multer.File, ): Promise { - const imageName = this.svc_utils.getUUID(); + const imageName = getUUID(); const ext = file.originalname.split('.').pop(); const imageUrl = await this.svc_aws.imageUploadToS3( diff --git a/src/APIs/articles/services/articles-paginate.service.ts b/src/APIs/articles/services/articles-paginate.service.ts index 36127ac..f8bcbb7 100644 --- a/src/APIs/articles/services/articles-paginate.service.ts +++ b/src/APIs/articles/services/articles-paginate.service.ts @@ -2,12 +2,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesPaginateRepository } from '../repositories/articles-paginate.repository.ts'; import { ArticleOrderOption } from 'src/common/enums/article-order-option'; -import { CustomCursorPageMetaDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto'; -import { CustomCursorPageDto } from 'src/modules/utils/cursor-pages/dtos/cursor-page.dto'; +import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; +import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; import { FollowsService } from 'src/APIs/follows/follows.service'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; -import { UtilsService } from 'src/modules/utils/utils.service'; import { IArticlesServiceCreateCursorResponse, IArticlesServiceFetchArticlesCursor, @@ -15,6 +14,7 @@ import { IArticlesServiceFetchUserArticlesCursor, } from '../interfaces/articles.service.interface'; import { ArticleDto } from '../dtos/common/article.dto'; +import { getDate } from 'src/utils/dateUtils'; @Injectable() export class ArticlesPaginateService { @@ -23,7 +23,6 @@ export class ArticlesPaginateService { private readonly svc_articlesValidate: ArticlesValidateService, private readonly repo_articlesPaginate: ArticlesPaginateRepository, private readonly svc_follow: FollowsService, - private readonly svc_utils: UtilsService, @Inject(CACHE_MANAGER) private db_cacheManager: Cache, ) {} @@ -96,7 +95,7 @@ export class ArticlesPaginateService { let dateFilter: Date; if (cursorOption.dateCreated) - dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + dateFilter = getDate(cursorOption.dateCreated); const { articles } = await this.repo_articlesPaginate.fetchArticlesCursor({ cursorOption, dateFilter, @@ -114,7 +113,7 @@ export class ArticlesPaginateService { > { let dateFilter: Date; if (cursorOption.dateCreated) - dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + dateFilter = getDate(cursorOption.dateCreated); const { articles } = await this.repo_articlesPaginate.fetchFriendsArticlesCursor({ @@ -134,7 +133,7 @@ export class ArticlesPaginateService { > { let dateFilter: Date; if (cursorOption.dateCreated) - dateFilter = this.svc_utils.getDate(cursorOption.dateCreated); + dateFilter = getDate(cursorOption.dateCreated); const scope = await this.svc_follow.getScope({ fromUser: targetUserId, diff --git a/src/APIs/users/dtos/common/user.dto.ts b/src/APIs/users/dtos/common/user.dto.ts new file mode 100644 index 0000000..f52c241 --- /dev/null +++ b/src/APIs/users/dtos/common/user.dto.ts @@ -0,0 +1,31 @@ +import { OmitType } from '@nestjs/swagger'; +import { User } from '../../entities/user.entity'; +import { getMetadataArgsStorage } from 'typeorm'; +import { getClassFields } from 'src/utils/classUtils'; + +// exclude refreshtoken!! +export class UserDto extends OmitType(User, [ + 'agreements', + 'articles', + 'articleCategories', + 'currentRefreshToken', + 'comments', + 'feedbacks', + 'followers', + 'followings', + 'receivedNotifications', + 'receivedReports', + 'sentNotifications', + 'sentReports', + 'stickers', +] as const) {} + +export const USER_SELECT_OPTION: { [key: string]: boolean } = getClassFields( + UserDto, +).reduce( + (options, field) => { + options[field] = true; + return options; + }, + {} as { [key: string]: boolean }, +); diff --git a/src/APIs/users/dtos/create-user.input.ts b/src/APIs/users/dtos/create-user.input.ts deleted file mode 100644 index d3862f3..0000000 --- a/src/APIs/users/dtos/create-user.input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -export class CreateUserInput { - @ApiProperty({ type: 'number' }) - @IsNotEmpty() - kakaoId: number; -} diff --git a/src/APIs/users/dtos/delete-user.dto.ts b/src/APIs/users/dtos/request/user-delete-request.dto.ts similarity index 55% rename from src/APIs/users/dtos/delete-user.dto.ts rename to src/APIs/users/dtos/request/user-delete-request.dto.ts index 3b37ac9..eb5901f 100644 --- a/src/APIs/users/dtos/delete-user.dto.ts +++ b/src/APIs/users/dtos/request/user-delete-request.dto.ts @@ -1,5 +1,7 @@ import { PickType } from '@nestjs/swagger'; - import { Feedback } from 'src/APIs/feedbacks/entities/feedback.entity'; -export class DeleteUserInput extends PickType(Feedback, ['content', 'type']) {} +export class UserDeleteRequestDto extends PickType(Feedback, [ + 'content', + 'type', +]) {} diff --git a/src/APIs/users/dtos/patch-user.input.ts b/src/APIs/users/dtos/request/user-patch-request.dto.ts similarity index 95% rename from src/APIs/users/dtos/patch-user.input.ts rename to src/APIs/users/dtos/request/user-patch-request.dto.ts index 382f129..82f7715 100644 --- a/src/APIs/users/dtos/patch-user.input.ts +++ b/src/APIs/users/dtos/request/user-patch-request.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, IsString } from 'class-validator'; -export class PatchUserInput { +export class PatchUserRequestDto { @ApiProperty({ description: '[optional] 핸들러 변경', type: String, diff --git a/src/APIs/users/dtos/request/user-update-image-request.dto.ts b/src/APIs/users/dtos/request/user-update-image-request.dto.ts new file mode 100644 index 0000000..6ba2a70 --- /dev/null +++ b/src/APIs/users/dtos/request/user-update-image-request.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserUploadImageRequestDto { + @ApiProperty({ description: '유저의 아이디', type: Number }) + userId: number; + + file: Express.Multer.File; +} diff --git a/src/APIs/users/dtos/response/user-following-response.dto.ts b/src/APIs/users/dtos/response/user-following-response.dto.ts new file mode 100644 index 0000000..dee5a34 --- /dev/null +++ b/src/APIs/users/dtos/response/user-following-response.dto.ts @@ -0,0 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserDto } from '../common/user.dto'; + +export class UserFollowingResponseDto extends UserDto { + @ApiProperty({ type: Boolean, description: '팔로잉 유무' }) + isFollowing: boolean; +} diff --git a/src/APIs/users/dtos/response/user-primary-response.dto.ts b/src/APIs/users/dtos/response/user-primary-response.dto.ts new file mode 100644 index 0000000..246c6f2 --- /dev/null +++ b/src/APIs/users/dtos/response/user-primary-response.dto.ts @@ -0,0 +1,19 @@ +import { PickType } from '@nestjs/swagger'; +import { User } from '../../entities/user.entity'; +import { getClassFields } from 'src/utils/classUtils'; + +export class UserPrimaryResponseDto extends PickType(User, [ + 'id', + 'username', + 'profileImage', + 'handle', +]) {} + +export const USER_PRIMARY_SELECT_OPTION: { [key: string]: boolean } = + getClassFields(UserPrimaryResponseDto).reduce( + (options, field) => { + options[field] = true; + return options; + }, + {} as { [key: string]: boolean }, + ); diff --git a/src/APIs/users/dtos/upload-image.dto.ts b/src/APIs/users/dtos/upload-image.dto.ts deleted file mode 100644 index e902a82..0000000 --- a/src/APIs/users/dtos/upload-image.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class UploadImageDto { - @ApiProperty({ description: '유저의 카카오 아이디', type: Number }) - userKakaoId: number; - - file: Express.Multer.File; -} diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts index 04ce75d..004cc8c 100644 --- a/src/APIs/users/dtos/user-response.dto.ts +++ b/src/APIs/users/dtos/user-response.dto.ts @@ -1,6 +1,3 @@ -import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; -import { User } from '../entities/user.entity'; - export const USER_SELECT_OPTION = { kakaoId: true, handle: true, @@ -14,40 +11,3 @@ export const USER_SELECT_OPTION = { date_created: true, date_deleted: true, }; -export const USER_PRIMARY_SELECT_OPTION = { - kakaoId: true, - handle: true, - username: true, - description: true, - profile_image: true, -}; -export class UserPrimaryResponseDto extends PickType(User, [ - 'kakaoId', - 'username', - 'profile_image', - 'description', - 'handle', -]) {} -export class UserResponseDto extends OmitType(User, ['current_refresh_token']) { - // @ApiProperty({ description: '카카오 id', type: Number }) - // kakaoId: number; - // // @ApiProperty({ description: 'crypted refresh token', type: String }) - // // current_refresh_token: string; - // @ApiProperty({ description: '어드민 유저 여부', type: Boolean }) - // isAdmin: boolean; - // @ApiProperty({ description: '유저 이름', type: String }) - // username: string; - // @ApiProperty({ description: '유저 설명', type: String }) - // description: string; - // @ApiProperty({ description: '프로필 이미지 url', type: String }) - // profile_image: string; - // @ApiProperty({ description: '생성된 날짜', type: Date }) - // date_created: Date; - // @ApiProperty({ description: '삭제된 날짜', type: Date }) - // date_deleted: Date; -} - -export class UserResponseDtoWithFollowing extends UserResponseDto { - @ApiProperty({ type: Boolean, description: '팔로잉 유무' }) - isFollowing: boolean; -} diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts index a3b1621..169aa81 100644 --- a/src/APIs/users/users.controller.ts +++ b/src/APIs/users/users.controller.ts @@ -41,17 +41,6 @@ import { DeleteUserInput } from './dtos/delete-user.dto'; export class UsersController { constructor(private readonly usersService: UsersService) {} - // 배포 때 삭제!!!! - @Get('all') - @ApiOperation({ - summary: '[ONLY FOR DEV] 모든 유저의 정보를 조회한다', - description: '배포 때 삭제할 거임. 개발 및 테스트용', - }) - findAllUsers() { - return this.usersService.getAll(); - } - // ================================ - @ApiOperation({ summary: '이름이 포함된 유저 검색', description: '이름에 username이 포함된 유저를 검색한다.', diff --git a/src/modules/utils/pages/page-request.ts b/src/modules/utils/pages/page-request.ts deleted file mode 100644 index 9675539..0000000 --- a/src/modules/utils/pages/page-request.ts +++ /dev/null @@ -1,53 +0,0 @@ -//pageRequest.ts -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsOptional, IsString } from 'class-validator'; - -//페이지네이션 요청 받을때 사용하는 클래스 양식 -export class PageRequest { - //@IsOptional() 데코레이터는 undefined도 받을 수 있다. - @ApiProperty({ - description: '요청할 페이지 번호', - type: Number, - required: false, - }) - @IsOptional() - @Type(() => Number) - pageNo?: number | 1; - - @ApiProperty({ - description: '한 페이지 당 아이템 갯수', - type: Number, - required: false, - }) - @IsOptional() - @Type(() => Number) - pageSize?: number | 10; - - getOffset(): number { - if (this.pageNo < 1 || this.pageNo === null || this.pageNo === undefined) { - this.pageNo = 1; - } - - if ( - this.pageSize < 1 || - this.pageSize === null || - this.pageSize === undefined - ) { - this.pageSize = 10; - } - - return (Number(this.pageNo) - 1) * Number(this.pageSize); - } - - getLimit(): number { - if ( - this.pageSize < 1 || - this.pageSize === null || - this.pageSize === undefined - ) { - this.pageSize = 10; - } - return Number(this.pageSize); - } -} diff --git a/src/modules/utils/pages/page.ts b/src/modules/utils/pages/page.ts deleted file mode 100644 index 9aaa64d..0000000 --- a/src/modules/utils/pages/page.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class Page { - @ApiProperty({ description: '한 페이지 당 아이템 갯수', type: Number }) - pageSize: number; - - @ApiProperty({ description: '전체 아이템 갯수', type: Number }) - totalCount: number; - - @ApiProperty({ description: '요청할 페이지 번호', type: Number }) - totalPage: number; - - @ApiProperty({ description: '아이템 배열', type: [] }) - items: T[]; - constructor(totalCount: number, pageSize: number, items: T[]) { - this.pageSize = pageSize; - this.totalCount = totalCount; - this.totalPage = Math.ceil(totalCount / pageSize); - this.items = items; - } -} diff --git a/src/modules/utils/utils.service.ts b/src/modules/utils/utils.service.ts index b7bb12e..b932bff 100644 --- a/src/modules/utils/utils.service.ts +++ b/src/modules/utils/utils.service.ts @@ -1,28 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { DateOption } from 'src/common/enums/date-option'; -import { v4 } from 'uuid'; @Injectable() -export class UtilsService { - getDate(date_created: DateOption): Date { - let currentDate = new Date(); - switch (date_created) { - case DateOption.WEEK: - currentDate.setDate(currentDate.getDate() - 7); - break; - case DateOption.MONTH: - currentDate.setMonth(currentDate.getMonth() - 1); - break; - case DateOption.YEAR: - currentDate.setFullYear(currentDate.getFullYear() - 1); - break; - default: - currentDate = null; - } - return currentDate; - } - - getUUID(): string { - return v4(); - } -} +export class UtilsService {} diff --git a/src/utils/classUtils.ts b/src/utils/classUtils.ts new file mode 100644 index 0000000..5c23806 --- /dev/null +++ b/src/utils/classUtils.ts @@ -0,0 +1,8 @@ +import { getMetadataArgsStorage } from 'typeorm'; + +export function getClassFields(dto: any): string[] { + const fields = getMetadataArgsStorage() + .filterColumns(dto) + .map((column) => column.propertyName); + return fields; +} diff --git a/src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts similarity index 100% rename from src/modules/utils/cursor-pages/dtos/cursor-page-meta.dto.ts rename to src/utils/cursor-pages/dtos/cursor-page-meta.dto.ts diff --git a/src/modules/utils/cursor-pages/dtos/cursor-page-option.dto.ts b/src/utils/cursor-pages/dtos/cursor-page-option.dto.ts similarity index 100% rename from src/modules/utils/cursor-pages/dtos/cursor-page-option.dto.ts rename to src/utils/cursor-pages/dtos/cursor-page-option.dto.ts diff --git a/src/modules/utils/cursor-pages/dtos/cursor-page.dto.ts b/src/utils/cursor-pages/dtos/cursor-page.dto.ts similarity index 100% rename from src/modules/utils/cursor-pages/dtos/cursor-page.dto.ts rename to src/utils/cursor-pages/dtos/cursor-page.dto.ts diff --git a/src/modules/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts b/src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts similarity index 100% rename from src/modules/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts rename to src/utils/cursor-pages/interfaces/cursor-page-meta-dto-params.ts diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts new file mode 100644 index 0000000..6ea7b60 --- /dev/null +++ b/src/utils/dateUtils.ts @@ -0,0 +1,19 @@ +import { DateOption } from 'src/common/enums/date-option'; + +export function getDate(date_created: DateOption): Date { + let currentDate = new Date(); + switch (date_created) { + case DateOption.WEEK: + currentDate.setDate(currentDate.getDate() - 7); + break; + case DateOption.MONTH: + currentDate.setMonth(currentDate.getMonth() - 1); + break; + case DateOption.YEAR: + currentDate.setFullYear(currentDate.getFullYear() - 1); + break; + default: + currentDate = null; + } + return currentDate; +} diff --git a/src/utils/uuidUtils.ts b/src/utils/uuidUtils.ts new file mode 100644 index 0000000..dd6b88d --- /dev/null +++ b/src/utils/uuidUtils.ts @@ -0,0 +1,5 @@ +import { v4 } from 'uuid'; + +export function getUUID(): string { + return v4(); +} From 8a06068b553fa393fe48133f6b51afbf94b64616 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 18:03:13 +0900 Subject: [PATCH 205/236] refactor(users): divide usersService --- .../articleBackgrounds.controller.ts | 2 +- .../services/articles-paginate.service.ts | 4 +- .../services/articles-read.service.ts | 1 - .../services/articles-update.service.ts | 3 - .../controllers/users-create.controller.ts | 8 + .../controllers/users-delete.controller.ts | 8 + .../users-read.controller copy 3.ts | 8 + .../users-update.controller copy.ts | 8 + src/APIs/users/dtos/common/user.dto.ts | 1 - .../request/user-update-image-request.dto.ts | 8 - src/APIs/users/dtos/user-response.dto.ts | 13 - .../interfaces/users.service.interface.ts | 11 +- .../users/services/users-create.service.ts | 19 ++ .../users/services/users-delete.service.ts | 116 +++++++ src/APIs/users/services/users-read.service.ts | 59 ++++ .../users/services/users-update.service.ts | 84 +++++ .../users/services/users-validate-service.ts | 28 ++ src/APIs/users/users.module.ts | 7 +- src/APIs/users/users.repository.ts | 63 ++-- src/APIs/users/users.service.ts | 298 ------------------ .../images/dtos/image-upload-request.dto.ts} | 2 +- .../images}/dtos/image-upload-response.dto.ts | 0 src/modules/images/images.module.ts | 10 + src/modules/images/images.service.ts | 32 ++ .../interfaces/images.service.interface.ts | 9 + src/utils/classUtils.ts | 15 + 26 files changed, 446 insertions(+), 371 deletions(-) create mode 100644 src/APIs/users/controllers/users-create.controller.ts create mode 100644 src/APIs/users/controllers/users-delete.controller.ts create mode 100644 src/APIs/users/controllers/users-read.controller copy 3.ts create mode 100644 src/APIs/users/controllers/users-update.controller copy.ts delete mode 100644 src/APIs/users/dtos/request/user-update-image-request.dto.ts delete mode 100644 src/APIs/users/dtos/user-response.dto.ts create mode 100644 src/APIs/users/services/users-create.service.ts create mode 100644 src/APIs/users/services/users-delete.service.ts create mode 100644 src/APIs/users/services/users-read.service.ts create mode 100644 src/APIs/users/services/users-update.service.ts create mode 100644 src/APIs/users/services/users-validate-service.ts delete mode 100644 src/APIs/users/users.service.ts rename src/{common/dtos/image-upload.dto.ts => modules/images/dtos/image-upload-request.dto.ts} (86%) rename src/{common => modules/images}/dtos/image-upload-response.dto.ts (100%) create mode 100644 src/modules/images/images.module.ts create mode 100644 src/modules/images/images.service.ts create mode 100644 src/modules/images/interfaces/images.service.interface.ts diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index fd1454f..08b7591 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -17,7 +17,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from '../../common/dtos/image-upload.dto'; +import { ImageUploadDto } from '../../modules/images/dtos/image-upload-request.dto'; import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { ArticleBackground } from './entities/articleBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; diff --git a/src/APIs/articles/services/articles-paginate.service.ts b/src/APIs/articles/services/articles-paginate.service.ts index f8bcbb7..1f14dd7 100644 --- a/src/APIs/articles/services/articles-paginate.service.ts +++ b/src/APIs/articles/services/articles-paginate.service.ts @@ -22,7 +22,7 @@ export class ArticlesPaginateService { // private readonly dataSource: DataSource, private readonly svc_articlesValidate: ArticlesValidateService, private readonly repo_articlesPaginate: ArticlesPaginateRepository, - private readonly svc_follow: FollowsService, + private readonly svc_follows: FollowsService, @Inject(CACHE_MANAGER) private db_cacheManager: Cache, ) {} @@ -135,7 +135,7 @@ export class ArticlesPaginateService { if (cursorOption.dateCreated) dateFilter = getDate(cursorOption.dateCreated); - const scope = await this.svc_follow.getScope({ + const scope = await this.svc_follows.getScope({ fromUser: targetUserId, toUser: userId, }); diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts index 8353198..1f0e7f0 100644 --- a/src/APIs/articles/services/articles-read.service.ts +++ b/src/APIs/articles/services/articles-read.service.ts @@ -9,7 +9,6 @@ import { IArticlesServiceArticleUserIdPair, } from '../interfaces/articles.service.interface'; import { ArticleDetailForUpdateResponseDto } from '../dtos/response/article-detail-for-update-response.dto'; -import { ArticleDto } from '../dtos/common/article.dto'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; @Injectable() diff --git a/src/APIs/articles/services/articles-update.service.ts b/src/APIs/articles/services/articles-update.service.ts index e58a8b6..a0354cb 100644 --- a/src/APIs/articles/services/articles-update.service.ts +++ b/src/APIs/articles/services/articles-update.service.ts @@ -9,11 +9,8 @@ import { ArticleDto } from '../dtos/common/article.dto'; export class ArticlesUpdateService { constructor( - private readonly dataSource: DataSource, private readonly svc_articlesValidate: ArticlesValidateService, - private readonly repo_articlesRead: ArticlesReadRepository, private readonly repo_articlesCreate: ArticlesCreateRepository, - private readonly repo_articlesUpdate: ArticlesUpdateRepository, ) {} async patchArticle({ userId, diff --git a/src/APIs/users/controllers/users-create.controller.ts b/src/APIs/users/controllers/users-create.controller.ts new file mode 100644 index 0000000..dc0c2c7 --- /dev/null +++ b/src/APIs/users/controllers/users-create.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersCreateController { + constructor(private readonly usersService: UsersService) {} +} diff --git a/src/APIs/users/controllers/users-delete.controller.ts b/src/APIs/users/controllers/users-delete.controller.ts new file mode 100644 index 0000000..41112fd --- /dev/null +++ b/src/APIs/users/controllers/users-delete.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersDeleteController { + constructor(private readonly usersService: UsersService) {} +} diff --git a/src/APIs/users/controllers/users-read.controller copy 3.ts b/src/APIs/users/controllers/users-read.controller copy 3.ts new file mode 100644 index 0000000..d34501d --- /dev/null +++ b/src/APIs/users/controllers/users-read.controller copy 3.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersReadController { + constructor(private readonly usersService: UsersService) {} +} diff --git a/src/APIs/users/controllers/users-update.controller copy.ts b/src/APIs/users/controllers/users-update.controller copy.ts new file mode 100644 index 0000000..7255d39 --- /dev/null +++ b/src/APIs/users/controllers/users-update.controller copy.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersUpdateController { + constructor(private readonly usersService: UsersService) {} +} diff --git a/src/APIs/users/dtos/common/user.dto.ts b/src/APIs/users/dtos/common/user.dto.ts index f52c241..1c3ed62 100644 --- a/src/APIs/users/dtos/common/user.dto.ts +++ b/src/APIs/users/dtos/common/user.dto.ts @@ -1,6 +1,5 @@ import { OmitType } from '@nestjs/swagger'; import { User } from '../../entities/user.entity'; -import { getMetadataArgsStorage } from 'typeorm'; import { getClassFields } from 'src/utils/classUtils'; // exclude refreshtoken!! diff --git a/src/APIs/users/dtos/request/user-update-image-request.dto.ts b/src/APIs/users/dtos/request/user-update-image-request.dto.ts deleted file mode 100644 index 6ba2a70..0000000 --- a/src/APIs/users/dtos/request/user-update-image-request.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class UserUploadImageRequestDto { - @ApiProperty({ description: '유저의 아이디', type: Number }) - userId: number; - - file: Express.Multer.File; -} diff --git a/src/APIs/users/dtos/user-response.dto.ts b/src/APIs/users/dtos/user-response.dto.ts deleted file mode 100644 index 004cc8c..0000000 --- a/src/APIs/users/dtos/user-response.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const USER_SELECT_OPTION = { - kakaoId: true, - handle: true, - isAdmin: true, - username: true, - follower_count: true, - following_count: true, - description: true, - profile_image: true, - background_image: true, - date_created: true, - date_deleted: true, -}; diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index 15d8e89..50b7a35 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -1,18 +1,23 @@ import { FeedbackType } from 'src/common/enums/feedback-type.enum'; import { User } from '../entities/user.entity'; +export interface IUsersServiceImageUpload { + userId: number; + file: Express.Multer.File; +} + export interface IUsersServiceCreate { - kakaoId: number; + userId: number; } export interface IUsersServiceFindUserByKakaoId { - kakaoId: number; + userId: number; } export interface IUsersServiceDelete { type: FeedbackType; content: string; - kakaoId: number; + userId: number; } export interface IUsersServiceFindUserByHandle extends Pick {} diff --git a/src/APIs/users/services/users-create.service.ts b/src/APIs/users/services/users-create.service.ts new file mode 100644 index 0000000..e4eb60c --- /dev/null +++ b/src/APIs/users/services/users-create.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { getUUID } from 'src/utils/uuidUtils'; +import { IUsersServiceCreate } from '../interfaces/users.service.interface'; +import { UsersRepository } from '../users.repository'; + +@Injectable() +export class UsersCreateService { + constructor(private readonly repo_users: UsersRepository) {} + + async create({ userId }: IUsersServiceCreate) { + const userTempName = 'USER' + getUUID().substring(0, 8); + const result = await this.repo_users.save({ + id: userId, + username: userTempName, + handle: userTempName, + }); + return result; + } +} diff --git a/src/APIs/users/services/users-delete.service.ts b/src/APIs/users/services/users-delete.service.ts new file mode 100644 index 0000000..b0fc42f --- /dev/null +++ b/src/APIs/users/services/users-delete.service.ts @@ -0,0 +1,116 @@ +import { DataSource } from 'typeorm'; +import { UsersRepository } from '../users.repository'; +import { Feedback } from 'src/APIs/feedbacks/entities/feedback.entity'; +import { Comment } from 'src/APIs/comments/entities/comment.entity'; +import { Article } from 'src/APIs/articles/entities/article.entity'; +import { Notification } from 'src/APIs/notifications/entities/notification.entity'; +import { Follow } from 'src/APIs/follows/entities/follow.entity'; +import { User } from '../entities/user.entity'; +import { Agreement } from 'src/APIs/agreements/entities/agreement.entity'; +import { getUUID } from 'src/utils/uuidUtils'; +import { Injectable } from '@nestjs/common'; +import { IUsersServiceDelete } from '../interfaces/users.service.interface'; + +@Injectable() +export class UsersDeleteService { + constructor( + private readonly repo_users: UsersRepository, + private readonly db_dataSource: DataSource, + ) {} + async delete({ userId, type, content }: IUsersServiceDelete): Promise { + const queryRunner = this.db_dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + // 피드백 생성 + await queryRunner.manager.save(Feedback, { + type, + content, + userId, + }); + const commentsToDelete = await queryRunner.manager.find(Comment, { + where: { userId }, + }); + for (const commentData of commentsToDelete) { + let childrenData = []; + if (commentData.parentId == null) + childrenData = await queryRunner.manager.find(Comment, { + where: { parentId: commentData.id }, + }); + if (childrenData.length == 0) { + await queryRunner.manager.delete(Comment, { + id: commentData.id, + user: { id: userId }, + }); + await queryRunner.manager.update(Article, commentData.articleId, { + commentCount: () => 'comment_count - 1', + }); + } else { + await queryRunner.manager.softDelete(Comment, { + user: { id: userId }, + id: commentData.id, + }); + } + } + await queryRunner.manager.delete(Notification, { + targetUserId: userId, + }); + await queryRunner.manager.softDelete(Article, { userId }); + // 연동된 댓글 soft delete + await queryRunner.manager.softDelete(Comment, { userId }); + // 팔로우 일괄 취소 + const followingsToDelete = await queryRunner.manager.find(Follow, { + where: { fromUserId: userId }, + }); + await queryRunner.manager.delete(Follow, { fromUserId: userId }); + await queryRunner.manager.update( + User, + { id: userId }, + { + followingCount: 0, + followerCount: 0, + }, + ); + for (const following of followingsToDelete) { + await queryRunner.manager.decrement( + User, + { kakaoId: following.toUserId }, + 'follower_count', + 1, + ); + } + // 팔로잉 일괄 취소 + const followersToDelete = await queryRunner.manager.find(Follow, { + where: { toUserId: userId }, + }); + await queryRunner.manager.delete(Follow, { toUserId: userId }); + for (const following of followersToDelete) { + await queryRunner.manager.decrement( + User, + { userId: following.fromUserId }, + 'following_count', + 1, + ); + } + await queryRunner.manager.delete(Agreement, { userId: userId }); + const userData = await queryRunner.manager.findOne(User, { + where: { id: userId }, + }); + const userTempName = 'USER' + getUUID().substring(0, 8); + userData.username = userTempName; + userData.handle = userTempName; + userData.description = ''; + userData.profileImage = ''; + userData.backgroundImage = ''; + await queryRunner.manager.save(User, userData); + await queryRunner.manager.softDelete(User, { id: userId }); + await queryRunner.commitTransaction(); + return; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } +} diff --git a/src/APIs/users/services/users-read.service.ts b/src/APIs/users/services/users-read.service.ts new file mode 100644 index 0000000..92fd258 --- /dev/null +++ b/src/APIs/users/services/users-read.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { UsersRepository } from '../users.repository'; +import { USER_SELECT_OPTION, UserDto } from '../dtos/common/user.dto'; +import { User } from '../entities/user.entity'; +import { UserFollowingResponseDto } from '../dtos/response/user-following-response.dto'; + +@Injectable() +export class UsersReadService { + constructor(private readonly repo_users: UsersRepository) {} + + async findUserById({ userId }: IUsersServiceFindUserById): Promise { + const result = await this.repo_users.findOne({ + select: USER_SELECT_OPTION, + where: { id: userId }, + }); + return result; + } + + async findUserByIdWithDelete({ + userId, + }: IUsersServiceFindUserByKakaoId): Promise { + const result = await this.repo_users.findOne({ + select: USER_SELECT_OPTION, + where: { id: userId }, + withDeleted: true, // 소프트 삭제된 사용자도 포함 + }); + return result; + } + + async findUserByHandle({ + handle, + }: IUsersServiceFindUserByHandle): Promise { + const result = await this.repo_users.findOne({ + select: USER_SELECT_OPTION, + where: { handle }, + }); + return result; + } + + async findUserByIdWithToken({ + userId, + }: IUsersServiceFindUserById): Promise { + const result = await this.repo_users.findOne({ + where: { id: userId }, + }); + return result; + } + + async findUsersByName({ + userId, + username, + }): Promise { + const users = await this.repo_users.readWithNameAndFollowing({ + userId, + username, + }); + return users; + } +} diff --git a/src/APIs/users/services/users-update.service.ts b/src/APIs/users/services/users-update.service.ts new file mode 100644 index 0000000..61df448 --- /dev/null +++ b/src/APIs/users/services/users-update.service.ts @@ -0,0 +1,84 @@ +import { ConflictException, Injectable } from '@nestjs/common'; +import { UpdateResult } from 'typeorm'; +import { User } from '../entities/user.entity'; +import { UsersRepository } from '../users.repository'; +import { UsersValidateService } from './users-validate-service'; +import { UserDto } from '../dtos/common/user.dto'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; +import { IUsersServiceImageUpload } from '../interfaces/users.service.interface'; +import { ImagesService } from 'src/modules/images/images.service'; + +@Injectable() +export class UsersUpdateService { + constructor( + private readonly repo_users: UsersRepository, + private readonly svc_usersValidate: UsersValidateService, + private readonly svc_images: ImagesService, + ) {} + + async activateUser({ userId }): Promise { + return await this.repo_users.update({ id: userId }, { dateDeleted: null }); + } + + async setCurrentRefreshToken({ userId, currentRefreshToken }): Promise { + const user = await this.repo_users.findOne({ where: { id: userId } }); + return await this.repo_users.save({ + ...user, + currentRefreshToken, + }); + } + + async patchUser({ userId, handle, description, username }): Promise { + const user = await this.svc_usersValidate.existCheck({ userId }); + if (description) { + user.description = description; + } + if (username) { + user.username = username; + } + if (handle) user.handle = handle; + try { + const data = await this.repo_users.save(user); + return data; + } catch (e) { + throw new ConflictException( + 'username || handle 값이 Unique하지 않습니다.', + ); + } + } + + async uploadProfileImage({ + userId, + file, + }: IUsersServiceImageUpload): Promise { + const user = await this.svc_usersValidate.existCheck({ + userId, + }); + const { imageUrl } = await this.svc_images.imageUpload({ + ext: 'jpg', + file, + resize: 800, + }); + await this.svc_images.deleteImage({ url: user.profileImage }); + + await this.repo_users.save({ ...user, profileImage: imageUrl }); + return { imageUrl }; + } + + async uploadBackgroundImage({ + userId, + file, + }: IUsersServiceImageUpload): Promise { + const user = await this.svc_usersValidate.existCheck({ + userId, + }); + const { imageUrl } = await this.svc_images.imageUpload({ + ext: 'jpg', + file, + resize: 1600, + }); + await this.svc_images.deleteImage({ url: user.backgroundImage }); + + await this.repo_users.save({ ...user, backgroundImage: imageUrl }); + return { imageUrl }; + } diff --git a/src/APIs/users/services/users-validate-service.ts b/src/APIs/users/services/users-validate-service.ts new file mode 100644 index 0000000..d51b963 --- /dev/null +++ b/src/APIs/users/services/users-validate-service.ts @@ -0,0 +1,28 @@ +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { UsersRepository } from '../users.repository'; +import { UserDto } from '../dtos/common/user.dto'; + +@Injectable() +export class UsersValidateService { + constructor(private readonly repo_users: UsersRepository) {} + async adminCheck({ userId }): Promise { + const user = await this.repo_users.findOne({ + where: { id: userId }, + }); + if (!user) throw new BadRequestException('존재하지 않는 유저 입니다.'); + if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); + return user; + } + + async existCheck({ userId }): Promise { + const user = await this.repo_users.findOne({ + where: { id: userId }, + }); + if (!user) throw new BadRequestException('존재하지 않는 유저 입니다.'); + return user; + } +} diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index 7579667..7af453e 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -3,13 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { UtilsService } from 'src/modules/utils/utils.service'; import { UsersRepository } from './users.repository'; -import { AwsService } from 'src/modules/aws/aws.service'; +import { ImagesModule } from 'src/modules/images/images.module'; @Module({ - imports: [TypeOrmModule.forFeature([User])], - providers: [UsersService, UsersRepository, AwsService, UtilsService], + imports: [TypeOrmModule.forFeature([User]), ImagesModule], + providers: [UsersService, UsersRepository], controllers: [UsersController], exports: [UsersService], }) diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index 34f461d..3f3fddd 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -2,48 +2,46 @@ import { Injectable } from '@nestjs/common'; import { User } from './entities/user.entity'; import { DataSource, Repository } from 'typeorm'; import { Follow } from '../follows/entities/follow.entity'; +import { plainToClass } from 'class-transformer'; +import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; +import { UserDto } from './dtos/common/user.dto'; +import { UserFollowingResponseDto } from './dtos/response/user-following-response.dto'; @Injectable() export class UsersRepository extends Repository { - constructor(private dataSource: DataSource) { - super(User, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(User, db_dataSource.createEntityManager()); } - getFollowQuery({ kakaoId }) { + getFollowQuery({ userId }) { const queryBuilder = this.createQueryBuilder('user') .leftJoinAndSelect( (subQuery) => { return subQuery - .select('follow.to_user', 'toUserKakaoId') + .select('follow.to_user', 'to_user_id') .from(Follow, 'follow') - .where('follow.fromUserKakaoId = :kakaoId', { kakaoId }); + .where('follow.from_user_id = :userId', { userId }); }, 'follow', - 'follow.toUserKakaoId = user.kakaoId', + 'follow.to_user_id = user.id', ) .where('user.date_deleted IS NULL') .select([ - 'user.username AS username', - 'user.kakaoId AS kakaoId', - 'user.handle AS handle', - 'user.isAdmin AS isAdmin', - 'user.username AS username', - 'user.following_count AS following_count', - 'user.follower_count AS follower_count', - 'user.description AS description', - 'user.profile_image AS profile_image', - 'user.background_image AS background_image', - 'user.date_created AS date_created', - 'user.date_deleted AS date_deleted', - 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ...getClassFields(UserDto).map( + (column) => `user.${column} AS ${column}`, + ), + 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ kakaoId }); + .setParameters({ userId }); return queryBuilder; } // 팔로잉 유무 포함 조회 - async fetchUsersWithNameAndFollowing({ kakaoId, username }) { - const queryBuilder = this.getFollowQuery({ kakaoId }); + async readWithNameAndFollowing({ + userId, + username, + }): Promise { + const queryBuilder = this.getFollowQuery({ userId }); const users = await queryBuilder // .andWhere('MATCH(user.username) AGAINST (:username IN BOOLEAN MODE)', { // username: `*${username}*`, @@ -53,19 +51,12 @@ export class UsersRepository extends Repository { }) .getRawMany(); - return users.map((user) => ({ - username: user.username, - kakaoId: user.kakaoId, - handle: user.handle, - following_count: user.following_count, - follower_count: user.follower_count, - isAdmin: user.isAdmin === 1, - description: user.description, - profile_image: user.profile_image, - background_image: user.background_image, - date_created: user.date_created, - date_deleted: user.date_deleted, - isFollowing: user.isFollowing === 1, - })); + return users.map((user) => + plainToClass(UserFollowingResponseDto, { + ...convertToCamelCase(user), + isAdmin: user.is_admin === 1, + isFollowing: user.isFollowing === 1, + }), + ); } } diff --git a/src/APIs/users/users.service.ts b/src/APIs/users/users.service.ts deleted file mode 100644 index cc17278..0000000 --- a/src/APIs/users/users.service.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { - BadRequestException, - ConflictException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; -import { - IUsersServiceCreate, - IUsersServiceDelete, - IUsersServiceFindUserByHandle, - IUsersServiceFindUserByKakaoId, - IUsersServiceImageUpload, -} from './interfaces/users.service.interface'; -import { - USER_SELECT_OPTION, - UserResponseDto, - UserResponseDtoWithFollowing, -} from './dtos/user-response.dto'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; -import { UtilsService } from 'src/modules/utils/utils.service'; -import { UploadImageDto } from './dtos/upload-image.dto'; -import { UsersRepository } from './users.repository'; -import { DataSource, UpdateResult } from 'typeorm'; -import { Posts } from '../articles/entities/article.entity'; -import { Follow } from '../follows/entities/follow.entity'; -import { User } from './entities/user.entity'; -import { Comment } from '../comments/entities/comment.entity'; -import { Feedback } from '../feedbacks/entities/feedback.entity'; -import { AwsService } from 'src/modules/aws/aws.service'; -import { Agreement } from '../agreements/entities/agreement.entity'; - -@Injectable() -export class UsersService { - constructor( - private readonly usersRepository: UsersRepository, - private readonly awsService: AwsService, - private readonly utilsService: UtilsService, - private readonly dataSource: DataSource, - ) {} - - // 배포 때 삭제 !!!! - async getAll() { - return this.usersRepository.find(); - } - // =========================== - - async adminCheck({ kakaoId }) { - const user = await this.findUserByKakaoId({ - kakaoId, - }); - if (!user.isAdmin) throw new UnauthorizedException('어드민이 아닙니다.'); - } - - async existCheck({ kakaoId }) { - const user = await this.findUserByKakaoId({ - kakaoId, - }); - if (!user) throw new BadRequestException('존재하지 않는 유저 입니다.'); - } - - async create({ kakaoId }: IUsersServiceCreate) { - const userTempName = 'USER' + this.utilsService.getUUID().substring(0, 8); - const result = await this.usersRepository.save({ - kakaoId, - username: userTempName, - handle: userTempName, - }); - return result; - } - - async findUserByKakaoId({ - kakaoId, - }: IUsersServiceFindUserByKakaoId): Promise { - const result = await this.usersRepository.findOne({ - select: USER_SELECT_OPTION, - where: { kakaoId: kakaoId }, - }); - return result; - } - - async findUserByKakaoIdWithDelete({ - kakaoId, - }: IUsersServiceFindUserByKakaoId): Promise { - const result = await this.usersRepository.findOne({ - select: USER_SELECT_OPTION, - where: { kakaoId: kakaoId }, - withDeleted: true, // 소프트 삭제된 사용자도 포함 - }); - return result; - } - - async activateUser({ kakaoId }): Promise { - return await this.usersRepository.update( - { kakaoId: kakaoId }, - { date_deleted: null }, - ); - } - - async findUserByHandle({ - handle, - }: IUsersServiceFindUserByHandle): Promise { - const result = await this.usersRepository.findOne({ - select: USER_SELECT_OPTION, - where: { handle }, - }); - return result; - } - - async findUserByKakaoIdWithToken({ - kakaoId, - }: IUsersServiceFindUserByKakaoId) { - const result = await this.usersRepository.findOne({ - where: { kakaoId: kakaoId }, - }); - return result; - } - - async setCurrentRefreshToken({ kakaoId, current_refresh_token }) { - const user = await this.findUserByKakaoId({ kakaoId }); - return await this.usersRepository.save({ - ...user, - current_refresh_token, - }); - } - - async patchUser({ - kakaoId, - handle, - description, - username, - }): Promise { - const user = await this.findUserByKakaoId({ kakaoId }); - if (description) { - user.description = description; - } - if (username) { - user.username = username; - } - if (handle) user.handle = handle; - try { - const data = await this.usersRepository.save(user); - return data; - } catch (e) { - throw new ConflictException( - 'username || handle 값이 Unique하지 않습니다.', - ); - } - } - - async findUsersByName({ - kakaoId, - username, - }): Promise { - const users = await this.usersRepository.fetchUsersWithNameAndFollowing({ - kakaoId, - username, - }); - return users; - } - - async uploadProfileImage({ - userKakaoId, - file, - }: UploadImageDto): Promise { - const user = await this.findUserByKakaoIdWithToken({ - kakaoId: userKakaoId, - }); - const { image_url } = await this.imageUpload({ file, resize: 800 }); - await this.usersRepository.save({ ...user, profile_image: image_url }); - await this.awsService.deleteImageFromS3({ url: user.profile_image }); - return { image_url }; - } - - async uploadBackgroundImage({ - userKakaoId, - file, - }: UploadImageDto): Promise { - const user = await this.findUserByKakaoIdWithToken({ - kakaoId: userKakaoId, - }); - const { image_url } = await this.imageUpload({ file, resize: 1600 }); - await this.usersRepository.save({ ...user, background_image: image_url }); - await this.awsService.deleteImageFromS3({ url: user.background_image }); - return { image_url }; - } - - async imageUpload({ - file, - resize, - }: IUsersServiceImageUpload): Promise { - const imageName = this.utilsService.getUUID(); - const ext = 'jpg'; - const image_url = await this.awsService.imageUploadToS3( - `${imageName}.${ext}`, - file, - ext, - resize, - ); - return { image_url }; - } - - async delete({ kakaoId, type, content }: IUsersServiceDelete): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - // 피드백 생성 - await queryRunner.manager.save(Feedback, { - type, - content, - userKakaoId: kakaoId, - }); - const commentsToDelete = await queryRunner.manager.find(Comment, { - where: { userKakaoId: kakaoId }, - }); - for (const data of commentsToDelete) { - let childrenData = []; - if (data.parentId == null) - childrenData = await queryRunner.manager.find(Comment, { - where: { parentId: data.id }, - }); - if (childrenData.length == 0) { - await queryRunner.manager.delete(Comment, { - id: data.id, - user: { kakaoId }, - }); - await queryRunner.manager.update(Posts, data.postsId, { - comment_count: () => 'comment_count - 1', - }); - } else { - await queryRunner.manager.softDelete(Comment, { - user: { kakaoId }, - id: data.id, - }); - } - } - await queryRunner.manager.delete(Notification, { - targetUserKakaoId: kakaoId, - }); - await queryRunner.manager.softDelete(Posts, { userKakaoId: kakaoId }); - // 연동된 댓글 soft delete - await queryRunner.manager.softDelete(Comment, { userKakaoId: kakaoId }); - // 팔로우 일괄 취소 - const followingsToDelete = await queryRunner.manager.find(Follow, { - where: { fromUserKakaoId: kakaoId }, - }); - await queryRunner.manager.delete(Follow, { fromUserKakaoId: kakaoId }); - await queryRunner.manager.update( - User, - { kakaoId }, - { - following_count: 0, - follower_count: 0, - }, - ); - for (const following of followingsToDelete) { - await queryRunner.manager.decrement( - User, - { kakaoId: following.toUserKakaoId }, - 'follower_count', - 1, - ); - } - // 팔로잉 일괄 취소 - const followersToDelete = await queryRunner.manager.find(Follow, { - where: { toUserKakaoId: kakaoId }, - }); - await queryRunner.manager.delete(Follow, { toUserKakaoId: kakaoId }); - for (const following of followersToDelete) { - await queryRunner.manager.decrement( - User, - { kakaoId: following.fromUserKakaoId }, - 'following_count', - 1, - ); - } - await queryRunner.manager.delete(Agreement, { userKakaoId: kakaoId }); - const userData = await queryRunner.manager.findOne(User, { - where: { kakaoId }, - }); - const userTempName = 'USER' + this.utilsService.getUUID().substring(0, 8); - userData.username = userTempName; - userData.handle = userTempName; - userData.description = ''; - userData.profile_image = ''; - userData.background_image = ''; - await queryRunner.manager.save(User, userData); - await queryRunner.manager.softDelete(User, { kakaoId }); - await queryRunner.commitTransaction(); - return; - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - } -} diff --git a/src/common/dtos/image-upload.dto.ts b/src/modules/images/dtos/image-upload-request.dto.ts similarity index 86% rename from src/common/dtos/image-upload.dto.ts rename to src/modules/images/dtos/image-upload-request.dto.ts index 779b2b2..51a45ad 100644 --- a/src/common/dtos/image-upload.dto.ts +++ b/src/modules/images/dtos/image-upload-request.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; -export class ImageUploadDto { +export class ImageUploadRequestDto { @ApiProperty({ type: String, format: 'binary', diff --git a/src/common/dtos/image-upload-response.dto.ts b/src/modules/images/dtos/image-upload-response.dto.ts similarity index 100% rename from src/common/dtos/image-upload-response.dto.ts rename to src/modules/images/dtos/image-upload-response.dto.ts diff --git a/src/modules/images/images.module.ts b/src/modules/images/images.module.ts new file mode 100644 index 0000000..03127a2 --- /dev/null +++ b/src/modules/images/images.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ImagesService } from './images.service'; +import { AwsModule } from '../aws/aws.module'; + +@Module({ + imports: [AwsModule], + providers: [ImagesService], + exports: [ImagesService], +}) +export class ImagesModule {} diff --git a/src/modules/images/images.service.ts b/src/modules/images/images.service.ts new file mode 100644 index 0000000..30b2098 --- /dev/null +++ b/src/modules/images/images.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { + IImagesServiceDeleteImage, + IImagesServiceUploadImage, +} from './interfaces/images.service.interface'; +import { ImageUploadResponseDto } from './dtos/image-upload-response.dto'; +import { getUUID } from 'src/utils/uuidUtils'; +import { AwsService } from '../aws/aws.service'; + +@Injectable() +export class ImagesService { + constructor(private readonly svc_aws: AwsService) {} + + async imageUpload({ + file, + resize, + ext, + }: IImagesServiceUploadImage): Promise { + const imageName = getUUID(); + const imageUrl = await this.svc_aws.imageUploadToS3( + `${imageName}.${ext}`, + file, + ext, + resize, + ); + return { imageUrl }; + } + + async deleteImage({ url }: IImagesServiceDeleteImage): Promise { + await this.svc_aws.deleteImageFromS3({ url }); + } +} diff --git a/src/modules/images/interfaces/images.service.interface.ts b/src/modules/images/interfaces/images.service.interface.ts new file mode 100644 index 0000000..0124550 --- /dev/null +++ b/src/modules/images/interfaces/images.service.interface.ts @@ -0,0 +1,9 @@ +export interface IImagesServiceUploadImage { + file: Express.Multer.File; + resize: number; + ext: 'jpg' | 'png'; +} + +export interface IImagesServiceDeleteImage { + url: string; +} diff --git a/src/utils/classUtils.ts b/src/utils/classUtils.ts index 5c23806..ae63c19 100644 --- a/src/utils/classUtils.ts +++ b/src/utils/classUtils.ts @@ -6,3 +6,18 @@ export function getClassFields(dto: any): string[] { .map((column) => column.propertyName); return fields; } +export function toCamelCase(snakeCase: string): string { + return snakeCase.replace(/_([a-z])/g, (group) => group[1].toUpperCase()); +} + +export function convertToCamelCase(obj: any): any { + if (Array.isArray(obj)) { + return obj.map((v) => convertToCamelCase(v)); + } else if (obj !== null && obj.constructor === Object) { + return Object.keys(obj).reduce((result, key) => { + result[toCamelCase(key)] = convertToCamelCase(obj[key]); + return result; + }, {} as any); + } + return obj; +} From e0a181d8743bae528312d0123670517eb7eff66d Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 19:19:01 +0900 Subject: [PATCH 206/236] refactor(users): divide usersController --- .../controllers/users-create.controller.ts | 3 +- .../controllers/users-delete.controller.ts | 56 ++++- .../users-read.controller copy 3.ts | 8 - .../controllers/users-read.controller.ts | 79 +++++++ .../users-update.controller copy.ts | 8 - .../controllers/users-update.controller.ts | 115 ++++++++++ .../dtos/request/user-patch-request.dto.ts | 2 +- .../interfaces/users.service.interface.ts | 4 +- .../users/services/users-create.service.ts | 2 +- .../users/services/users-delete.service.ts | 6 +- src/APIs/users/services/users-read.service.ts | 6 +- .../users/services/users-update.service.ts | 14 +- src/APIs/users/users.controller.ts | 215 ------------------ 13 files changed, 273 insertions(+), 245 deletions(-) delete mode 100644 src/APIs/users/controllers/users-read.controller copy 3.ts create mode 100644 src/APIs/users/controllers/users-read.controller.ts delete mode 100644 src/APIs/users/controllers/users-update.controller copy.ts create mode 100644 src/APIs/users/controllers/users-update.controller.ts delete mode 100644 src/APIs/users/users.controller.ts diff --git a/src/APIs/users/controllers/users-create.controller.ts b/src/APIs/users/controllers/users-create.controller.ts index dc0c2c7..8bdb5a5 100644 --- a/src/APIs/users/controllers/users-create.controller.ts +++ b/src/APIs/users/controllers/users-create.controller.ts @@ -1,8 +1,9 @@ import { Controller } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { UsersCreateService } from '../services/users-create.service'; @ApiTags('유저 API') @Controller('users') export class UsersCreateController { - constructor(private readonly usersService: UsersService) {} + constructor(private readonly svc_usersCreate: UsersCreateService) {} } diff --git a/src/APIs/users/controllers/users-delete.controller.ts b/src/APIs/users/controllers/users-delete.controller.ts index 41112fd..c3ff32f 100644 --- a/src/APIs/users/controllers/users-delete.controller.ts +++ b/src/APIs/users/controllers/users-delete.controller.ts @@ -1,8 +1,58 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + HttpCode, + Req, + Res, + UseGuards, +} from '@nestjs/common'; +import { + ApiCookieAuth, + ApiNoContentResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UsersDeleteService } from '../services/users-delete.service'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request, Response } from 'express'; +import { UserDeleteRequestDto } from '../dtos/request/user-delete-request.dto'; @ApiTags('유저 API') @Controller('users') export class UsersDeleteController { - constructor(private readonly usersService: UsersService) {} + constructor(private readonly svc_usersDelete: UsersDeleteService) {} + + @ApiOperation({ + summary: '회원 탈퇴(soft delete)', + description: '회원을 탈퇴하고 연동된 게시글과 댓글을 soft delete한다.', + }) + @ApiCookieAuth() + @UseGuards(AuthGuardV2) + @ApiNoContentResponse() + @HttpCode(204) + @Delete('me') + async deleteUser( + @Req() req: Request, + @Res() res: Response, + @Body() body: UserDeleteRequestDto, + ) { + const userId = req.user.userId; + const clientDomain = process.env.CLIENT_DOMAIN; + await this.svc_usersDelete.deleteUser({ userId, ...body }); + res.clearCookie('accessToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('refreshToken', { + httpOnly: true, + domain: clientDomain, + sameSite: 'none', + secure: true, + }); + res.clearCookie('isLoggedIn', { httpOnly: false, domain: clientDomain }); + return res.send(); + } } diff --git a/src/APIs/users/controllers/users-read.controller copy 3.ts b/src/APIs/users/controllers/users-read.controller copy 3.ts deleted file mode 100644 index d34501d..0000000 --- a/src/APIs/users/controllers/users-read.controller copy 3.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; - -@ApiTags('유저 API') -@Controller('users') -export class UsersReadController { - constructor(private readonly usersService: UsersService) {} -} diff --git a/src/APIs/users/controllers/users-read.controller.ts b/src/APIs/users/controllers/users-read.controller.ts new file mode 100644 index 0000000..5791eaf --- /dev/null +++ b/src/APIs/users/controllers/users-read.controller.ts @@ -0,0 +1,79 @@ +import { + Controller, + Get, + HttpCode, + Param, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UsersReadService } from '../services/users-read.service'; +import { UserFollowingResponseDto } from '../dtos/response/user-following-response.dto'; +import { Request } from 'express'; +import { UserDto } from '../dtos/common/user.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersReadController { + constructor(private readonly svc_usersRead: UsersReadService) {} + + @ApiOperation({ + summary: '이름이 포함된 유저 검색', + description: '이름에 username이 포함된 유저를 검색한다.', + }) + @ApiOkResponse({ + description: '조회 성공', + type: [UserFollowingResponseDto], + }) + @HttpCode(200) + @Get('username/:username') + async getUsersByName( + @Req() req: Request, + @Param('username') username: string, + ): Promise { + const userId = req.user.userId; + return await this.svc_usersRead.findUsersByName({ userId, username }); + } + + @ApiOperation({ + summary: '특정 유저 프로필 조회(id)', + description: 'id가 일치하는 유저 프로필을 조회한다.', + }) + @ApiOkResponse({ description: '조회 성공', type: UserDto }) + @HttpCode(200) + @Get('profile/id/:userId') + async getUserById(@Param('userId') userId: number): Promise { + return await this.svc_usersRead.findUserById({ userId }); + } + + @ApiOperation({ + summary: '특정 유저 프로필 조회(handle)', + description: 'handle이 일치하는 유저 프로필을 조회한다.', + }) + @ApiOkResponse({ description: '조회 성공', type: UserDto }) + @HttpCode(200) + @Get('profile/handle/:handle') + async getUserByHandle(@Param('handle') handle: string): Promise { + return await this.svc_usersRead.findUserByHandle({ handle }); + } + + @ApiOperation({ + summary: '로그인된 유저의 프로필 불러오기', + description: '로그인된 유저의 프로필을 불러온다.', + }) + @ApiCookieAuth() + @ApiOkResponse({ description: '불러오기 완료', type: UserDto }) + @Get('me') + @UseGuards(AuthGuardV2) + @HttpCode(200) + async getMyProfile(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.svc_usersRead.findUserById({ userId }); + } +} diff --git a/src/APIs/users/controllers/users-update.controller copy.ts b/src/APIs/users/controllers/users-update.controller copy.ts deleted file mode 100644 index 7255d39..0000000 --- a/src/APIs/users/controllers/users-update.controller copy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; - -@ApiTags('유저 API') -@Controller('users') -export class UsersUpdateController { - constructor(private readonly usersService: UsersService) {} -} diff --git a/src/APIs/users/controllers/users-update.controller.ts b/src/APIs/users/controllers/users-update.controller.ts new file mode 100644 index 0000000..e1e8f9d --- /dev/null +++ b/src/APIs/users/controllers/users-update.controller.ts @@ -0,0 +1,115 @@ +import { + Body, + Controller, + HttpCode, + Patch, + Post, + Req, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBody, + ApiConsumes, + ApiCookieAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UsersUpdateService } from '../services/users-update.service'; +import { UserDto } from '../dtos/common/user.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { UserPatchRequestDto } from '../dtos/request/user-patch-request.dto'; +import { Request } from 'express'; +import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; + +@ApiTags('유저 API') +@Controller('users') +export class UsersUpdateController { + constructor(private readonly svc_usersUpdate: UsersUpdateService) {} + + @ApiOperation({ + summary: '로그인된 유저의 이름이나 설명, 핸들을 변경', + description: '로그인된 유저의 이름이나 설명, 핸들, 혹은 모두를 변경한다.', + }) + @ApiOkResponse({ description: '변경 성공', type: UserDto }) + @ApiCookieAuth() + @Patch('me') + @HttpCode(200) + @UseGuards(AuthGuardV2) + async patchUser( + @Req() req: Request, + @Body() body: UserPatchRequestDto, + ): Promise { + const userId = req.user.userId; + const { description, username, handle } = body; + return await this.svc_usersUpdate.updateUser({ + userId, + description, + username, + handle, + }); + } + + @ApiOperation({ + summary: '로그인된 유저의 프로필 이미지를 변경', + description: '스토리지에 프로필 사진을 업로드하고 변경한다.', + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: '업로드 할 파일', + type: ImageUploadRequestDto, + }) + @ApiCreatedResponse({ + description: '업로드 성공', + type: ImageUploadResponseDto, + }) + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @UseInterceptors(FileInterceptor('file')) + @HttpCode(201) + @Post('me/profile-image') + async postProfileImage( + @Req() req: Request, + @UploadedFile() file: Express.Multer.File, + ): Promise { + const userId = req.user.userId; + return await this.svc_usersUpdate.updateProfileImage({ + userId, + file, + }); + } + + @ApiOperation({ + summary: '로그인된 유저의 배경 이미지를 변경', + description: '스토리지에 배경 사진을 업로드하고 변경한다.', + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: '업로드 할 파일', + type: ImageUploadRequestDto, + }) + @ApiCreatedResponse({ + description: '업로드 성공', + type: ImageUploadResponseDto, + }) + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @UseInterceptors(FileInterceptor('file')) + @HttpCode(201) + @Post('me/background-image') + async uploadBackgroundImage( + @Req() req: Request, + @UploadedFile() file: Express.Multer.File, + ): Promise { + const userId = req.user.userId; + return await this.svc_usersUpdate.updateBackgroundImage({ + userId, + file, + }); + } +} diff --git a/src/APIs/users/dtos/request/user-patch-request.dto.ts b/src/APIs/users/dtos/request/user-patch-request.dto.ts index 82f7715..06578e6 100644 --- a/src/APIs/users/dtos/request/user-patch-request.dto.ts +++ b/src/APIs/users/dtos/request/user-patch-request.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, IsString } from 'class-validator'; -export class PatchUserRequestDto { +export class UserPatchRequestDto { @ApiProperty({ description: '[optional] 핸들러 변경', type: String, diff --git a/src/APIs/users/interfaces/users.service.interface.ts b/src/APIs/users/interfaces/users.service.interface.ts index 50b7a35..7528f0f 100644 --- a/src/APIs/users/interfaces/users.service.interface.ts +++ b/src/APIs/users/interfaces/users.service.interface.ts @@ -10,7 +10,7 @@ export interface IUsersServiceCreate { userId: number; } -export interface IUsersServiceFindUserByKakaoId { +export interface IUsersServiceFindUserById { userId: number; } @@ -28,5 +28,5 @@ export interface IUsersServiceFindUser { export interface IUsersServiceImageUpload { file: Express.Multer.File; - resize: number; + userId: number; } diff --git a/src/APIs/users/services/users-create.service.ts b/src/APIs/users/services/users-create.service.ts index e4eb60c..3eb03d4 100644 --- a/src/APIs/users/services/users-create.service.ts +++ b/src/APIs/users/services/users-create.service.ts @@ -7,7 +7,7 @@ import { UsersRepository } from '../users.repository'; export class UsersCreateService { constructor(private readonly repo_users: UsersRepository) {} - async create({ userId }: IUsersServiceCreate) { + async createUser({ userId }: IUsersServiceCreate) { const userTempName = 'USER' + getUUID().substring(0, 8); const result = await this.repo_users.save({ id: userId, diff --git a/src/APIs/users/services/users-delete.service.ts b/src/APIs/users/services/users-delete.service.ts index b0fc42f..0525005 100644 --- a/src/APIs/users/services/users-delete.service.ts +++ b/src/APIs/users/services/users-delete.service.ts @@ -17,7 +17,11 @@ export class UsersDeleteService { private readonly repo_users: UsersRepository, private readonly db_dataSource: DataSource, ) {} - async delete({ userId, type, content }: IUsersServiceDelete): Promise { + async deleteUser({ + userId, + type, + content, + }: IUsersServiceDelete): Promise { const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); diff --git a/src/APIs/users/services/users-read.service.ts b/src/APIs/users/services/users-read.service.ts index 92fd258..0972bec 100644 --- a/src/APIs/users/services/users-read.service.ts +++ b/src/APIs/users/services/users-read.service.ts @@ -3,6 +3,10 @@ import { UsersRepository } from '../users.repository'; import { USER_SELECT_OPTION, UserDto } from '../dtos/common/user.dto'; import { User } from '../entities/user.entity'; import { UserFollowingResponseDto } from '../dtos/response/user-following-response.dto'; +import { + IUsersServiceFindUserByHandle, + IUsersServiceFindUserById, +} from '../interfaces/users.service.interface'; @Injectable() export class UsersReadService { @@ -18,7 +22,7 @@ export class UsersReadService { async findUserByIdWithDelete({ userId, - }: IUsersServiceFindUserByKakaoId): Promise { + }: IUsersServiceFindUserById): Promise { const result = await this.repo_users.findOne({ select: USER_SELECT_OPTION, where: { id: userId }, diff --git a/src/APIs/users/services/users-update.service.ts b/src/APIs/users/services/users-update.service.ts index 61df448..506713d 100644 --- a/src/APIs/users/services/users-update.service.ts +++ b/src/APIs/users/services/users-update.service.ts @@ -28,7 +28,12 @@ export class UsersUpdateService { }); } - async patchUser({ userId, handle, description, username }): Promise { + async updateUser({ + userId, + handle, + description, + username, + }): Promise { const user = await this.svc_usersValidate.existCheck({ userId }); if (description) { user.description = description; @@ -47,7 +52,7 @@ export class UsersUpdateService { } } - async uploadProfileImage({ + async updateProfileImage({ userId, file, }: IUsersServiceImageUpload): Promise { @@ -64,8 +69,8 @@ export class UsersUpdateService { await this.repo_users.save({ ...user, profileImage: imageUrl }); return { imageUrl }; } - - async uploadBackgroundImage({ + + async updateBackgroundImage({ userId, file, }: IUsersServiceImageUpload): Promise { @@ -82,3 +87,4 @@ export class UsersUpdateService { await this.repo_users.save({ ...user, backgroundImage: imageUrl }); return { imageUrl }; } +} diff --git a/src/APIs/users/users.controller.ts b/src/APIs/users/users.controller.ts deleted file mode 100644 index 169aa81..0000000 --- a/src/APIs/users/users.controller.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - HttpCode, - Param, - Patch, - Post, - Req, - Res, - UploadedFile, - UseGuards, - UseInterceptors, -} from '@nestjs/common'; -import { UsersService } from './users.service'; -import { - ApiBody, - ApiConsumes, - ApiCookieAuth, - ApiCreatedResponse, - ApiNoContentResponse, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { Request, Response } from 'express'; -import { - UserResponseDto, - UserResponseDtoWithFollowing, -} from './dtos/user-response.dto'; -import { PatchUserInput } from './dtos/patch-user.input'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; -import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { DeleteUserInput } from './dtos/delete-user.dto'; - -@ApiTags('유저 API') -@Controller('users') -export class UsersController { - constructor(private readonly usersService: UsersService) {} - - @ApiOperation({ - summary: '이름이 포함된 유저 검색', - description: '이름에 username이 포함된 유저를 검색한다.', - }) - @ApiOkResponse({ - description: '조회 성공', - type: [UserResponseDtoWithFollowing], - }) - @HttpCode(200) - @Get('username/:username') - async findUsersByName( - @Req() req: Request, - @Param('username') username: string, - ): Promise { - const kakaoId = req.user.userId; - return await this.usersService.findUsersByName({ kakaoId, username }); - } - - @ApiOperation({ - summary: '특정 유저 프로필 조회(id)', - description: 'id가 일치하는 유저 프로필을 조회한다.', - }) - @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) - @HttpCode(200) - @Get('profile/id/:userId') - async findUserByKakaoId( - @Param('userId') kakaoId: number, - ): Promise { - return await this.usersService.findUserByKakaoId({ kakaoId }); - } - - @ApiOperation({ - summary: '특정 유저 프로필 조회(handle)', - description: 'handle이 일치하는 유저 프로필을 조회한다.', - }) - @ApiOkResponse({ description: '조회 성공', type: UserResponseDto }) - @HttpCode(200) - @Get('profile/handle/:handle') - async findUserByHandle( - @Param('handle') handle: string, - ): Promise { - return await this.usersService.findUserByHandle({ handle }); - } - - @ApiOperation({ - summary: '로그인된 유저의 프로필 불러오기', - description: '로그인된 유저의 프로필을 불러온다.', - }) - @ApiCookieAuth() - @ApiOkResponse({ description: '불러오기 완료', type: UserResponseDto }) - @Get('me') - @UseGuards(AuthGuardV2) - @HttpCode(200) - async fetchUser(@Req() req: Request): Promise { - const kakaoId = req.user.userId; - return await this.usersService.findUserByKakaoId({ kakaoId }); - } - - @ApiOperation({ - summary: '로그인된 유저의 이름이나 설명, 핸들을 변경', - description: '로그인된 유저의 이름이나 설명, 핸들, 혹은 모두를 변경한다.', - }) - @ApiOkResponse({ description: '변경 성공', type: UserResponseDto }) - @ApiCookieAuth() - @Patch('me') - @HttpCode(200) - @UseGuards(AuthGuardV2) - async patchUser( - @Req() req: Request, - @Body() body: PatchUserInput, - ): Promise { - const kakaoId = req.user.userId; - const { description, username, handle } = body; - return await this.usersService.patchUser({ - kakaoId, - description, - username, - handle, - }); - } - - @ApiOperation({ - summary: '로그인된 유저의 프로필 이미지를 변경', - description: '스토리지에 프로필 사진을 업로드하고 변경한다.', - }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: '업로드 할 파일', - type: ImageUploadDto, - }) - @ApiCreatedResponse({ - description: '업로드 성공', - type: ImageUploadResponseDto, - }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @UseInterceptors(FileInterceptor('file')) - @HttpCode(201) - @Post('me/profile-image') - async uploadProfileImage( - @Req() req: Request, - @UploadedFile() file: Express.Multer.File, - ): Promise { - const userKakaoId = req.user.userId; - return await this.usersService.uploadProfileImage({ - userKakaoId, - file, - }); - } - - @ApiOperation({ - summary: '로그인된 유저의 배경 이미지를 변경', - description: '스토리지에 배경 사진을 업로드하고 변경한다.', - }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: '업로드 할 파일', - type: ImageUploadDto, - }) - @ApiCreatedResponse({ - description: '업로드 성공', - type: ImageUploadResponseDto, - }) - @UseGuards(AuthGuardV2) - @ApiCookieAuth() - @UseInterceptors(FileInterceptor('file')) - @HttpCode(201) - @Post('me/background-image') - async uploadBackgroundImage( - @Req() req: Request, - @UploadedFile() file: Express.Multer.File, - ): Promise { - const userKakaoId = req.user.userId; - return await this.usersService.uploadBackgroundImage({ - userKakaoId, - file, - }); - } - - @ApiOperation({ - summary: '회원 탈퇴(soft delete)', - description: '회원을 탈퇴하고 연동된 게시글과 댓글을 soft delete한다.', - }) - @ApiCookieAuth() - @UseGuards(AuthGuardV2) - @ApiNoContentResponse() - @HttpCode(204) - @Delete('me') - async deleteUser( - @Req() req: Request, - @Res() res: Response, - @Body() body: DeleteUserInput, - ) { - const kakaoId = req.user.userId; - const clientDomain = process.env.CLIENT_DOMAIN; - await this.usersService.delete({ kakaoId, ...body }); - res.clearCookie('accessToken', { - httpOnly: true, - domain: clientDomain, - sameSite: 'none', - secure: true, - }); - res.clearCookie('refreshToken', { - httpOnly: true, - domain: clientDomain, - sameSite: 'none', - secure: true, - }); - res.clearCookie('isLoggedIn', { httpOnly: false, domain: clientDomain }); - return res.send(); - } -} From aec35fd17bc4ee7a66fdad11d972dc591a0ba18c Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 19:21:03 +0900 Subject: [PATCH 207/236] refactor(users): reorganize module structure --- src/APIs/users/users.module.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index 7af453e..1a2ca04 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -1,15 +1,37 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; -import { UsersService } from './users.service'; -import { UsersController } from './users.controller'; import { UsersRepository } from './users.repository'; import { ImagesModule } from 'src/modules/images/images.module'; +import { UsersCreateService } from './services/users-create.service'; +import { UsersReadService } from './services/users-read.service'; +import { UsersUpdateService } from './services/users-update.service'; +import { UsersDeleteService } from './services/users-delete.service'; +import { UsersCreateController } from './controllers/users-create.controller'; +import { UsersReadController } from './controllers/users-read.controller'; +import { UsersUpdateController } from './controllers/users-update.controller'; +import { UsersDeleteController } from './controllers/users-delete.controller'; @Module({ imports: [TypeOrmModule.forFeature([User]), ImagesModule], - providers: [UsersService, UsersRepository], - controllers: [UsersController], - exports: [UsersService], + providers: [ + UsersCreateService, + UsersReadService, + UsersUpdateService, + UsersDeleteService, + UsersRepository, + ], + controllers: [ + UsersCreateController, + UsersReadController, + UsersUpdateController, + UsersDeleteController, + ], + exports: [ + UsersCreateService, + UsersReadService, + UsersUpdateService, + UsersDeleteService, + ], }) export class UsersModule {} From d04e2327f788bab89eb192acd9aadaa628e3952f Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 19:43:35 +0900 Subject: [PATCH 208/236] refactor(Agreement): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/agreements/agreements.controller.ts | 60 ++++++++++--------- src/APIs/agreements/agreements.repository.ts | 4 +- src/APIs/agreements/agreements.service.ts | 59 +++++++++--------- .../agreements/dtos/common/agreement.dto.ts | 4 ++ .../agreements/dtos/create-agreements.dto.ts | 11 ---- .../agreements/dtos/fetch-agreement.dto.ts | 4 -- .../agreements/dtos/fetch-contract.dto.ts | 4 -- .../agreements/dtos/patch-agreement.dto.ts | 4 -- .../request/agreement-create-request.dto.ts | 10 ++++ .../agreement-get-contract-request.dto.ts | 6 ++ .../request/agreement-patch-request.dto.ts | 6 ++ .../agreements.service.interface.ts | 23 +++---- 12 files changed, 99 insertions(+), 96 deletions(-) create mode 100644 src/APIs/agreements/dtos/common/agreement.dto.ts delete mode 100644 src/APIs/agreements/dtos/create-agreements.dto.ts delete mode 100644 src/APIs/agreements/dtos/fetch-agreement.dto.ts delete mode 100644 src/APIs/agreements/dtos/fetch-contract.dto.ts delete mode 100644 src/APIs/agreements/dtos/patch-agreement.dto.ts create mode 100644 src/APIs/agreements/dtos/request/agreement-create-request.dto.ts create mode 100644 src/APIs/agreements/dtos/request/agreement-get-contract-request.dto.ts create mode 100644 src/APIs/agreements/dtos/request/agreement-patch-request.dto.ts diff --git a/src/APIs/agreements/agreements.controller.ts b/src/APIs/agreements/agreements.controller.ts index da2fe01..9f5dc43 100644 --- a/src/APIs/agreements/agreements.controller.ts +++ b/src/APIs/agreements/agreements.controller.ts @@ -19,74 +19,78 @@ import { } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; -import { CreateAgreementsInput } from './dtos/create-agreements.dto'; -import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; -import { PatchAgreementInput } from './dtos/patch-agreement.dto'; -import { FetchContractDto } from './dtos/fetch-contract.dto'; +import { AgreementGetContractRequestDto } from './dtos/request/agreement-get-contract-request.dto'; +import { AgreementCreateRequestDto } from './dtos/request/agreement-create-request.dto'; +import { AgreementDto } from './dtos/common/agreement.dto'; +import { AgreementPatchRequestDto } from './dtos/request/agreement-patch-request.dto'; @ApiTags('유저 API') @Controller('users') export class AgreementsController { - constructor(private readonly agreementsService: AgreementsService) {} + constructor(private readonly svc_agreements: AgreementsService) {} @ApiOperation({ summary: 'contract fetch' }) @Get('contracts') - async fetchContract(@Query() query: FetchContractDto) { - const data = await this.agreementsService.fetchContract({ ...query }); + async getContract(@Query() query: AgreementGetContractRequestDto) { + const data = await this.svc_agreements.findContract({ ...query }); return data; } @ApiOperation({ summary: '온보딩 동의' }) @ApiCookieAuth() - @ApiCreatedResponse({ type: FetchAgreementDto }) + @ApiCreatedResponse({ type: AgreementDto }) @UseGuards(AuthGuardV2) @Post('me/agreement') async agree( @Req() req: Request, - @Body() body: CreateAgreementsInput, - ): Promise { - const kakaoId = req.user.userId; - return await this.agreementsService.create({ ...body, kakaoId }); + @Body() body: AgreementCreateRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.svc_agreements.createAgreement({ ...body, userId }); } @ApiOperation({ summary: '로그인된 유저의 온보딩 동의 내용들을 fetch' }) @ApiCookieAuth() - @ApiOkResponse({ type: [FetchAgreementDto] }) + @ApiOkResponse({ type: [AgreementDto] }) @UseGuards(AuthGuardV2) @Get('me/agreements') - async fetchAgreements(@Req() req: Request): Promise { - const kakaoId = req.user.userId; - return await this.agreementsService.fetchAll({ kakaoId }); + async fetchAgreements(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.svc_agreements.findAgreements({ userId }); } @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 특정 유저의 온보딩 동의 내용을 조회' }) @ApiCookieAuth() - @ApiOkResponse({ type: [FetchAgreementDto] }) + @ApiOkResponse({ type: [AgreementDto] }) @UseGuards(AuthGuardV2) @Get('admin/:userId/agreements') async fetchAgreementAdmin( @Req() req: Request, @Param('userId') targetUserKakaoId: number, - ): Promise { - const kakaoId = req.user.userId; - await this.agreementsService.adminCheck({ kakaoId }); - return await this.agreementsService.fetchAll({ - kakaoId: targetUserKakaoId, + ): Promise { + const userId = req.user.userId; + await this.svc_agreements.adminCheck({ userId }); + return await this.svc_agreements.findAgreements({ + userId: targetUserKakaoId, }); } @ApiOperation({ summary: '동의 여부를 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: FetchAgreementDto }) + @ApiOkResponse({ type: AgreementDto }) @UseGuards(AuthGuardV2) @Patch('me/agreement/:agreementId') async patchAgreement( @Req() req: Request, - @Param('agreementId') id: number, - @Body() body: PatchAgreementInput, - ): Promise { - const userKakaoId = req.user.userId; - return await this.agreementsService.patch({ ...body, id, userKakaoId }); + @Param('agreementId') agreementId: number, + @Body() body: AgreementPatchRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.svc_agreements.patchAgreement({ + ...body, + agreementId, + userId, + }); } } diff --git a/src/APIs/agreements/agreements.repository.ts b/src/APIs/agreements/agreements.repository.ts index 085f061..b2a30cb 100644 --- a/src/APIs/agreements/agreements.repository.ts +++ b/src/APIs/agreements/agreements.repository.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AgreementsRepository extends Repository { - constructor(private dataSource: DataSource) { - super(Agreement, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(Agreement, db_dataSource.createEntityManager()); } } diff --git a/src/APIs/agreements/agreements.service.ts b/src/APIs/agreements/agreements.service.ts index b27fb31..c70ffd5 100644 --- a/src/APIs/agreements/agreements.service.ts +++ b/src/APIs/agreements/agreements.service.ts @@ -8,38 +8,38 @@ import { IAgreementsServiceCreate, IAgreementsServiceFetchContract, IAgreementsServiceId, - IAgreementsServiceKakaoId, - IAgreementsServicePatch, + IAgreementsServicePatchAgreement, + IAgreementsServiceUserId, } from './interfaces/agreements.service.interface'; -import { FetchAgreementDto } from './dtos/fetch-agreement.dto'; -import { UsersService } from '../users/users.service'; import path from 'path'; import fs from 'fs'; +import { UsersValidateService } from '../users/services/users-validate-service'; +import { AgreementDto } from './dtos/common/agreement.dto'; @Injectable() export class AgreementsService { constructor( - private readonly agreementsRepository: AgreementsRepository, - private readonly usersService: UsersService, + private readonly repo_agreements: AgreementsRepository, + private readonly svc_usersValidate: UsersValidateService, ) {} - async adminCheck({ kakaoId }: IAgreementsServiceKakaoId): Promise { - await this.usersService.adminCheck({ kakaoId }); + async adminCheck({ userId }: IAgreementsServiceUserId): Promise { + await this.svc_usersValidate.adminCheck({ userId }); } - async create({ - kakaoId, + async createAgreement({ + userId, agreementType, isAgreed, - }: IAgreementsServiceCreate): Promise { - return await this.agreementsRepository.save({ + }: IAgreementsServiceCreate): Promise { + return await this.repo_agreements.save({ agreementType, isAgreed, - userKakaoId: kakaoId, + userId, }); } - async fetchContract({ agreementType }: IAgreementsServiceFetchContract) { + async findContract({ agreementType }: IAgreementsServiceFetchContract) { const fileName = agreementType + '.html'; const rootPath = process.cwd(); const filePath = path.join(rootPath, 'src', 'assets', 'terms', fileName); @@ -47,29 +47,30 @@ export class AgreementsService { return data; } - async fetchOne({ id }: IAgreementsServiceId): Promise { - return await this.agreementsRepository.findOne({ where: { id } }); + async findAgreement({ + agreementId, + }: IAgreementsServiceId): Promise { + return await this.repo_agreements.findOne({ where: { id: agreementId } }); } - async fetchAll({ - kakaoId, - }: IAgreementsServiceKakaoId): Promise { - return await this.agreementsRepository.find({ - where: { user: { kakaoId } }, + async findAgreements({ + userId, + }: IAgreementsServiceUserId): Promise { + return await this.repo_agreements.find({ + where: { user: { id: userId } }, }); } - async patch({ - userKakaoId, - id, + async patchAgreement({ + userId, + agreementId, isAgreed, - }: IAgreementsServicePatch): Promise { - const data = await this.fetchOne({ id }); + }: IAgreementsServicePatchAgreement): Promise { + const data = await this.findAgreement({ agreementId }); if (!data) throw new NotFoundException('데이터를 찾을 수 없습니다.'); - if (data.userKakaoId != userKakaoId) - throw new ForbiddenException('권한이 없습니다.'); + if (data.userId != userId) throw new ForbiddenException('권한이 없습니다.'); // if(data.agreementType != AgreementType.MARKETING_CONSENT) data.isAgreed = isAgreed; - return await this.agreementsRepository.save(data); + return await this.repo_agreements.save(data); } } diff --git a/src/APIs/agreements/dtos/common/agreement.dto.ts b/src/APIs/agreements/dtos/common/agreement.dto.ts new file mode 100644 index 0000000..9216c4e --- /dev/null +++ b/src/APIs/agreements/dtos/common/agreement.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Agreement } from '../../entities/agreement.entity'; + +export class AgreementDto extends OmitType(Agreement, ['user'] as const) {} diff --git a/src/APIs/agreements/dtos/create-agreements.dto.ts b/src/APIs/agreements/dtos/create-agreements.dto.ts deleted file mode 100644 index bcdc0f0..0000000 --- a/src/APIs/agreements/dtos/create-agreements.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Agreement } from '../entities/agreement.entity'; - -export class CreateAgreementsInput extends OmitType(Agreement, [ - 'id', - 'user', - 'userKakaoId', - 'date_created', - 'date_deleted', - 'date_updated', -]) {} diff --git a/src/APIs/agreements/dtos/fetch-agreement.dto.ts b/src/APIs/agreements/dtos/fetch-agreement.dto.ts deleted file mode 100644 index a45445d..0000000 --- a/src/APIs/agreements/dtos/fetch-agreement.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Agreement } from '../entities/agreement.entity'; - -export class FetchAgreementDto extends OmitType(Agreement, ['user']) {} diff --git a/src/APIs/agreements/dtos/fetch-contract.dto.ts b/src/APIs/agreements/dtos/fetch-contract.dto.ts deleted file mode 100644 index 861da4a..0000000 --- a/src/APIs/agreements/dtos/fetch-contract.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { Agreement } from '../entities/agreement.entity'; - -export class FetchContractDto extends PickType(Agreement, ['agreementType']) {} diff --git a/src/APIs/agreements/dtos/patch-agreement.dto.ts b/src/APIs/agreements/dtos/patch-agreement.dto.ts deleted file mode 100644 index 7f3bd1e..0000000 --- a/src/APIs/agreements/dtos/patch-agreement.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { Agreement } from '../entities/agreement.entity'; - -export class PatchAgreementInput extends PickType(Agreement, ['isAgreed']) {} diff --git a/src/APIs/agreements/dtos/request/agreement-create-request.dto.ts b/src/APIs/agreements/dtos/request/agreement-create-request.dto.ts new file mode 100644 index 0000000..b3c78bd --- /dev/null +++ b/src/APIs/agreements/dtos/request/agreement-create-request.dto.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { AgreementDto } from '../common/agreement.dto'; + +export class AgreementCreateRequestDto extends OmitType(AgreementDto, [ + 'id', + 'userId', + 'dateCreated', + 'dateUpdated', + 'dateDeleted', +] as const) {} diff --git a/src/APIs/agreements/dtos/request/agreement-get-contract-request.dto.ts b/src/APIs/agreements/dtos/request/agreement-get-contract-request.dto.ts new file mode 100644 index 0000000..856512b --- /dev/null +++ b/src/APIs/agreements/dtos/request/agreement-get-contract-request.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { AgreementDto } from '../common/agreement.dto'; + +export class AgreementGetContractRequestDto extends PickType(AgreementDto, [ + 'agreementType', +] as const) {} diff --git a/src/APIs/agreements/dtos/request/agreement-patch-request.dto.ts b/src/APIs/agreements/dtos/request/agreement-patch-request.dto.ts new file mode 100644 index 0000000..f7699ca --- /dev/null +++ b/src/APIs/agreements/dtos/request/agreement-patch-request.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { AgreementDto } from '../common/agreement.dto'; + +export class AgreementPatchRequestDto extends PickType(AgreementDto, [ + 'isAgreed', +]) {} diff --git a/src/APIs/agreements/interfaces/agreements.service.interface.ts b/src/APIs/agreements/interfaces/agreements.service.interface.ts index 6f170e4..e58e831 100644 --- a/src/APIs/agreements/interfaces/agreements.service.interface.ts +++ b/src/APIs/agreements/interfaces/agreements.service.interface.ts @@ -4,25 +4,20 @@ import { Agreement } from '../entities/agreement.entity'; export interface IAgreementsServiceCreate extends Omit< Agreement, - | 'id' - | 'user' - | 'userKakaoId' - | 'date_created' - | 'date_updated' - | 'date_deleted' - > { - kakaoId: number; -} + 'id' | 'user' | 'dateCreated' | 'dateUpdated' | 'dateDeleted' + > {} -export interface IAgreementsServicePatch - extends Pick {} +export interface IAgreementsServicePatchAgreement + extends Pick { + agreementId: number; +} -export interface IAgreementsServiceKakaoId { - kakaoId: number; +export interface IAgreementsServiceUserId { + userId: number; } export interface IAgreementsServiceId { - id: number; + agreementId: number; } export interface IAgreementsServiceFetchContract { From efafaf589a2d6b30f096121661472ffb4b88e2dc Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 19:53:06 +0900 Subject: [PATCH 209/236] refactor(Announcement): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- .../announcements/announcements.controller.ts | 58 ++++++++++------- .../announcements/announcements.service.ts | 62 ++++++++++--------- .../dtos/announcement-response.dto.ts | 3 - .../dtos/common/announcement.dto.ts | 4 ++ .../dtos/create-announcement.dto.ts | 9 --- .../dtos/patch-announcment.dto.ts | 6 -- .../announcement-create-request.dto.ts | 9 +++ .../request/announcement-patch-request.dto.ts | 6 ++ .../announcements.service.interface.ts | 18 +++--- 9 files changed, 95 insertions(+), 80 deletions(-) delete mode 100644 src/APIs/announcements/dtos/announcement-response.dto.ts create mode 100644 src/APIs/announcements/dtos/common/announcement.dto.ts delete mode 100644 src/APIs/announcements/dtos/create-announcement.dto.ts delete mode 100644 src/APIs/announcements/dtos/patch-announcment.dto.ts create mode 100644 src/APIs/announcements/dtos/request/announcement-create-request.dto.ts create mode 100644 src/APIs/announcements/dtos/request/announcement-patch-request.dto.ts diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index efec8e2..0a2f8cd 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -20,9 +20,9 @@ import { } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; -import { CreateAnouncementInput } from './dtos/create-announcement.dto'; -import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; -import { PatchAnnouncementInput } from './dtos/patch-announcment.dto'; +import { AnnouncementDto } from './dtos/common/announcement.dto'; +import { AnnouncementPatchRequestDto } from './dtos/request/announcement-patch-request.dto'; +import { AnnouncementCreateRequestDto } from './dtos/request/announcement-create-request.dto'; @ApiTags('공지 API') @Controller() @@ -33,37 +33,44 @@ export class AnnouncementsController { @ApiOperation({ summary: '[어드민용] 공지사항 작성' }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @ApiCreatedResponse({ type: AnnouncementResponseDto }) + @ApiCreatedResponse({ type: AnnouncementDto }) @Post('users/admin/anmts') @HttpCode(201) async createAnmt( @Req() req: Request, - @Body() body: CreateAnouncementInput, - ): Promise { - const kakaoId = req.user.userId; - return await this.announcementsService.create({ ...body, kakaoId }); + @Body() body: AnnouncementCreateRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.announcementsService.createAnnoucement({ + ...body, + userId, + }); } @ApiOperation({ summary: '공지사항 조회' }) - @ApiOkResponse({ type: [AnnouncementResponseDto] }) + @ApiOkResponse({ type: [AnnouncementDto] }) @Get('anmts') - async fetchAnmts(): Promise { - return await this.announcementsService.fetchAll(); + async fetchAnmts(): Promise { + return await this.announcementsService.getAnnouncements(); } @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 공지사항 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: [AnnouncementResponseDto] }) + @ApiOkResponse({ type: [AnnouncementDto] }) @UseGuards(AuthGuardV2) - @Patch('users/admin/anmts/:id') + @Patch('users/admin/anmts/:announcementId') async patchAnmt( @Req() req: Request, - @Body() body: PatchAnnouncementInput, - @Param('id') id: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.announcementsService.patch({ ...body, id, kakaoId }); + @Body() body: AnnouncementPatchRequestDto, + @Param('announcementId') announcementId: number, + ): Promise { + const userId = req.user.userId; + return await this.announcementsService.patchAnnouncement({ + ...body, + announcementId, + userId, + }); } @ApiTags('어드민 API') @@ -72,14 +79,17 @@ export class AnnouncementsController { description: 'id에 해당하는 공지사항 삭제, 삭제된 공지사항을 반환', }) @ApiCookieAuth() - @ApiOkResponse({ type: AnnouncementResponseDto }) + @ApiOkResponse({ type: AnnouncementDto }) @UseGuards(AuthGuardV2) - @Delete('users/admin/anmts/:id') + @Delete('users/admin/anmts/:announcementId') async removeAnmt( @Req() req: Request, - @Param('id') id: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.announcementsService.remove({ kakaoId, id }); + @Param('announcementId') announcementId: number, + ): Promise { + const userId = req.user.userId; + return await this.announcementsService.removeAnnouncement({ + userId, + announcementId, + }); } } diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index a25d76d..96c9493 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -3,56 +3,60 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Announcement } from './entities/announcement.entity'; import { Repository } from 'typeorm'; import { - IAnnouncementsSerciceCreate, - IAnnouncementsSercicePatch, - IAnnouncementsSerciceRemove, + IAnnouncementsSerciceCreateAnnouncement, + IAnnouncementsSercicePatchAnnouncement, + IAnnouncementsSerciceRemoveAnnouncement, } from './interfaces/announcements.service.interface'; -import { UsersService } from '../users/users.service'; -import { AnnouncementResponseDto } from './dtos/announcement-response.dto'; +import { UsersValidateService } from '../users/services/users-validate-service'; +import { AnnouncementDto } from './dtos/common/announcement.dto'; @Injectable() export class AnnouncementsService { constructor( @InjectRepository(Announcement) - private readonly annoucementsRepository: Repository, - private readonly usersService: UsersService, + private readonly repo_announcements: Repository, + private readonly svc_usersValidate: UsersValidateService, ) {} - async create({ - kakaoId, + async createAnnoucement({ + userId, title, content, - }: IAnnouncementsSerciceCreate): Promise { - await this.usersService.adminCheck({ kakaoId }); - return await this.annoucementsRepository.save({ title, content }); + }: IAnnouncementsSerciceCreateAnnouncement): Promise { + await this.svc_usersValidate.adminCheck({ userId }); + return await this.repo_announcements.save({ title, content }); } - async fetchAll(): Promise { - return await this.annoucementsRepository.find(); + async getAnnouncements(): Promise { + return await this.repo_announcements.find(); } - async patch({ - kakaoId, - id, + async patchAnnouncement({ + userId, + announcementId, title, content, - }: IAnnouncementsSercicePatch): Promise { - await this.usersService.adminCheck({ kakaoId }); - const anmt = await this.annoucementsRepository.findOne({ where: { id } }); + }: IAnnouncementsSercicePatchAnnouncement): Promise { + await this.svc_usersValidate.adminCheck({ userId }); + const anmt = await this.repo_announcements.findOne({ + where: { id: announcementId }, + }); if (!anmt) throw new NotFoundException('공지를 찾을 수 없습니다.'); if (title) anmt.title = title; if (content) anmt.content = content; - await this.annoucementsRepository.save(anmt); - return await this.annoucementsRepository.find({ where: { id: anmt.id } }); + await this.repo_announcements.save(anmt); + return await this.repo_announcements.find({ where: { id: anmt.id } }); } - async remove({ - kakaoId, - id, - }: IAnnouncementsSerciceRemove): Promise { - await this.usersService.adminCheck({ kakaoId }); - const anmt = await this.annoucementsRepository.findOne({ where: { id } }); + async removeAnnouncement({ + userId, + announcementId, + }: IAnnouncementsSerciceRemoveAnnouncement): Promise { + await this.svc_usersValidate.adminCheck({ userId }); + const anmt = await this.repo_announcements.findOne({ + where: { id: announcementId }, + }); if (!anmt) throw new NotFoundException('공지를 찾을 수 없습니다.'); - return await this.annoucementsRepository.softRemove(anmt); + return await this.repo_announcements.softRemove(anmt); } } diff --git a/src/APIs/announcements/dtos/announcement-response.dto.ts b/src/APIs/announcements/dtos/announcement-response.dto.ts deleted file mode 100644 index 64e75a7..0000000 --- a/src/APIs/announcements/dtos/announcement-response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Announcement } from '../entities/announcement.entity'; - -export class AnnouncementResponseDto extends Announcement {} diff --git a/src/APIs/announcements/dtos/common/announcement.dto.ts b/src/APIs/announcements/dtos/common/announcement.dto.ts new file mode 100644 index 0000000..c229f40 --- /dev/null +++ b/src/APIs/announcements/dtos/common/announcement.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Announcement } from '../../entities/announcement.entity'; + +export class AnnouncementDto extends OmitType(Announcement, [] as const) {} diff --git a/src/APIs/announcements/dtos/create-announcement.dto.ts b/src/APIs/announcements/dtos/create-announcement.dto.ts deleted file mode 100644 index 4034290..0000000 --- a/src/APIs/announcements/dtos/create-announcement.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Announcement } from '../entities/announcement.entity'; - -export class CreateAnouncementInput extends OmitType(Announcement, [ - 'date_created', - 'date_deleted', - 'date_updated', - 'id', -]) {} diff --git a/src/APIs/announcements/dtos/patch-announcment.dto.ts b/src/APIs/announcements/dtos/patch-announcment.dto.ts deleted file mode 100644 index 42ced37..0000000 --- a/src/APIs/announcements/dtos/patch-announcment.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateAnouncementInput } from './create-announcement.dto'; - -export class PatchAnnouncementInput extends PartialType( - CreateAnouncementInput, -) {} diff --git a/src/APIs/announcements/dtos/request/announcement-create-request.dto.ts b/src/APIs/announcements/dtos/request/announcement-create-request.dto.ts new file mode 100644 index 0000000..b36b5ae --- /dev/null +++ b/src/APIs/announcements/dtos/request/announcement-create-request.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from '@nestjs/swagger'; +import { AnnouncementDto } from '../common/announcement.dto'; + +export class AnnouncementCreateRequestDto extends OmitType(AnnouncementDto, [ + 'dateCreated', + 'dateDeleted', + 'dateUpdated', + 'id', +] as const) {} diff --git a/src/APIs/announcements/dtos/request/announcement-patch-request.dto.ts b/src/APIs/announcements/dtos/request/announcement-patch-request.dto.ts new file mode 100644 index 0000000..dc3c4f1 --- /dev/null +++ b/src/APIs/announcements/dtos/request/announcement-patch-request.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { AnnouncementCreateRequestDto } from './announcement-create-request.dto'; + +export class AnnouncementPatchRequestDto extends PartialType( + AnnouncementCreateRequestDto, +) {} diff --git a/src/APIs/announcements/interfaces/announcements.service.interface.ts b/src/APIs/announcements/interfaces/announcements.service.interface.ts index 74906c2..aedbf0c 100644 --- a/src/APIs/announcements/interfaces/announcements.service.interface.ts +++ b/src/APIs/announcements/interfaces/announcements.service.interface.ts @@ -1,21 +1,21 @@ import { Announcement } from '../entities/announcement.entity'; -export interface IAnnouncementsSerciceCreate +export interface IAnnouncementsSerciceCreateAnnouncement extends Omit< Announcement, - 'id' | 'date_created' | 'date_updated' | 'date_deleted' + 'id' | 'dateCreated' | 'dateUpdated' | 'dateDeleted' > { - kakaoId: number; + userId: number; } -export interface IAnnouncementsSerciceRemove { - kakaoId: number; - id: number; +export interface IAnnouncementsSerciceRemoveAnnouncement { + userId: number; + announcementId: number; } -export interface IAnnouncementsSercicePatch { - kakaoId: number; - id: number; +export interface IAnnouncementsSercicePatchAnnouncement { + userId: number; + announcementId: number; title?: string; content?: string; } From f7604ce34d962d7584a531267d49a65bdc787b53 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 20:02:40 +0900 Subject: [PATCH 210/236] refactor(ArticleBackground): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- .../articleBackgrounds.controller.ts | 24 +++++----- .../articleBackgrounds.module.ts | 9 +--- .../articleBackgrounds.service.ts | 44 ++++++++----------- .../dtos/common/articleBackground.dto.ts | 6 +++ 4 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 src/APIs/articleBackgrounds/dtos/common/articleBackground.dto.ts diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index 08b7591..5337c80 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -17,10 +17,11 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from '../../modules/images/dtos/image-upload-request.dto'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { ArticleBackground } from './entities/articleBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; +import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; +import { ArticleBackgroundDto } from './dtos/common/articleBackground.dto'; @Controller('') export class ArticleBackgroundsController { @@ -33,7 +34,7 @@ export class ArticleBackgroundsController { @ApiConsumes('multipart/form-data') @ApiBody({ description: '업로드 할 파일', - type: ImageUploadDto, + type: ImageUploadRequestDto, }) @ApiCreatedResponse({ description: '이미지 서버에 파일 업로드 완료', @@ -42,10 +43,11 @@ export class ArticleBackgroundsController { @Post('users/admin/article/background') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) - async uploadImage( + async createArticleBackground( @UploadedFile() file: Express.Multer.File, ): Promise { - const url = await this.articleBackgroundsService.imageUpload(file); + const url = + await this.articleBackgroundsService.createArticleBackground(file); return url; } @@ -56,14 +58,16 @@ export class ArticleBackgroundsController { type: [ArticleBackground], }) @Get('article/backgrounds') - async fetchAll(): Promise { - return await this.articleBackgroundsService.fetchAll(); + async getArticleBackgrounds(): Promise { + return await this.articleBackgroundsService.findArticleBackgrounds(); } @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) - @Delete('users/admin/article/background/:id') - async delete(@Param('id') id: string) { - return await this.articleBackgroundsService.delete({ id }); + @Delete('users/admin/article/background/:articleId') + async delete(@Param('articleId') articleId: string) { + return await this.articleBackgroundsService.deleteArticleBackground({ + articleId, + }); } } diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts index 3db029b..f8873b3 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts @@ -1,18 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtStrategy } from '../auth/strategies/jwt.strategy'; -import { UtilsModule } from 'src/modules/utils/utils.module'; import { ArticleBackgroundsController } from './articleBackgrounds.controller'; -import { AwsModule } from 'src/modules/aws/aws.module'; import { ArticleBackgroundsService } from './articleBackgrounds.service'; import { ArticleBackground } from './entities/articleBackground.entity'; +import { ImagesModule } from 'src/modules/images/images.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([ArticleBackground]), - UtilsModule, - AwsModule, - ], + imports: [TypeOrmModule.forFeature([ArticleBackground]), ImagesModule], providers: [JwtStrategy, ArticleBackgroundsService], controllers: [ArticleBackgroundsController], }) diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts index d728da4..7c6d6a1 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -1,46 +1,40 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; - -import { UtilsService } from 'src/modules/utils/utils.service'; import { ArticleBackground } from './entities/articleBackground.entity'; import { Repository } from 'typeorm'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; -import { AwsService } from 'src/modules/aws/aws.service'; +import { ImagesService } from 'src/modules/images/images.service'; +import { ArticleBackgroundDto } from './dtos/common/articleBackground.dto'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; @Injectable() export class ArticleBackgroundsService { constructor( - private readonly awsService: AwsService, - private readonly utilsService: UtilsService, @InjectRepository(ArticleBackground) - private readonly articleBackgroundsRepository: Repository, + private readonly repo_articleBackgrounds: Repository, + private readonly svc_images: ImagesService, ) {} - async saveImage(file: Express.Multer.File): Promise { - return await this.imageUpload(file); - } - async imageUpload( + async createArticleBackground( file: Express.Multer.File, ): Promise { - const imageName = this.utilsService.getUUID(); - const ext = file.originalname.split('.').pop(); - - const imageUrl = await this.awsService.imageUploadToS3( - `${imageName}.${ext}`, + const { imageUrl } = await this.svc_images.imageUpload({ file, - ext, - 2000, - ); - await this.articleBackgroundsRepository.save({ imageUrl }); + resize: 2000, + ext: 'png', + }); + await this.repo_articleBackgrounds.save({ imageUrl }); return { imageUrl }; } - async fetchAll(): Promise { - return await this.articleBackgroundsRepository.find(); + async findArticleBackgrounds(): Promise { + return await this.repo_articleBackgrounds.find(); } - async delete({ id }) { - // s3 서버에서 이미지 삭제하는 것까지 구현하기!! - return await this.articleBackgroundsRepository.delete({ id }); + async deleteArticleBackground({ articleId }) { + const articleBackground = await this.repo_articleBackgrounds.findOne({ + where: { id: articleId }, + }); + await this.repo_articleBackgrounds.remove(articleBackground); + await this.svc_images.deleteImage({ url: articleBackground.imageUrl }); } } diff --git a/src/APIs/articleBackgrounds/dtos/common/articleBackground.dto.ts b/src/APIs/articleBackgrounds/dtos/common/articleBackground.dto.ts new file mode 100644 index 0000000..1137750 --- /dev/null +++ b/src/APIs/articleBackgrounds/dtos/common/articleBackground.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { ArticleBackground } from '../../entities/articleBackground.entity'; + +export class ArticleBackgroundDto extends OmitType(ArticleBackground, [ + 'articles', +]) {} From 79167e3a3e14d8300fc5532b817876c30759f7a6 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 20:15:43 +0900 Subject: [PATCH 211/236] refactor(ArticleCategory): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- .../articleCategories.controller.ts | 83 ++++++++++--------- .../articleCategories.repository.ts | 10 +-- .../articleCategories.service.ts | 74 +++++++++-------- .../dtos/{ => common}/articleCategory.dto.ts | 2 +- .../dtos/create-articleCategory.dto.ts | 8 -- .../dtos/patch-articleCategory.dto.ts | 6 -- .../articleCategory-create-request.dto.ts | 7 ++ .../articleCategory-patch-request.dto.ts | 7 ++ .../articleCategories-response.dto.ts} | 4 +- 9 files changed, 104 insertions(+), 97 deletions(-) rename src/APIs/articleCategories/dtos/{ => common}/articleCategory.dto.ts (66%) delete mode 100644 src/APIs/articleCategories/dtos/create-articleCategory.dto.ts delete mode 100644 src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts create mode 100644 src/APIs/articleCategories/dtos/request/articleCategory-create-request.dto.ts create mode 100644 src/APIs/articleCategories/dtos/request/articleCategory-patch-request.dto.ts rename src/APIs/articleCategories/dtos/{fetch-articleCategory.dto.ts => response/articleCategories-response.dto.ts} (55%) diff --git a/src/APIs/articleCategories/articleCategories.controller.ts b/src/APIs/articleCategories/articleCategories.controller.ts index 36f5ade..514132b 100644 --- a/src/APIs/articleCategories/articleCategories.controller.ts +++ b/src/APIs/articleCategories/articleCategories.controller.ts @@ -19,22 +19,17 @@ import { } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { - FetchArticleCategoriesResponse, - FetchArticleCategoryResponse, -} from './dtos/fetch-articleCategory.dto'; -import { - CreateArticleCategoryInput, - CreateArticleCategoryResponse, -} from './dtos/create-articleCategory.dto'; import { ArticleCategoriesService } from './articleCategories.service'; -import { PatchArticleCategoryInput } from './dtos/patch-articleCategory.dto'; +import { ArticleCategoriesResponseDto } from './dtos/response/articleCategories-response.dto'; +import { ArticleCategoryDto } from './dtos/common/articleCategory.dto'; +import { ArticleCategoryCreateRequestDto } from './dtos/request/articleCategory-create-request.dto'; +import { ArticleCategoryPatchRequestDto } from './dtos/request/articleCategory-patch-request.dto'; @ApiTags('유저 API') @Controller('users') export class ArticleCategoriesController { constructor( - private readonly articleCategoriesService: ArticleCategoriesService, + private readonly svc_articleCategories: ArticleCategoriesService, ) {} @ApiOperation({ @@ -43,18 +38,18 @@ export class ArticleCategoriesController { '특정 유저가 생성한 카테고리의 이름과 id, 게시글 개수를 조회한다.', }) @ApiOkResponse({ - type: [FetchArticleCategoriesResponse], + type: [ArticleCategoriesResponseDto], }) @Get(':userId/categories') @HttpCode(200) async fetchArticleCategories( @Req() req: Request, - @Param('userId') targetKakaoId: number, - ): Promise { - const kakaoId = req.user.userId; - return await this.articleCategoriesService.fetchAll({ - kakaoId, - targetKakaoId, + @Param('userId') targetUserId: number, + ): Promise { + const userId = req.user.userId; + return await this.svc_articleCategories.fetchAll({ + userId, + targetUserId, }); } @@ -62,13 +57,15 @@ export class ArticleCategoriesController { summary: '특정 카테고리 조회', description: 'id에 해당하는 카테고리를 조회한다.', }) - @ApiOkResponse({ type: FetchArticleCategoryResponse }) - @Get('categories/:categoryId') + @ApiOkResponse({ type: ArticleCategoryDto }) + @Get('categories/:articleCategoryId') async fetchMyCategory( @Req() req: Request, - @Param('categoryId') id: string, - ): Promise { - return await this.articleCategoriesService.findWithId({ id }); + @Param('articleCategoryId') articleCategoryId: string, + ): Promise { + return await this.svc_articleCategories.findArticleCategoryById({ + articleCategoryId, + }); } @ApiOperation({ @@ -78,34 +75,37 @@ export class ArticleCategoriesController { @ApiCookieAuth() @ApiCreatedResponse({ description: '카테고리 생성 완료', - type: CreateArticleCategoryResponse, + type: ArticleCategoryDto, }) @UseGuards(AuthGuardV2) @Post('me/categories') @HttpCode(201) async createArticleCategory( @Req() req: Request, - @Body() body: CreateArticleCategoryInput, - ): Promise { - const kakaoId = req.user.userId; + @Body() body: ArticleCategoryCreateRequestDto, + ): Promise { + const userId = req.user.userId; const name = body.name; - return await this.articleCategoriesService.create({ kakaoId, name }); + return await this.svc_articleCategories.createArticleCategory({ + userId, + name, + }); } @ApiOperation({ summary: '로그인된 유저의 특정 카테고리 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: FetchArticleCategoryResponse }) + @ApiOkResponse({ type: ArticleCategoryDto }) @UseGuards(AuthGuardV2) @Patch('me/categories/:categoryId') async patchArticleCategory( @Req() req: Request, - @Param('categoryId') id: string, - @Body() body: PatchArticleCategoryInput, - ): Promise { - const kakaoId = req.user.userId; - return await this.articleCategoriesService.patch({ - kakaoId, - id, + @Param('articleCategoryId') articleCategoryId: string, + @Body() body: ArticleCategoryPatchRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.svc_articleCategories.patchArticleCategory({ + userId, + articleCategoryId, ...body, }); } @@ -113,16 +113,19 @@ export class ArticleCategoriesController { @ApiOperation({ summary: '유저의 지정 카테고리 삭제하기', description: - '로그인된 유저의 카테고리 중 categoryId 일치하는 카테고리를 삭제한다', + '로그인된 유저의 카테고리 중 articleCategoryId 일치하는 카테고리를 삭제한다', }) @ApiCookieAuth() - @Delete('me/categories/:categoryId') + @Delete('me/categories/:articleCategoryId') @UseGuards(AuthGuardV2) async deleteArticleCategory( @Req() req: Request, - @Param('categoryId') id: string, + @Param('articleCategoryId') articleCategoryId: string, ) { - const kakaoId = req.user.userId; - return await this.articleCategoriesService.delete({ kakaoId, id }); + const userId = req.user.userId; + return await this.svc_articleCategories.deleteArticleCategory({ + userId, + articleCategoryId, + }); } } diff --git a/src/APIs/articleCategories/articleCategories.repository.ts b/src/APIs/articleCategories/articleCategories.repository.ts index d09fa4b..c421f1b 100644 --- a/src/APIs/articleCategories/articleCategories.repository.ts +++ b/src/APIs/articleCategories/articleCategories.repository.ts @@ -4,11 +4,11 @@ import { ArticleCategory } from './entities/articleCategory.entity'; @Injectable() export class ArticleCategoriesRepository extends Repository { - constructor(private dataSource: DataSource) { - super(ArticleCategory, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(ArticleCategory, db_dataSource.createEntityManager()); } - async fetchUserCategory({ scope, userKakaoId }) { + async fetchUserCategory({ scope, userId }) { const query = this.createQueryBuilder('pc') .select([ 'COALESCE(COUNT(p.id), 0) as postCount', // postCategory당 posts의 개수를 집계 @@ -21,9 +21,9 @@ export class ArticleCategoriesRepository extends Repository { 'p.scope IN (:scope) AND p.isPublished = true', { scope }, ) // LEFT JOIN으로 연결된 엔티티의 조건을 추가 - .where('pc.userKakaoId = :userKakaoId', { userKakaoId }) + .where('pc.userKakaoId = :userKakaoId', { userId }) .groupBy('pc.id'); // postCategory.id를 기준으로 그룹화 - console.log('??', userKakaoId); + console.log('??', userId); return await query.getRawMany(); } diff --git a/src/APIs/articleCategories/articleCategories.service.ts b/src/APIs/articleCategories/articleCategories.service.ts index 5416ee7..122367b 100644 --- a/src/APIs/articleCategories/articleCategories.service.ts +++ b/src/APIs/articleCategories/articleCategories.service.ts @@ -6,69 +6,75 @@ import { } from '@nestjs/common'; import { FollowsService } from '../follows/follows.service'; import { ArticleCategoriesRepository } from './articleCategories.repository'; -import { CreateArticleCategoryResponse } from './dtos/create-articleCategory.dto'; -import { - FetchArticleCategoriesResponse, - FetchArticleCategoryResponse, -} from './dtos/fetch-articleCategory.dto'; +import { ArticleCategoryDto } from './dtos/common/articleCategory.dto'; +import { ArticleCategoriesResponseDto } from './dtos/response/articleCategories-response.dto'; @Injectable() export class ArticleCategoriesService { constructor( - private readonly followsService: FollowsService, - private readonly articleCategoryRepository: ArticleCategoriesRepository, + private readonly svc_follows: FollowsService, + private readonly repo_articleCategories: ArticleCategoriesRepository, ) {} - async findWithName({ kakaoId, name }) { - return await this.articleCategoryRepository.find({ - where: { user: { kakaoId }, name }, + async findArticleCategoryByName({ + userId, + name, + }): Promise { + return await this.repo_articleCategories.findOne({ + where: { user: { id: userId }, name }, }); } - async create({ kakaoId, name }): Promise { - const data = await this.findWithName({ kakaoId, name }); - if (data.length > 0) { + async createArticleCategory({ userId, name }): Promise { + const articleData = await this.findArticleCategoryByName({ userId, name }); + if (articleData) { throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); } - const result = await this.articleCategoryRepository.save({ - user: { kakaoId }, + const result = await this.repo_articleCategories.save({ + user: { id: userId }, name, }); return result; } - async patch({ kakaoId, id, name }): Promise { - const data = await this.findWithId({ id }); + async patchArticleCategory({ + userId, + articleCategoryId, + name, + }): Promise { + const data = await this.findArticleCategoryById({ articleCategoryId }); if (!data) throw new NotFoundException('카테고리를 찾을 수 없습니다.'); - if (data.userId != kakaoId) + if (data.userId != userId) throw new ForbiddenException('카테고리를 수정할 권한이 없습니다.'); data.name = name; - return await this.articleCategoryRepository.save(data); + return await this.repo_articleCategories.save(data); } - async findWithId({ id }): Promise { - return await this.articleCategoryRepository.findOne({ - where: { id }, + async findArticleCategoryById({ + articleCategoryId, + }): Promise { + return await this.repo_articleCategories.findOne({ + where: { id: articleCategoryId }, }); } async fetchAll({ - kakaoId, - targetKakaoId, - }): Promise { - const scope = await this.followsService.getScope({ - from_user: targetKakaoId, - to_user: kakaoId, + userId, + targetUserId, + }): Promise { + const scope = await this.svc_follows.getScope({ + fromUser: targetUserId, + toUser: userId, }); - return await this.articleCategoryRepository.fetchUserCategory({ - userKakaoId: targetKakaoId, + return await this.repo_articleCategories.fetchUserCategory({ + userId: targetUserId, scope, }); } - delete({ kakaoId, id }) { - return this.articleCategoryRepository.delete({ - id, - user: { kakaoId }, + deleteArticleCategory({ userId, articleCategoryId }) { + return this.repo_articleCategories.delete({ + id: articleCategoryId, + user: { id: userId }, }); } } diff --git a/src/APIs/articleCategories/dtos/articleCategory.dto.ts b/src/APIs/articleCategories/dtos/common/articleCategory.dto.ts similarity index 66% rename from src/APIs/articleCategories/dtos/articleCategory.dto.ts rename to src/APIs/articleCategories/dtos/common/articleCategory.dto.ts index fdb543f..7f3672c 100644 --- a/src/APIs/articleCategories/dtos/articleCategory.dto.ts +++ b/src/APIs/articleCategories/dtos/common/articleCategory.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { ArticleCategory } from '../entities/articleCategory.entity'; +import { ArticleCategory } from '../../entities/articleCategory.entity'; export class ArticleCategoryDto extends OmitType(ArticleCategory, [ 'user', diff --git a/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts deleted file mode 100644 index 45cc12f..0000000 --- a/src/APIs/articleCategories/dtos/create-articleCategory.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { ArticleCategoryDto } from './articleCategory.dto'; - -export class CreateArticleCategoryInput extends PickType(ArticleCategoryDto, [ - 'name', -]) {} - -export class CreateArticleCategoryResponse extends ArticleCategoryDto {} diff --git a/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts deleted file mode 100644 index 57c52be..0000000 --- a/src/APIs/articleCategories/dtos/patch-articleCategory.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { ArticleCategoryDto } from './articleCategory.dto'; - -export class PatchArticleCategoryInput extends PickType(ArticleCategoryDto, [ - 'name', -]) {} diff --git a/src/APIs/articleCategories/dtos/request/articleCategory-create-request.dto.ts b/src/APIs/articleCategories/dtos/request/articleCategory-create-request.dto.ts new file mode 100644 index 0000000..5b8bf74 --- /dev/null +++ b/src/APIs/articleCategories/dtos/request/articleCategory-create-request.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ArticleCategoryDto } from '../common/articleCategory.dto'; + +export class ArticleCategoryCreateRequestDto extends PickType( + ArticleCategoryDto, + ['name'] as const, +) {} diff --git a/src/APIs/articleCategories/dtos/request/articleCategory-patch-request.dto.ts b/src/APIs/articleCategories/dtos/request/articleCategory-patch-request.dto.ts new file mode 100644 index 0000000..ae89b53 --- /dev/null +++ b/src/APIs/articleCategories/dtos/request/articleCategory-patch-request.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ArticleCategoryDto } from '../common/articleCategory.dto'; + +export class ArticleCategoryPatchRequestDto extends PickType( + ArticleCategoryDto, + ['name'] as const, +) {} diff --git a/src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts similarity index 55% rename from src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts rename to src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts index 71c582a..87fec79 100644 --- a/src/APIs/articleCategories/dtos/fetch-articleCategory.dto.ts +++ b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts @@ -1,8 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ArticleCategoryDto } from './articleCategory.dto'; -export class FetchArticleCategoryResponse extends ArticleCategoryDto {} -export class FetchArticleCategoriesResponse { +export class ArticleCategoriesResponseDto { @ApiProperty({ type: Number }) postCount: number; From fbc5f81e7477cc5740971b53f9b95d139701316b Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 21:02:23 +0900 Subject: [PATCH 212/236] refactor(Notification): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/follows/follows.repository.ts | 14 +- src/APIs/follows/follows.service.ts | 127 +++++++++--------- .../follows.repository.interface.ts | 4 +- .../interfaces/follows.service.interface.ts | 8 +- .../dtos/common/notification.dto.ts | 8 ++ src/APIs/notifications/dtos/emit-noti.dto.ts | 15 --- src/APIs/notifications/dtos/fetch-noti.dto.ts | 40 ------ .../request/notification-emit-request.dto.ts | 8 ++ .../request/notifications-get-request.dto.ts | 23 ++++ .../notifications-get-response.dto.ts | 10 ++ .../notifications.service.interface.ts | 17 ++- .../notifications/notifications.controller.ts | 38 +++--- .../notifications/notifications.repository.ts | 51 ++++--- .../notifications/notifications.service.ts | 63 ++++----- src/APIs/users/entities/user.entity.ts | 4 +- src/app.module.ts | 14 +- 16 files changed, 229 insertions(+), 215 deletions(-) create mode 100644 src/APIs/notifications/dtos/common/notification.dto.ts delete mode 100644 src/APIs/notifications/dtos/emit-noti.dto.ts delete mode 100644 src/APIs/notifications/dtos/fetch-noti.dto.ts create mode 100644 src/APIs/notifications/dtos/request/notification-emit-request.dto.ts create mode 100644 src/APIs/notifications/dtos/request/notifications-get-request.dto.ts create mode 100644 src/APIs/notifications/dtos/response/notifications-get-response.dto.ts diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 605cb1e..9458be9 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -1,8 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; -import { IFollowsRepositoryGetList } from './interfaces/follows.repository.interface'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { IFollowsRepositoryFindList } from './interfaces/follows.repository.interface'; @Injectable() export class FollowsRepository extends Repository { @@ -11,9 +11,9 @@ export class FollowsRepository extends Repository { } async getFollowers({ - kakaoId, + userId, loggedUser, - }: IFollowsRepositoryGetList): Promise { + }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.from_user', 'user') .where('user.date_deleted IS NULL') @@ -43,7 +43,7 @@ export class FollowsRepository extends Repository { 'user.date_deleted AS date_deleted', 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ kakaoId, loggedUser }) + .setParameters({ userId, loggedUser }) .getRawMany(); return followings.map((follower) => ({ @@ -63,9 +63,9 @@ export class FollowsRepository extends Repository { } async getFollowings({ - kakaoId, + userId, loggedUser, - }: IFollowsRepositoryGetList): Promise { + }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.to_user', 'user') .where('user.date_deleted IS NULL') @@ -95,7 +95,7 @@ export class FollowsRepository extends Repository { 'user.date_deleted AS date_deleted', 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ kakaoId, loggedUser }) + .setParameters({ userId, loggedUser }) .getRawMany(); return followings.map((follower) => ({ diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index f0ca490..36c8cfc 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -6,44 +6,45 @@ import { FollowsRepository } from './follows.repository'; import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { User } from '../users/entities/user.entity'; import { - IFollowsServiceGetList, + IFollowsServiceFindList, IFollowsServiceUsers, } from './interfaces/follows.service.interface'; import { NotificationsService } from '../notifications/notifications.service'; import { NotType } from 'src/common/enums/not-type.enum'; +import { from, identity } from 'rxjs'; @Injectable() export class FollowsService { constructor( - private readonly followsRepository: FollowsRepository, - private readonly notificationsService: NotificationsService, - private readonly dataSource: DataSource, + private readonly repo_follows: FollowsRepository, + private readonly svc_notifications: NotificationsService, + private readonly db_dataSource: DataSource, ) {} - isSame({ from_user, to_user }: IFollowsServiceUsers): boolean { - if (from_user == to_user) { + isSame({ fromUser, toUser }: IFollowsServiceUsers): boolean { + if (fromUser == toUser) { return true; } return false; } async getScope({ - from_user, - to_user, + fromUser, + toUser, }: IFollowsServiceUsers): Promise { - if (from_user === to_user) + if (fromUser === toUser) return [OpenScope.PUBLIC, OpenScope.PROTECTED, OpenScope.PRIVATE]; - if (from_user !== null && to_user !== null) { - const following = await this.followsRepository.findOne({ + if (fromUser !== null && toUser !== null) { + const following = await this.repo_follows.findOne({ where: { - from_user: { kakaoId: from_user }, - to_user: { kakaoId: to_user }, + fromUser: { id: fromUser }, + toUser: { id: toUser }, }, }); - const follower = await this.followsRepository.findOne({ + const follower = await this.repo_follows.findOne({ where: { - from_user: { kakaoId: to_user }, - to_user: { kakaoId: from_user }, + fromUser: { id: toUser }, + toUser: { id: fromUser }, }, }); if (following && follower) { @@ -56,14 +57,14 @@ export class FollowsService { return [OpenScope.PUBLIC]; } - async isExist({ - from_user, - to_user, + async existCheck({ + fromUser, + toUser, }: IFollowsServiceUsers): Promise { - const follow = await this.followsRepository.findOne({ + const follow = await this.repo_follows.findOne({ where: { - from_user: { kakaoId: from_user }, - to_user: { kakaoId: to_user }, + fromUser: { id: fromUser }, + toUser: { id: toUser }, }, loadRelationIds: true, }); @@ -74,47 +75,47 @@ export class FollowsService { } async followUser({ - from_user, - to_user, + fromUser, + toUser, }: IFollowsServiceUsers): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const toUserData = await queryRunner.manager.findOne(User, { - where: { kakaoId: to_user }, + where: { id: toUser }, }); const fromUserData = await queryRunner.manager.findOne(User, { - where: { kakaoId: from_user }, + where: { id: fromUser }, }); - const isExist = await this.isExist({ from_user, to_user }); + const isExist = await this.existCheck({ fromUser, toUser }); if (isExist) { throw new ConflictException('already exists'); } - if (this.isSame({ from_user, to_user })) { + if (this.isSame({ fromUser, toUser })) { throw new ConflictException('you cannot follow yourself!'); } - const follow = await this.followsRepository.save({ - from_user: { kakaoId: from_user }, - to_user: { kakaoId: to_user }, + const follow = await this.repo_follows.save({ + fromUser: { id: fromUser }, + toUser: { id: toUser }, }); - await queryRunner.manager.update(User, fromUserData.kakaoId, { - following_count: () => 'following_count +1', + await queryRunner.manager.update(User, fromUserData.id, { + followingCount: () => 'following_count +1', }); - await queryRunner.manager.update(User, toUserData.kakaoId, { - follower_count: () => 'follower_count +1', + await queryRunner.manager.update(User, toUserData.id, { + followerCount: () => 'follower_count +1', }); await queryRunner.commitTransaction(); console.log('commited'); - await this.notificationsService.emitAlarm({ - userKakaoId: from_user, - targetUserKakaoId: to_user, + await this.svc_notifications.emitAlarm({ + userId: fromUser, + targetUserId: toUser, type: NotType.FOLLOW, - postId: null, + articleId: null, }); - return await this.followsRepository.findOne({ where: { id: follow.id } }); + return await this.repo_follows.findOne({ where: { id: follow.id } }); } catch (e) { await queryRunner.rollbackTransaction(); throw e; @@ -124,39 +125,39 @@ export class FollowsService { } async unfollowUser({ - from_user, - to_user, + fromUser, + toUser, }: IFollowsServiceUsers): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const toUserData = await queryRunner.manager.findOne(User, { - where: { kakaoId: to_user }, + where: { id: toUser }, }); const fromUserData = await queryRunner.manager.findOne(User, { - where: { kakaoId: from_user }, + where: { id: fromUser }, }); - const isExist = await this.isExist({ from_user, to_user }); + const isExist = await this.existCheck({ fromUser, toUser }); if (!isExist) { throw new ConflictException('no data exists'); } - if (this.isSame({ from_user, to_user })) { + if (this.isSame({ fromUser, toUser })) { throw new ConflictException('you cannot unfollow yourself!'); } - await queryRunner.manager.update(User, fromUserData.kakaoId, { - following_count: () => 'following_count -1', + await queryRunner.manager.update(User, fromUserData.id, { + followingCount: () => 'following_count -1', }); - await queryRunner.manager.update(User, toUserData.kakaoId, { - follower_count: () => 'follower_count -1', + await queryRunner.manager.update(User, toUserData.id, { + followerCount: () => 'follower_count -1', }); - await this.followsRepository.delete({ - from_user: { kakaoId: from_user }, - to_user: { kakaoId: to_user }, + await this.repo_follows.delete({ + fromUser: { id: fromUser }, + toUser: { id: toUser }, }); await queryRunner.commitTransaction(); return; @@ -169,22 +170,22 @@ export class FollowsService { } async getFollows({ - kakaoId, + userId, loggedUser, - }: IFollowsServiceGetList): Promise { - const follows = await this.followsRepository.getFollowings({ - kakaoId, + }: IFollowsServiceFindList): Promise { + const follows = await this.repo_follows.getFollowings({ + userId, loggedUser, }); return follows; } async getFollowers({ - kakaoId, + userId, loggedUser, - }: IFollowsServiceGetList): Promise { - const follows = await this.followsRepository.getFollowers({ - kakaoId, + }: IFollowsServiceFindList): Promise { + const follows = await this.repo_follows.getFollowers({ + userId, loggedUser, }); return follows; diff --git a/src/APIs/follows/interfaces/follows.repository.interface.ts b/src/APIs/follows/interfaces/follows.repository.interface.ts index dc52108..e68a0a3 100644 --- a/src/APIs/follows/interfaces/follows.repository.interface.ts +++ b/src/APIs/follows/interfaces/follows.repository.interface.ts @@ -1,4 +1,4 @@ -export interface IFollowsRepositoryGetList { - kakaoId: number; +export interface IFollowsRepositoryFindList { + userId: number; loggedUser: number; } diff --git a/src/APIs/follows/interfaces/follows.service.interface.ts b/src/APIs/follows/interfaces/follows.service.interface.ts index b196d67..d5b7cda 100644 --- a/src/APIs/follows/interfaces/follows.service.interface.ts +++ b/src/APIs/follows/interfaces/follows.service.interface.ts @@ -1,9 +1,9 @@ export interface IFollowsServiceUsers { - from_user: number; - to_user: number; + fromUser: number; + toUser: number; } -export interface IFollowsServiceGetList { - kakaoId: number; +export interface IFollowsServiceFindList { + userId: number; loggedUser: number; } diff --git a/src/APIs/notifications/dtos/common/notification.dto.ts b/src/APIs/notifications/dtos/common/notification.dto.ts new file mode 100644 index 0000000..0db7520 --- /dev/null +++ b/src/APIs/notifications/dtos/common/notification.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { Notification } from '../../entities/notification.entity'; + +export class NotificationDto extends OmitType(Notification, [ + 'article', + 'targetUser', + 'user', +] as const) {} diff --git a/src/APIs/notifications/dtos/emit-noti.dto.ts b/src/APIs/notifications/dtos/emit-noti.dto.ts deleted file mode 100644 index 9ad823f..0000000 --- a/src/APIs/notifications/dtos/emit-noti.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; -import { Notification } from '../entities/notification.entity'; -export class EmitNotiDto extends PickType(Notification, [ - 'userKakaoId', - 'targetUserKakaoId', - 'type', -]) { - @ApiProperty({ - type: Number, - description: '알림이 발생한 게시글 id(nullable)', - }) - postId: number; -} - -export class EmitNotiInput extends OmitType(EmitNotiDto, ['userKakaoId']) {} diff --git a/src/APIs/notifications/dtos/fetch-noti.dto.ts b/src/APIs/notifications/dtos/fetch-noti.dto.ts deleted file mode 100644 index 8782816..0000000 --- a/src/APIs/notifications/dtos/fetch-noti.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; -import { IsEnum, IsOptional } from 'class-validator'; -import { DateOption } from 'src/common/enums/date-option'; -import { Notification } from '../entities/notification.entity'; -import { User } from 'src/APIs/users/entities/user.entity'; - -export class FetchNotiInput { - @ApiProperty({ - type: Boolean, - description: '확인 된 알림 조회 여부(true: 조회, false: 스킵)', - default: true, - }) - is_checked: boolean; - - @ApiProperty({ - type: 'enun', - enum: DateOption, - description: '특정 기간 이후 알림 조회, null 일 경우 전체 조회', - required: false, - default: null, - }) - @IsEnum(DateOption) - @IsOptional() - date_created?: DateOption; -} - -export class FetchNotiDto extends FetchNotiInput { - kakaoId: number; -} - -export class FetchNotiResponse extends OmitType(Notification, [ - 'targetUser', - 'user', - 'post', -]) { - @ApiProperty({ - type: PickType(User, ['username', 'profile_image', 'handle']), - }) - user: Pick; -} diff --git a/src/APIs/notifications/dtos/request/notification-emit-request.dto.ts b/src/APIs/notifications/dtos/request/notification-emit-request.dto.ts new file mode 100644 index 0000000..0d5b001 --- /dev/null +++ b/src/APIs/notifications/dtos/request/notification-emit-request.dto.ts @@ -0,0 +1,8 @@ +import { PickType } from '@nestjs/swagger'; +import { NotificationDto } from '../common/notification.dto'; + +export class NotificationEmitRequestDto extends PickType(NotificationDto, [ + 'targetUserId', + 'type', + 'articleId', +]) {} diff --git a/src/APIs/notifications/dtos/request/notifications-get-request.dto.ts b/src/APIs/notifications/dtos/request/notifications-get-request.dto.ts new file mode 100644 index 0000000..f5407da --- /dev/null +++ b/src/APIs/notifications/dtos/request/notifications-get-request.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { DateOption } from 'src/common/enums/date-option'; + +export class NotificationsGetRequestDto { + @ApiProperty({ + type: Boolean, + description: '확인 된 알림 조회 여부(true: 조회, false: 스킵)', + default: true, + }) + isChecked: boolean; + + @ApiProperty({ + type: 'enun', + enum: DateOption, + description: '특정 기간 이후 알림 조회, null 일 경우 전체 조회', + required: false, + default: null, + }) + @IsEnum(DateOption) + @IsOptional() + dateCreated?: DateOption; +} diff --git a/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts b/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts new file mode 100644 index 0000000..8c5c55c --- /dev/null +++ b/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { NotificationDto } from '../common/notification.dto'; +import { UserDto } from 'src/APIs/users/dtos/common/user.dto'; + +export class NotificationsGetResponseDto extends NotificationDto { + @ApiProperty({ + type: PickType(UserDto, ['username', 'profileImage', 'handle']), + }) + user: Pick; +} diff --git a/src/APIs/notifications/interfaces/notifications.service.interface.ts b/src/APIs/notifications/interfaces/notifications.service.interface.ts index c546a0b..0a5b041 100644 --- a/src/APIs/notifications/interfaces/notifications.service.interface.ts +++ b/src/APIs/notifications/interfaces/notifications.service.interface.ts @@ -1,8 +1,19 @@ +import { NotificationEmitRequestDto } from '../dtos/request/notification-emit-request.dto'; +import { NotificationsGetRequestDto } from '../dtos/request/notifications-get-request.dto'; + export interface INotificationsServiceConnectUser { - targetUserKakaoId: number; + targetUserId: number; } export interface INotificationsServiceRead { - id: number; - targetUserKakaoId: number; + notificationId: number; + targetUserId: number; +} +export interface INotificationsSeviceEmitNotification + extends NotificationEmitRequestDto { + userId: number; +} + +export class INotificationsServiceGetNotifications extends NotificationsGetRequestDto { + userId: number; } diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index b3d49fc..066d128 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -21,8 +21,9 @@ import { import { Request, Response } from 'express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { FetchNotiInput, FetchNotiResponse } from './dtos/fetch-noti.dto'; import { interval, map, merge } from 'rxjs'; +import { NotificationsGetResponseDto } from './dtos/response/notifications-get-response.dto'; +import { NotificationsGetRequestDto } from './dtos/request/notifications-get-request.dto'; @ApiTags('알림 API') @Controller('notifications') @@ -39,11 +40,11 @@ export class NotificationsController { @UseGuards(AuthGuardV2) @Sse('subscribe') connectUser(@Req() req: Request, @Res() res: Response) { - const targetUserKakaoId = req.user.userId; + const targetUserId = req.user.userId; res.setTimeout(60 * 10000); // 600초로 설정, 필요에 따라 변경 가능 nginx도 함께 변경할 것. const sseStream = this.notificationsService.connectUser({ - targetUserKakaoId, + targetUserId, }); const pingStream = interval(30000).pipe( map(() => ({ type: 'ping', data: 'keep-alive' })), @@ -56,17 +57,17 @@ export class NotificationsController { description: '로그인된 유저들에게 보내진 알림들을 조회한다. query를 통해 알림 조회 옵션 설정. sse 연결 이전 이니셜 데이터 fetch 시 사용', }) - @ApiOkResponse({ type: [FetchNotiResponse] }) + @ApiOkResponse({ type: [NotificationsGetResponseDto] }) @Get() @ApiCookieAuth() @UseGuards(AuthGuardV2) - async fetchNoti( + async getNotifications( @Req() req: Request, - @Query() fetchNotiInput: FetchNotiInput, - ): Promise { - const kakaoId = req.user.userId; - return await this.notificationsService.fetch({ - kakaoId, + @Query() fetchNotiInput: NotificationsGetRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.notificationsService.findNotifications({ + userId, ...fetchNotiInput, }); } @@ -77,14 +78,17 @@ export class NotificationsController { }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @ApiOkResponse({ type: FetchNotiResponse }) + @ApiOkResponse({ type: NotificationsGetResponseDto }) @HttpCode(200) - @Post(':id/read') - async readNoti( + @Post(':notificationId/read') + async readNotification( @Req() req: Request, - @Param('id') id: number, - ): Promise { - const targetUserKakaoId = req.user.userId; - return await this.notificationsService.read({ id, targetUserKakaoId }); + @Param('notificationId') notificationId: number, + ): Promise { + const targetUserId = req.user.userId; + return await this.notificationsService.readNotification({ + notificationId, + targetUserId, + }); } } diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index a9c53f7..d888823 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -1,17 +1,20 @@ import { Injectable } from '@nestjs/common'; import { Notification } from './entities/notification.entity'; import { DataSource, Repository } from 'typeorm'; -import { EmitNotiDto } from './dtos/emit-noti.dto'; -import { FetchNotiResponse } from './dtos/fetch-noti.dto'; -import { INotificationsServiceRead } from './interfaces/notifications.service.interface'; + +import { + INotificationsServiceRead, + INotificationsSeviceEmitNotification, +} from './interfaces/notifications.service.interface'; +import { NotificationsGetResponseDto } from './dtos/response/notifications-get-response.dto'; @Injectable() export class NotificationsRepository extends Repository { - constructor(private dataSource: DataSource) { - super(Notification, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(Notification, db_dataSource.createEntityManager()); } - async createOne(emitNotiDto: EmitNotiDto) { + async createOne(emitNotiDto: INotificationsSeviceEmitNotification) { return await this.createQueryBuilder() .insert() .into(Notification, Object.keys(emitNotiDto)) @@ -20,41 +23,37 @@ export class NotificationsRepository extends Repository { } async fetchOne({ - id, - targetUserKakaoId, - }: INotificationsServiceRead): Promise { + notificationId, + targetUserId, + }: INotificationsServiceRead): Promise { return await this.createQueryBuilder('n') .leftJoin('n.user', 'user') .addSelect(['user.profile_image', 'user.username', 'user.handle']) - .where('n.id = :id', { id }) - .andWhere('n.targetUserKakaoId = :targetUserKakaoId', { - targetUserKakaoId, + .where('n.id = :id', { id: notificationId }) + .andWhere('n.target_user_id = :targetUserId', { + targetUserId, }) .getOne(); } async fetchAll({ - kakaoId, - date_created, - is_checked, - }): Promise { + userId, + dateCreated, + isChecked, + }): Promise { const query = this.createQueryBuilder('n') .leftJoin('n.user', 'user') .addSelect(['user.profile_image', 'user.username', 'user.handle']) - .where('n.targetUserKakaoId = :kakaoId', { - kakaoId, + .where('n.target_user_id = :userId', { + userId, }); - if (!is_checked) { + if (!isChecked) { query.andWhere('n.is_checked = true'); } - if (date_created) { - query.andWhere('n.date_created > :date_created', { date_created }); + if (dateCreated) { + query.andWhere('n.date_created > :date_created', { dateCreated }); } - // 열 이름을 별칭으로 지정하여 원래 이름 그대로 출력 - // const columnNames = (await this.metadata.columns).map( - // (column) => `n.${column.databaseName} AS ${column.propertyName}`, - // ); - // query.addSelect(columnNames); + return await query.getMany(); } } diff --git a/src/APIs/notifications/notifications.service.ts b/src/APIs/notifications/notifications.service.ts index ef92772..335af14 100644 --- a/src/APIs/notifications/notifications.service.ts +++ b/src/APIs/notifications/notifications.service.ts @@ -1,15 +1,16 @@ import { BadRequestException, Injectable, MessageEvent } from '@nestjs/common'; import { NotificationsRepository } from './notifications.repository'; import { Observable, Subject, filter, map } from 'rxjs'; -import { EmitNotiDto } from './dtos/emit-noti.dto'; -import { FetchNotiDto, FetchNotiResponse } from './dtos/fetch-noti.dto'; import { DateOption } from 'src/common/enums/date-option'; import { INotificationsServiceConnectUser, + INotificationsServiceGetNotifications, INotificationsServiceRead, + INotificationsSeviceEmitNotification, } from './interfaces/notifications.service.interface'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; +import { NotificationsGetResponseDto } from './dtos/response/notifications-get-response.dto'; @Injectable() export class NotificationsService { @@ -17,7 +18,7 @@ export class NotificationsService { @InjectQueue('audio') private redisQueue: Queue, private readonly notificationsRepository: NotificationsRepository, ) {} - private notis$: Subject = new Subject(); + private notis$: Subject = new Subject(); private observer = this.notis$.asObservable(); private readonly queueName = 'audio'; @@ -33,11 +34,11 @@ export class NotificationsService { } connectUser({ - targetUserKakaoId, + targetUserId, }: INotificationsServiceConnectUser): Observable { - console.log('connected: ' + targetUserKakaoId); + console.log('connected: ' + targetUserId); const pipe = this.observer.pipe( - filter((noti) => noti.targetUserKakaoId == targetUserKakaoId), + filter((noti) => noti.targetUserId == targetUserId), map((noti) => { return { data: noti, @@ -48,19 +49,19 @@ export class NotificationsService { } async emitAlarm({ - userKakaoId, - targetUserKakaoId, + userId, + targetUserId, type, - }: EmitNotiDto): Promise { + }: INotificationsSeviceEmitNotification): Promise { try { const data = await this.notificationsRepository.save({ - userKakaoId, - targetUserKakaoId, + userId, + targetUserId, type, }); const response = await this.notificationsRepository.fetchOne({ - id: data.id, - targetUserKakaoId, + notificationId: data.id, + targetUserId, }); // Redis 큐에 이벤트를 전송 await this.redisQueue.add(this.queueName, response); @@ -70,14 +71,16 @@ export class NotificationsService { } } - async fetch({ - is_checked, - kakaoId, - date_created, - }: FetchNotiDto): Promise { + async findNotifications({ + isChecked, + userId, + dateCreated, + }: INotificationsServiceGetNotifications): Promise< + NotificationsGetResponseDto[] + > { let currentDate = new Date(); - switch (date_created) { + switch (dateCreated) { case DateOption.WEEK: currentDate.setDate(currentDate.getDate() - 7); break; @@ -91,28 +94,28 @@ export class NotificationsService { currentDate = null; } return await this.notificationsRepository.fetchAll({ - is_checked, - kakaoId, - date_created: currentDate, + isChecked, + userId, + dateCreated: currentDate, }); } - async read({ - id, - targetUserKakaoId, - }: INotificationsServiceRead): Promise { + async readNotification({ + notificationId, + targetUserId, + }: INotificationsServiceRead): Promise { const updateResult = await this.notificationsRepository.update( - { id, targetUserKakaoId }, + { id: notificationId, targetUserId }, { - is_checked: true, + isChecked: true, }, ); if (updateResult.affected < 1) { throw new BadRequestException('알림을 찾을 수 없거나 권한이 없습니다.'); } return await this.notificationsRepository.fetchOne({ - id, - targetUserKakaoId, + notificationId, + targetUserId, }); } } diff --git a/src/APIs/users/entities/user.entity.ts b/src/APIs/users/entities/user.entity.ts index 3528173..e399881 100644 --- a/src/APIs/users/entities/user.entity.ts +++ b/src/APIs/users/entities/user.entity.ts @@ -125,7 +125,7 @@ export class User extends CommonEntity { description: '연결된 팔로잉', nullable: true, }) - @OneToMany(() => Follow, (follow) => follow.from_user) + @OneToMany(() => Follow, (follow) => follow.fromUser) followings: Follow[]; @ApiProperty({ @@ -133,7 +133,7 @@ export class User extends CommonEntity { description: '연결된 팔로워', nullable: true, }) - @OneToMany(() => Follow, (follow) => follow.to_user) + @OneToMany(() => Follow, (follow) => follow.toUser) followers: Follow[]; @ApiProperty({ diff --git a/src/app.module.ts b/src/app.module.ts index a08df95..98f73bf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,13 +3,10 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsModule } from './APIs/comments/comments.module'; -import { PostsModule } from './APIs/articles/articles.module'; import { UsersModule } from './APIs/users/users.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from './APIs/auth/auth.module'; import { FollowsModule } from './APIs/follows/follows.module'; -import { PostBackgroundsModule } from './APIs/articleBackgrounds/articleBackgrounds.module'; -import { PostCategoriesModule } from './APIs/articleCategories/articleCategories.module'; import { LikesModule } from './APIs/likes/likes.module'; import { StickersModule } from './APIs/stickers/stickers.module'; import { StickerCategoriesModule } from './APIs/stickerCategories/stickerCategories.module'; @@ -31,6 +28,10 @@ import { TerminusModule } from '@nestjs/terminus'; import { HttpModule } from '@nestjs/axios'; import { MetricsModule } from './modules/metrics/metrics.module'; import { DevtoolsModule } from '@nestjs/devtools-integration'; +import { ArticlesModule } from './APIs/articles/articles.module'; +import { ArticleCategoriesModule } from './APIs/articleCategories/articleCategories.module'; +import { ArticleBackgroundsModule } from './APIs/articleBackgrounds/articleBackgrounds.module'; +import { ImagesModule } from './modules/images/images.module'; @Module({ imports: [ @@ -41,15 +42,16 @@ import { DevtoolsModule } from '@nestjs/devtools-integration'; StickersModule, StickerCategoriesModule, StickerBlocksModule, - PostsModule, + ArticlesModule, + ArticleCategoriesModule, + ArticleBackgroundsModule, + ImagesModule, CommentsModule, LikesModule, UsersModule, - PostCategoriesModule, AuthModule, FollowsModule, NotificationsModule, - PostBackgroundsModule, ReportsModule, TerminusModule, HttpModule, From f008965e39cd42b6241a3c4819b619243fcd0999 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 21:24:16 +0900 Subject: [PATCH 213/236] refactor(Follow): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/follows/dtos/common/follow.dto.ts | 4 ++ src/APIs/follows/dtos/follow-user.dto.ts | 4 -- src/APIs/follows/follows.controller.ts | 52 +++++++++++----------- src/APIs/follows/follows.module.ts | 3 +- src/APIs/follows/follows.service.ts | 5 +-- 5 files changed, 33 insertions(+), 35 deletions(-) create mode 100644 src/APIs/follows/dtos/common/follow.dto.ts delete mode 100644 src/APIs/follows/dtos/follow-user.dto.ts diff --git a/src/APIs/follows/dtos/common/follow.dto.ts b/src/APIs/follows/dtos/common/follow.dto.ts new file mode 100644 index 0000000..e55c07f --- /dev/null +++ b/src/APIs/follows/dtos/common/follow.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Follow } from '../../entities/follow.entity'; + +export class FollowDto extends OmitType(Follow, ['fromUser', 'toUser']) {} diff --git a/src/APIs/follows/dtos/follow-user.dto.ts b/src/APIs/follows/dtos/follow-user.dto.ts deleted file mode 100644 index 1bac305..0000000 --- a/src/APIs/follows/dtos/follow-user.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Follow } from '../entities/follow.entity'; - -export class FollowUserDto extends OmitType(Follow, ['from_user', 'to_user']) {} diff --git a/src/APIs/follows/follows.controller.ts b/src/APIs/follows/follows.controller.ts index ad2fc33..f6f110e 100644 --- a/src/APIs/follows/follows.controller.ts +++ b/src/APIs/follows/follows.controller.ts @@ -19,10 +19,10 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { FollowUserDto } from './dtos/follow-user.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { FollowsService } from './follows.service'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { FollowDto } from './dtos/common/follow.dto'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; @ApiTags('유저 API') @Controller('users') @@ -34,19 +34,19 @@ export class FollowsController { description: '로그인된 유저가 userId를 팔로우한다.', }) @ApiCookieAuth() - @ApiCreatedResponse({ description: '이웃 추가 성공', type: FollowUserDto }) + @ApiCreatedResponse({ description: '이웃 추가 성공', type: FollowDto }) @ApiConflictResponse({ description: '이미 팔로우한 상태이다.' }) @UseGuards(AuthGuardV2) @Post(':userId/follow') @HttpCode(201) async followUser( @Req() req: Request, - @Param('userId') to_user: number, - ): Promise { - const kakaoId = parseInt(req.user.userId); + @Param('userId') toUser: number, + ): Promise { + const userId = parseInt(req.user.userId); return await this.followsService.followUser({ - from_user: kakaoId, - to_user, + fromUser: userId, + toUser, }); } @@ -62,12 +62,12 @@ export class FollowsController { @HttpCode(204) unfollowUser( @Req() req: Request, - @Param('userId') to_user: number, + @Param('userId') toUser: number, ): Promise { - const kakaoId = parseInt(req.user.userId); + const userId = parseInt(req.user.userId); return this.followsService.unfollowUser({ - from_user: kakaoId, - to_user, + fromUser: userId, + toUser, }); } @@ -82,10 +82,10 @@ export class FollowsController { @Get('me/follower/:userId') async checkFollower( @Req() req: Request, - @Param('userId') to_user: number, + @Param('userId') toUser: number, ): Promise { - const from_user = req.user.userId; - return await this.followsService.isExist({ from_user, to_user }); + const fromUser = req.user.userId; + return await this.followsService.existCheck({ fromUser, toUser }); } @ApiOperation({ @@ -99,10 +99,10 @@ export class FollowsController { @Get('me/following/:userId') async checkFollowing( @Req() req: Request, - @Param('userId') from_user: number, + @Param('userId') fromUser: number, ): Promise { - const to_user = req.user.userId; - return await this.followsService.isExist({ from_user, to_user }); + const toUser = req.user.userId; + return await this.followsService.existCheck({ fromUser, toUser }); } @ApiOperation({ summary: '팔로워 목록 조회', @@ -110,16 +110,16 @@ export class FollowsController { }) @ApiOkResponse({ description: '팔로워 목록 조회 성공', - type: [UserResponseDtoWithFollowing], + type: [UserFollowingResponseDto], }) @HttpCode(200) @Get(':userId/followers') getFollowers( @Req() req: Request, - @Param('userId') kakaoId: number, - ): Promise { + @Param('userId') userId: number, + ): Promise { const loggedUser = req.user.userId; - return this.followsService.getFollowers({ kakaoId, loggedUser }); + return this.followsService.findFollowers({ userId, loggedUser }); } @ApiOperation({ @@ -128,15 +128,15 @@ export class FollowsController { }) @ApiOkResponse({ description: '팔로잉 목록 조회 성공', - type: [UserResponseDtoWithFollowing], + type: [UserFollowingResponseDto], }) @HttpCode(200) @Get(':userId/followings') getFollows( @Req() req: Request, - @Param('userId') kakaoId: number, - ): Promise { + @Param('userId') userId: number, + ): Promise { const loggedUser = req.user.userId; - return this.followsService.getFollows({ kakaoId, loggedUser }); + return this.followsService.findFollowings({ userId, loggedUser }); } } diff --git a/src/APIs/follows/follows.module.ts b/src/APIs/follows/follows.module.ts index c57a71f..accffce 100644 --- a/src/APIs/follows/follows.module.ts +++ b/src/APIs/follows/follows.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersModule } from '../users/users.module'; -import { User } from '../users/entities/user.entity'; import { FollowsService } from './follows.service'; import { FollowsController } from './follows.controller'; import { Follow } from './entities/follow.entity'; @@ -12,7 +11,7 @@ import { NotificationsModule } from '../notifications/notifications.module'; imports: [ UsersModule, NotificationsModule, - TypeOrmModule.forFeature([Follow, User]), + TypeOrmModule.forFeature([Follow]), ], providers: [FollowsService, FollowsRepository], controllers: [FollowsController], diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index 36c8cfc..f814113 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -11,7 +11,6 @@ import { } from './interfaces/follows.service.interface'; import { NotificationsService } from '../notifications/notifications.service'; import { NotType } from 'src/common/enums/not-type.enum'; -import { from, identity } from 'rxjs'; @Injectable() export class FollowsService { @@ -169,7 +168,7 @@ export class FollowsService { } } - async getFollows({ + async findFollowings({ userId, loggedUser, }: IFollowsServiceFindList): Promise { @@ -180,7 +179,7 @@ export class FollowsService { return follows; } - async getFollowers({ + async findFollowers({ userId, loggedUser, }: IFollowsServiceFindList): Promise { From 49d983e94301ecb34a86952b778097f050bb26f5 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 21:32:53 +0900 Subject: [PATCH 214/236] refactor(Auth): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/auth/auth.controller.ts | 2 +- src/APIs/auth/auth.service.ts | 67 +++++++++++-------- .../auth/dtos/{ => common}/kakao-user.dto.ts | 2 +- src/APIs/auth/strategies/jwt.strategy.ts | 4 +- 4 files changed, 42 insertions(+), 33 deletions(-) rename src/APIs/auth/dtos/{ => common}/kakao-user.dto.ts (83%) diff --git a/src/APIs/auth/auth.controller.ts b/src/APIs/auth/auth.controller.ts index 4e82503..e49c4d5 100644 --- a/src/APIs/auth/auth.controller.ts +++ b/src/APIs/auth/auth.controller.ts @@ -39,7 +39,7 @@ export class AuthController { @HttpCode(301) async kakaoLogin(@Req() req: Request, @Res() res: Response) { const { accessToken, refreshToken } = await this.authService.getJWT({ - kakaoId: req.user.kakaoId, + userId: req.user.kakaoId, }); // 클라이언트 도메인 설정 diff --git a/src/APIs/auth/auth.service.ts b/src/APIs/auth/auth.service.ts index 876f187..4dca915 100644 --- a/src/APIs/auth/auth.service.ts +++ b/src/APIs/auth/auth.service.ts @@ -1,53 +1,62 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { UsersService } from '../users/users.service'; -import { KakaoUserDto } from './dtos/kakao-user.dto'; +import { KakaoUserDto } from './dtos/common/kakao-user.dto'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcrypt'; +import { UsersReadService } from '../users/services/users-read.service'; +import { UsersCreateService } from '../users/services/users-create.service'; +import { UsersUpdateService } from '../users/services/users-update.service'; @Injectable() export class AuthService { constructor( - private readonly usersService: UsersService, - private readonly jwtService: JwtService, - private readonly configService: ConfigService, + private readonly svc_usersRead: UsersReadService, + private readonly svc_usersCreate: UsersCreateService, + private readonly svc_usersUpdate: UsersUpdateService, + private readonly svc_jwt: JwtService, + private readonly svc_config: ConfigService, ) {} async getJWT(kakaoUserDto: KakaoUserDto) { const user = await this.kakaoValidateUser(kakaoUserDto); // 카카오 정보 검증 및 회원가입 로직 - const accessToken = this.generateAccessToken(user); // AccessToken 생성 - const refreshToken = await this.generateRefreshToken(user); // refreshToken 생성 + const accessToken = this.generateAccessToken({ userId: user.id }); // AccessToken 생성 + const refreshToken = await this.generateRefreshToken({ userId: user.id }); // refreshToken 생성 return { accessToken, refreshToken }; } async kakaoValidateUser(kakaoUserDto: KakaoUserDto) { - let user = - await this.usersService.findUserByKakaoIdWithDelete(kakaoUserDto); // 유저 조회 + let user = await this.svc_usersRead.findUserByIdWithDelete({ + userId: kakaoUserDto.userId, + }); // 유저 조회 if (!user) { // 회원 가입 로직 - user = await this.usersService.create(kakaoUserDto); + user = await this.svc_usersCreate.createUser({ + userId: kakaoUserDto.userId, + }); } - if (user.date_deleted != null) { - await this.usersService.activateUser({ kakaoId: kakaoUserDto.kakaoId }); + if (user.dateDeleted != null) { + await this.svc_usersUpdate.activateUser({ + userId: kakaoUserDto.userId, + }); } return user; } generateAccessToken(kakaoUserDto: KakaoUserDto) { - const payload = { userId: kakaoUserDto.kakaoId }; - return this.jwtService.sign(payload); + const payload = { userId: kakaoUserDto.userId }; + return this.svc_jwt.sign(payload); } async generateRefreshToken(kakaoUserDto: KakaoUserDto) { - const payload = { userId: kakaoUserDto.kakaoId }; - const refreshToken = this.jwtService.sign(payload, { - secret: this.configService.get('JWT_REFRESH_SECRET'), - expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN'), + const payload = { userId: kakaoUserDto.userId }; + const refreshToken = this.svc_jwt.sign(payload, { + secret: this.svc_config.get('JWT_REFRESH_SECRET'), + expiresIn: this.svc_config.get('JWT_REFRESH_EXPIRES_IN'), }); const saltOrRounds = 10; - const current_refresh_token = await bcrypt.hash(refreshToken, saltOrRounds); - const user = await this.usersService.setCurrentRefreshToken({ - kakaoId: payload.userId, - current_refresh_token, + const currentRefreshToken = await bcrypt.hash(refreshToken, saltOrRounds); + const user = await this.svc_usersUpdate.setCurrentRefreshToken({ + userId: payload.userId, + currentRefreshToken, }); console.log(user); return refreshToken; @@ -55,19 +64,19 @@ export class AuthService { async refresh(refreshToken: string): Promise { try { // 1차 검증 - const decodedRefreshToken = this.jwtService.verify(refreshToken, { - secret: this.configService.get('JWT_REFRESH_SECRET'), + const decodedRefreshToken = this.svc_jwt.verify(refreshToken, { + secret: this.svc_config.get('JWT_REFRESH_SECRET'), }); - const kakaoId = decodedRefreshToken.userId; + const userId = decodedRefreshToken.userId; // 데이터베이스에서 User 객체 가져오기 - const user = await this.usersService.findUserByKakaoIdWithToken({ - kakaoId, + const user = await this.svc_usersRead.findUserByIdWithToken({ + userId, }); // 2차 검증 const isRefreshTokenMatching = await bcrypt.compare( refreshToken, - user.current_refresh_token, + user.currentRefreshToken, ); if (!isRefreshTokenMatching) { @@ -75,7 +84,7 @@ export class AuthService { } // 새로운 accessToken 생성 - const accessToken = this.generateAccessToken(user); + const accessToken = this.generateAccessToken({ userId }); return accessToken; } catch (err) { diff --git a/src/APIs/auth/dtos/kakao-user.dto.ts b/src/APIs/auth/dtos/common/kakao-user.dto.ts similarity index 83% rename from src/APIs/auth/dtos/kakao-user.dto.ts rename to src/APIs/auth/dtos/common/kakao-user.dto.ts index ffb6500..61254bb 100644 --- a/src/APIs/auth/dtos/kakao-user.dto.ts +++ b/src/APIs/auth/dtos/common/kakao-user.dto.ts @@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger'; export class KakaoUserDto { @ApiProperty() - kakaoId: number; + userId: number; } diff --git a/src/APIs/auth/strategies/jwt.strategy.ts b/src/APIs/auth/strategies/jwt.strategy.ts index 3e923c3..45028dc 100644 --- a/src/APIs/auth/strategies/jwt.strategy.ts +++ b/src/APIs/auth/strategies/jwt.strategy.ts @@ -6,7 +6,7 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { // controller에 요청이 왔을 때 constructor가 실행 - constructor(private readonly configService: ConfigService) { + constructor(private readonly svc_config: ConfigService) { super({ // accessToken 위치 jwtFromRequest: ExtractJwt.fromExtractors([ @@ -19,7 +19,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { }, ]), ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), + secretOrKey: svc_config.get('JWT_SECRET'), }); } From 868f72fdaf0c1d7dc8ac941174fb043c69a8a8ce Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 22:08:51 +0900 Subject: [PATCH 215/236] refactor(feedback): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/comments/comments.controller.ts | 76 +++++------ src/APIs/comments/comments.repository.ts | 28 ++-- src/APIs/comments/comments.service.ts | 123 +++++++++--------- .../dtos/common/comment-children.dto.ts | 18 +++ src/APIs/comments/dtos/common/comment.dto.ts | 10 ++ src/APIs/comments/dtos/create-comment.dto.ts | 29 ----- src/APIs/comments/dtos/fetch-comments.dto.ts | 43 ------ src/APIs/comments/dtos/patch-comment.dto.ts | 4 - .../request/comment-create-request.dto.ts | 11 ++ .../dtos/request/comment-patch-request.dto.ts | 4 + .../response/comments-get-response.dto.ts | 7 + .../comments.repository.interface.ts | 8 +- .../interfaces/comments.service.interface.ts | 33 +++-- .../feedbacks/dtos/common/feedback.dto.ts | 4 + .../feedbacks/dtos/create-feedback.dto.ts | 4 - src/APIs/feedbacks/dtos/fetch-feedback.dto.ts | 4 - .../request/feedback-create-request.dto.ts | 6 + src/APIs/feedbacks/feedbacks.controller.ts | 24 ++-- src/APIs/feedbacks/feedbacks.service.ts | 32 ++--- .../interfaces/feedbacks.service.interface.ts | 8 +- 20 files changed, 230 insertions(+), 246 deletions(-) create mode 100644 src/APIs/comments/dtos/common/comment-children.dto.ts create mode 100644 src/APIs/comments/dtos/common/comment.dto.ts delete mode 100644 src/APIs/comments/dtos/create-comment.dto.ts delete mode 100644 src/APIs/comments/dtos/fetch-comments.dto.ts delete mode 100644 src/APIs/comments/dtos/patch-comment.dto.ts create mode 100644 src/APIs/comments/dtos/request/comment-create-request.dto.ts create mode 100644 src/APIs/comments/dtos/request/comment-patch-request.dto.ts create mode 100644 src/APIs/comments/dtos/response/comments-get-response.dto.ts create mode 100644 src/APIs/feedbacks/dtos/common/feedback.dto.ts delete mode 100644 src/APIs/feedbacks/dtos/create-feedback.dto.ts delete mode 100644 src/APIs/feedbacks/dtos/fetch-feedback.dto.ts create mode 100644 src/APIs/feedbacks/dtos/request/feedback-create-request.dto.ts diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 5b83aa9..4b58f7f 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -11,7 +11,6 @@ import { UseGuards, } from '@nestjs/common'; import { CommentsService } from './comments.service'; -import { CreateCommentInput } from './dtos/create-comment.dto'; import { Request } from 'express'; import { ApiCookieAuth, @@ -21,63 +20,66 @@ import { ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { - ChildrenComment, - FetchCommentDto, - FetchCommentsDto, -} from './dtos/fetch-comments.dto'; -import { PatchCommentDto } from './dtos/patch-comment.dto'; +import { CommentChildrenDto } from './dtos/common/comment-children.dto'; +import { CommentCreateRequestDto } from './dtos/request/comment-create-request.dto'; +import { CommentsGetResponseDto } from './dtos/response/comments-get-response.dto'; +import { CommentDto } from './dtos/common/comment.dto'; +import { CommentPatchRequestDto } from './dtos/request/comment-patch-request.dto'; @ApiTags('게시글 API') -@Controller('posts/:postId/comments') +@Controller('articles/:articleId/comments') export class CommentsController { - constructor(private readonly commentsService: CommentsService) {} + constructor(private readonly svc_comments: CommentsService) {} @ApiOperation({ summary: '댓글을 작성한다.', description: '댓글을 작성한다.', }) - @ApiOkResponse({ type: ChildrenComment }) + @ApiOkResponse({ type: CommentChildrenDto }) @ApiCookieAuth() @Post() @UseGuards(AuthGuardV2) @HttpCode(200) - async insertComment( + async createComment( @Req() req: Request, - @Param('postId') postsId: number, - @Body() body: CreateCommentInput, - ): Promise { - const userKakaoId = req.user.userId; - return await this.commentsService.insert({ ...body, postsId, userKakaoId }); + @Param('articleId') articleId: number, + @Body() body: CommentCreateRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.svc_comments.createComment({ + ...body, + articleId, + userId, + }); } @ApiOperation({ summary: '특정 게시글에 대한 댓글 조회', }) - @ApiOkResponse({ type: [FetchCommentsDto] }) + @ApiOkResponse({ type: [CommentsGetResponseDto] }) @Get() async fetchComments( - @Param('postId') postsId: number, - ): Promise { - return await this.commentsService.fetchComments({ postsId }); + @Param('articleId') articleId: number, + ): Promise { + return await this.svc_comments.fetchComments({ articleId }); } @ApiOperation({ summary: '특정 게시글에 대한 댓글 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: FetchCommentDto }) + @ApiOkResponse({ type: CommentDto }) @UseGuards(AuthGuardV2) @Patch(':commentId') async patchComment( @Req() req: Request, - @Param('postId') postsId: number, - @Param('commentId') id: number, - @Body() dto: PatchCommentDto, - ): Promise { - const kakaoId = req.user.userId; - return await this.commentsService.patchComment({ - kakaoId, - postsId, - id, + @Param('articleId') articleId: number, + @Param('commentId') commentId: number, + @Body() dto: CommentPatchRequestDto, + ): Promise { + const userId = req.user.userId; + return await this.svc_comments.patchComment({ + userId, + articleId, + commentId, ...dto, }); } @@ -93,14 +95,14 @@ export class CommentsController { @HttpCode(204) async deleteComment( @Req() req: Request, - @Param('postId') postsId: number, - @Param('commentId') id: number, + @Param('articleId') articleId: number, + @Param('commentId') commentId: number, ): Promise { - const userKakaoId = req.user.userId; - return await this.commentsService.delete({ - postsId, - id, - userKakaoId, + const userId = req.user.userId; + return await this.svc_comments.delete({ + articleId, + commentId, + userId, }); } } diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 6b9a20a..7412668 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -1,17 +1,17 @@ import { DataSource, InsertResult, Repository } from 'typeorm'; import { Comment } from './entities/comment.entity'; import { Injectable } from '@nestjs/common'; -import { FetchCommentsDto } from './dtos/fetch-comments.dto'; import { ICommentsRepositoryId, ICommentsRepositoryInsertComment, ICommentsRepositoryfetchComments, } from './interfaces/comments.repository.interface'; +import { CommentsGetResponseDto } from './dtos/response/comments-get-response.dto'; @Injectable() export class CommentsRepository extends Repository { - constructor(private dataSource: DataSource) { - super(Comment, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(Comment, db_dataSource.createEntityManager()); } async insertComment({ createCommentDto, @@ -24,31 +24,31 @@ export class CommentsRepository extends Repository { } async fetchCommentWithNotiInfo({ - id, + commentId, }: ICommentsRepositoryId): Promise { return await this.createQueryBuilder('c') .leftJoinAndSelect('c.user', 'user') - .leftJoinAndSelect('c.posts', 'posts') + .leftJoinAndSelect('c.article', 'article') .leftJoinAndSelect('c.parent', 'parent') - .where('c.id = :id', { id }) + .where('c.id = :commentId', { commentId }) .getOne(); } async fetchComments({ - postsId, - }: ICommentsRepositoryfetchComments): Promise { + articleId, + }: ICommentsRepositoryfetchComments): Promise { let comments = await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') .addSelect([ - 'u.kakaoId', + 'u.id', 'u.username', 'u.description', 'u.profile_image', 'u.handle', ]) .addSelect([ - 'childrenUser.kakaoId', + 'childrenUser.id', 'childrenUser.username', 'childrenUser.description', 'childrenUser.profile_image', @@ -56,18 +56,18 @@ export class CommentsRepository extends Repository { ]) .leftJoinAndSelect('c.children', 'children') .leftJoin('children.user', 'childrenUser') - .where('c.postsId = :postsId', { postsId }) - .andWhere('c.parentId IS NULL') + .where('c.article_id = :articleId', { articleId }) + .andWhere('c.parent_id IS NULL') .orderBy('c.date_created', 'ASC') .addOrderBy('children.date_created', 'ASC') .getMany(); comments = comments.filter((comment) => { comment.children = comment.children.filter( - (child) => child.date_deleted === null, + (child) => child.dateDeleted === null, ); // comment.children.length가 0이고 comment.date_deleted가 null이 아닌 경우를 제외 - return !(comment.children.length === 0 && comment.date_deleted !== null); + return !(comment.children.length === 0 && comment.dateDeleted !== null); }); return comments; diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 0bc6327..b58470a 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -4,40 +4,39 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { CreateCommentDto } from './dtos/create-comment.dto'; import { CommentsRepository } from './comments.repository'; import { DataSource, EntityManager, UpdateResult } from 'typeorm'; -import { Posts } from '../articles/entities/article.entity'; import { - ChildrenComment, - FetchCommentDto, - FetchCommentsDto, -} from './dtos/fetch-comments.dto'; -import { - ICommentsServiceDelete, - ICommentsServiceFetch, + ICommentsServiceArticleIdValidCheck, + ICommentsServiceCreateComment, + ICommentsServiceDeleteComment, + ICommentsServiceFindComments, ICommentsServiceId, - ICommentsServicePatch, - ICommentsServicePostsIdValidCheck, + ICommentsServicePatchComment, +, } from './interfaces/comments.service.interface'; import { Comment } from './entities/comment.entity'; import { NotificationsService } from '../notifications/notifications.service'; import { NotType } from 'src/common/enums/not-type.enum'; +import { CommentDto } from './dtos/common/comment.dto'; +import { CommentChildrenDto } from './dtos/common/comment-children.dto'; +import { Article } from '../articles/entities/article.entity'; +import { CommentsGetResponseDto } from './dtos/response/comments-get-response.dto'; @Injectable() export class CommentsService { constructor( - private readonly commentsRepository: CommentsRepository, - private readonly dataSource: DataSource, - private readonly notificationsService: NotificationsService, + private readonly svc_notifications: NotificationsService, + private readonly repo_comments: CommentsRepository, + private readonly db_dataSource: DataSource, ) {} async postsIdValidCheck({ parentId, - postsId, - }: ICommentsServicePostsIdValidCheck): Promise { - const parent = await this.existCheck({ id: parentId }); - if (parent.postsId != postsId) + articleId, + }: ICommentsServiceArticleIdValidCheck): Promise { + const parent = await this.existCheck({ commentId: parentId }); + if (parent.articleId != articleId) throw new BadRequestException( '게시글 아이디가 루트 댓글이 작성된 게시글 아이디와 일치하지 않습니다.', ); @@ -45,8 +44,8 @@ export class CommentsService { throw new BadRequestException('부모 댓글이 루트 댓글이 아닙니다.'); } - async existCheck({ id }: ICommentsServiceId): Promise { - const comment = await this.commentsRepository.findOne({ where: { id } }); + async existCheck({ commentId }: ICommentsServiceId): Promise { + const comment = await this.repo_comments.findOne({ where: { id: commentId} }); if (!comment) { throw new NotFoundException( '댓글의 아이디를 찾을 수 없습니다. 존재하지 않거나 이미 삭제되었습니다.', @@ -55,43 +54,43 @@ export class CommentsService { return comment; } - async insert(createCommentDto: CreateCommentDto): Promise { - const post = await this.dataSource.manager.findOne(Posts, { - where: { id: createCommentDto.postsId }, + async createComment(createCommentDto: ICommentsServiceCreateComment): Promise { + const post = await this.db_dataSource.manager.findOne(Article, { + where: { id: createCommentDto.articleId }, }); - if (post.allow_comment === false) + if (post.allowComment === false) throw new ForbiddenException('댓글이 허용되지 않은 게시물 입니다.'); if (createCommentDto.parentId) await this.postsIdValidCheck({ parentId: createCommentDto.parentId, - postsId: createCommentDto.postsId, + articleId: createCommentDto.articleId, }); - await this.dataSource.manager.update(Posts, createCommentDto.postsId, { - comment_count: () => 'comment_count +1', + await this.db_dataSource.manager.update(Article, createCommentDto.articleId, { + commentCount: () => 'comment_count +1', }); - const commentData = await this.commentsRepository.insertComment({ + const commentData = await this.repo_comments.insertComment({ createCommentDto, }); const { id } = commentData.identifiers[0]; - const { posts, parent, ...result } = - await this.commentsRepository.fetchCommentWithNotiInfo({ id }); + const { article, parent, ...result } = + await this.repo_comments.fetchCommentWithNotiInfo({ id }); - if (result.parentId && parent.userKakaoId != result.userKakaoId) { - await this.notificationsService.emitAlarm({ - userKakaoId: result.userKakaoId, - targetUserKakaoId: parent.userKakaoId, + if (result.parentId && parent.userId != result.userId) { + await this.svc_notifications.emitAlarm({ + userId: result.userId, + targetUserId: parent.userId, type: NotType.REPLY, - postId: result.postsId, + articleId: result.articleId, }); } // 자신에게 알림 보내는 경우 생략 - if (result.userKakaoId != posts.userKakaoId) { - await this.notificationsService.emitAlarm({ - userKakaoId: result.userKakaoId, - targetUserKakaoId: posts.userKakaoId, + if (result.userId != article.userId) { + await this.svc_notifications.emitAlarm({ + userId: result.userId, + targetUserId: article.userId, type: NotType.COMMENT, - postId: result.postsId, + articleId: result.articleId, }); } @@ -99,37 +98,37 @@ export class CommentsService { } async patchComment({ - kakaoId, - postsId, - id, + userId, + articleId, + commentId, content, - }: ICommentsServicePatch): Promise { + }: ICommentsServicePatchComment): Promise { const commentData = await this.existCheck({ id }); if (!commentData) throw new NotFoundException('댓글을 찾을 수 없습니다.'); - if (commentData.postsId != postsId) + if (commentData.articleId != articleId) throw new NotFoundException('루트 게시글의 아이디가 일치하지 않습니다.'); - if (commentData.userKakaoId != kakaoId) + if (commentData.userId != userId) throw new ForbiddenException('댓글을 수정할 권한이 없습니다.'); commentData.content = content; - return await this.commentsRepository.save(commentData); + return await this.repo_comments.save(commentData); } async fetchComments({ - postsId, - }: ICommentsServiceFetch): Promise { - return await this.commentsRepository.fetchComments({ postsId }); + articleId, + }: ICommentsServiceFindComments): Promise { + return await this.repo_comments.fetchComments({ articleId }); } async delete({ - id, - userKakaoId, - postsId, - }: ICommentsServiceDelete): Promise { - await this.dataSource.transaction(async (manager: EntityManager) => { - const data = await this.existCheck({ id }); + commentId, + userId, + articleId, + }: ICommentsServiceDeleteComment): Promise { + await this.db_dataSource.transaction(async (manager: EntityManager) => { + const data = await this.existCheck({ commentId }); let childrenData = []; let deletedResult: UpdateResult; - if (data.postsId !== postsId) { + if (data.articleId !== articleId) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); } if (data.parentId == null) @@ -137,14 +136,14 @@ export class CommentsService { where: { parentId: data.id }, }); if (childrenData.length == 0) { - await manager.delete(Comment, { user: { kakaoId: userKakaoId }, id }); - await manager.update(Posts, data.postsId, { - comment_count: () => 'comment_count - 1', + await manager.delete(Comment, { user: { id: userId }, id: commentId }); + await manager.update(Article, data.articleId, { + commentCount: () => 'comment_count - 1', }); } else { deletedResult = await manager.softDelete(Comment, { - user: { kakaoId: userKakaoId }, - id, + user: { id: userId }, + id: commentId, }); if (deletedResult.affected < 1) throw new NotFoundException('삭제할 댓글이 존재하지 않습니다'); diff --git a/src/APIs/comments/dtos/common/comment-children.dto.ts b/src/APIs/comments/dtos/common/comment-children.dto.ts new file mode 100644 index 0000000..3ed6bb7 --- /dev/null +++ b/src/APIs/comments/dtos/common/comment-children.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { Comment } from '../../entities/comment.entity'; +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/response/user-primary-response.dto'; + +export class CommentChildrenDto extends PickType(Comment, [ + 'id', + 'userId', + 'content', + 'dateCreated', + 'dateUpdated', + 'dateDeleted', + 'reportCount', + 'parentId', + 'articleId', +]) { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; +} diff --git a/src/APIs/comments/dtos/common/comment.dto.ts b/src/APIs/comments/dtos/common/comment.dto.ts new file mode 100644 index 0000000..5be31b0 --- /dev/null +++ b/src/APIs/comments/dtos/common/comment.dto.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { Comment } from '../../entities/comment.entity'; + +export class CommentDto extends OmitType(Comment, [ + 'article', + 'parent', + 'children', + 'reports', + 'user', +]) {} diff --git a/src/APIs/comments/dtos/create-comment.dto.ts b/src/APIs/comments/dtos/create-comment.dto.ts deleted file mode 100644 index 4dcbb27..0000000 --- a/src/APIs/comments/dtos/create-comment.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; - -export class CreateCommentDto { - @ApiProperty({ - description: '루트 게시글 아이디', - type: Number, - }) - postsId: number; - - @ApiProperty({ - description: '댓글 내용', - type: String, - }) - content: string; - - @ApiProperty({ - description: '[optional] 부모 댓글 id', - type: Number, - required: false, - }) - parentId?: number; - - userKakaoId: number; -} - -export class CreateCommentInput extends OmitType(CreateCommentDto, [ - 'userKakaoId', - 'postsId', -]) {} diff --git a/src/APIs/comments/dtos/fetch-comments.dto.ts b/src/APIs/comments/dtos/fetch-comments.dto.ts deleted file mode 100644 index 07766fd..0000000 --- a/src/APIs/comments/dtos/fetch-comments.dto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; -import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; -import { Comment } from '../entities/comment.entity'; - -export class ChildrenComment extends PickType(Comment, [ - 'id', - 'userKakaoId', - 'content', - 'date_created', - 'date_updated', - 'date_deleted', - 'report_count', - 'parentId', - 'postsId', -]) { - @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) - user: UserPrimaryResponseDto; -} - -export class FetchCommentDto extends OmitType(Comment, [ - 'user', - 'posts', - 'parent', - 'children', -]) {} - -export class FetchCommentsDto extends PickType(Comment, [ - 'id', - 'userKakaoId', - 'content', - 'date_created', - 'date_updated', - 'date_deleted', - 'report_count', - 'parentId', - 'postsId', -]) { - @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) - user: UserPrimaryResponseDto; - - @ApiProperty({ description: '자식 댓글 배열', type: [ChildrenComment] }) - children: ChildrenComment[]; -} diff --git a/src/APIs/comments/dtos/patch-comment.dto.ts b/src/APIs/comments/dtos/patch-comment.dto.ts deleted file mode 100644 index 3da7670..0000000 --- a/src/APIs/comments/dtos/patch-comment.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { Comment } from '../entities/comment.entity'; - -export class PatchCommentDto extends PickType(Comment, ['content']) {} diff --git a/src/APIs/comments/dtos/request/comment-create-request.dto.ts b/src/APIs/comments/dtos/request/comment-create-request.dto.ts new file mode 100644 index 0000000..3f5875d --- /dev/null +++ b/src/APIs/comments/dtos/request/comment-create-request.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { CommentDto } from '../common/comment.dto'; + +export class CommentCreateRequestDto extends PickType(CommentDto, ['content']) { + @ApiProperty({ + description: '[optional] 부모 댓글 id', + type: Number, + required: false, + }) + parentId?: number; +} diff --git a/src/APIs/comments/dtos/request/comment-patch-request.dto.ts b/src/APIs/comments/dtos/request/comment-patch-request.dto.ts new file mode 100644 index 0000000..5fdb277 --- /dev/null +++ b/src/APIs/comments/dtos/request/comment-patch-request.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { CommentDto } from '../common/comment.dto'; + +export class CommentPatchRequestDto extends PickType(CommentDto, ['content']) {} diff --git a/src/APIs/comments/dtos/response/comments-get-response.dto.ts b/src/APIs/comments/dtos/response/comments-get-response.dto.ts new file mode 100644 index 0000000..6cbe4b5 --- /dev/null +++ b/src/APIs/comments/dtos/response/comments-get-response.dto.ts @@ -0,0 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CommentChildrenDto } from '../common/comment-children.dto'; + +export class CommentsGetResponseDto extends CommentChildrenDto { + @ApiProperty({ description: '자식 댓글 배열', type: [CommentChildrenDto] }) + children: CommentChildrenDto[]; +} diff --git a/src/APIs/comments/interfaces/comments.repository.interface.ts b/src/APIs/comments/interfaces/comments.repository.interface.ts index 53a7451..1801e5f 100644 --- a/src/APIs/comments/interfaces/comments.repository.interface.ts +++ b/src/APIs/comments/interfaces/comments.repository.interface.ts @@ -1,13 +1,13 @@ -import { CreateCommentDto } from '../dtos/create-comment.dto'; +import { ICommentsServiceCreateComment } from './comments.service.interface'; export interface ICommentsRepositoryInsertComment { - createCommentDto: CreateCommentDto; + createCommentDto: ICommentsServiceCreateComment; } export interface ICommentsRepositoryfetchComments { - postsId: number; + articleId: number; } export interface ICommentsRepositoryId { - id: number; + commentId: number; } diff --git a/src/APIs/comments/interfaces/comments.service.interface.ts b/src/APIs/comments/interfaces/comments.service.interface.ts index 36245e6..e401231 100644 --- a/src/APIs/comments/interfaces/comments.service.interface.ts +++ b/src/APIs/comments/interfaces/comments.service.interface.ts @@ -1,25 +1,32 @@ -export interface ICommentsServicePostsIdValidCheck { +import { CommentCreateRequestDto } from '../dtos/request/comment-create-request.dto'; + +export interface ICommentsServiceArticleIdValidCheck { parentId: number; - postsId: number; + articleId: number; } export interface ICommentsServiceId { - id: number; + commentId: number; } -export interface ICommentsServicePatch { - kakaoId: number; - postsId: number; - id: number; +export interface ICommentsServicePatchComment { + userId: number; + articleId: number; + commentId: number; content: string; } -export interface ICommentsServiceFetch { - postsId: number; +export interface ICommentsServiceFindComments { + articleId: number; +} + +export interface ICommentsServiceDeleteComment { + commentId: number; + userId: number; + articleId: number; } -export interface ICommentsServiceDelete { - id: number; - userKakaoId: number; - postsId: number; +export interface ICommentsServiceCreateComment extends CommentCreateRequestDto { + articleId: number; + userId: number; } diff --git a/src/APIs/feedbacks/dtos/common/feedback.dto.ts b/src/APIs/feedbacks/dtos/common/feedback.dto.ts new file mode 100644 index 0000000..0f60e25 --- /dev/null +++ b/src/APIs/feedbacks/dtos/common/feedback.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from "@nestjs/swagger"; +import { Feedback } from "../../entities/feedback.entity"; + +export class FeedbackDto extends OmitType(Feedback, ['user']) \ No newline at end of file diff --git a/src/APIs/feedbacks/dtos/create-feedback.dto.ts b/src/APIs/feedbacks/dtos/create-feedback.dto.ts deleted file mode 100644 index 68bf05e..0000000 --- a/src/APIs/feedbacks/dtos/create-feedback.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PickType } from '@nestjs/swagger'; -import { Feedback } from '../entities/feedback.entity'; - -export class CreateFeedbackInput extends PickType(Feedback, ['content']) {} diff --git a/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts b/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts deleted file mode 100644 index 4351808..0000000 --- a/src/APIs/feedbacks/dtos/fetch-feedback.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Feedback } from '../entities/feedback.entity'; - -export class FetchFeedbackDto extends OmitType(Feedback, ['user']) {} diff --git a/src/APIs/feedbacks/dtos/request/feedback-create-request.dto.ts b/src/APIs/feedbacks/dtos/request/feedback-create-request.dto.ts new file mode 100644 index 0000000..0a64d8a --- /dev/null +++ b/src/APIs/feedbacks/dtos/request/feedback-create-request.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { FeedbackDto } from '../common/feedback.dto'; + +export class FeedbackCreateRequestDto extends PickType(FeedbackDto, [ + 'content', +]) {} diff --git a/src/APIs/feedbacks/feedbacks.controller.ts b/src/APIs/feedbacks/feedbacks.controller.ts index 813e74d..27a9d48 100644 --- a/src/APIs/feedbacks/feedbacks.controller.ts +++ b/src/APIs/feedbacks/feedbacks.controller.ts @@ -8,10 +8,10 @@ import { ApiTags, } from '@nestjs/swagger'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { CreateFeedbackInput } from './dtos/create-feedback.dto'; import { Request } from 'express'; -import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; import { FeedbackType } from 'src/common/enums/feedback-type.enum'; +import { FeedbackDto } from './dtos/common/feedback.dto'; +import { FeedbackCreateRequestDto } from './dtos/request/feedback-create-request.dto'; @ApiTags('유저 API') @Controller('users') @@ -20,17 +20,17 @@ export class FeedbacksController { @ApiOperation({ summary: '피드백 작성하기' }) @ApiCookieAuth() - @ApiCreatedResponse({ type: FetchFeedbackDto }) + @ApiCreatedResponse({ type: FeedbackDto }) @UseGuards(AuthGuardV2) @Post('feedback') async createFeedback( - @Body() body: CreateFeedbackInput, + @Body() body: FeedbackCreateRequestDto, @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.feedbacksService.create({ + ): Promise { + const userId = req.user.userId; + return await this.feedbacksService.createFeedback({ ...body, - kakaoId, + userId, type: FeedbackType.GENERAL_FEEDBACK, }); } @@ -38,11 +38,11 @@ export class FeedbacksController { @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 피드백 내용 조회' }) @ApiCookieAuth() - @ApiOkResponse({ type: [FetchFeedbackDto] }) + @ApiOkResponse({ type: [FeedbackDto] }) @UseGuards(AuthGuardV2) @Get('admin/feedbacks') - async getFeedbacks(@Req() req: Request): Promise { - const kakaoId = req.user.userId; - return await this.feedbacksService.fetchAll({ kakaoId }); + async getFeedbacks(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.feedbacksService.fetchFeedbacks({ userId }); } } diff --git a/src/APIs/feedbacks/feedbacks.service.ts b/src/APIs/feedbacks/feedbacks.service.ts index 134257d..a9b3eaa 100644 --- a/src/APIs/feedbacks/feedbacks.service.ts +++ b/src/APIs/feedbacks/feedbacks.service.ts @@ -1,35 +1,35 @@ import { Injectable } from '@nestjs/common'; import { FeedbacksRepository } from './feedbacks.repository'; import { - IFeedbacksServiceCreate, - IFeedbacksServiceKakaoId, + IFeedbacksServiceCreateFeedback, + IFeedbacksServiceUserId, } from './interfaces/feedbacks.service.interface'; -import { FetchFeedbackDto } from './dtos/fetch-feedback.dto'; -import { UsersService } from '../users/users.service'; +import { FeedbackDto } from './dtos/common/feedback.dto'; +import { UsersValidateService } from '../users/services/users-validate-service'; @Injectable() export class FeedbacksService { constructor( - private readonly feedbacksRepository: FeedbacksRepository, - private readonly usersService: UsersService, + private readonly repo_feedbacks: FeedbacksRepository, + private readonly svc_usersValidate: UsersValidateService, ) {} - async create({ - kakaoId, + async createFeedback({ + userId, content, type, - }: IFeedbacksServiceCreate): Promise { - return await this.feedbacksRepository.save({ + }: IFeedbacksServiceCreateFeedback): Promise { + return await this.repo_feedbacks.save({ type, content, - userKakaoId: kakaoId, + userId: userId, }); } - async fetchAll({ - kakaoId, - }: IFeedbacksServiceKakaoId): Promise { - await this.usersService.adminCheck({ kakaoId }); - return await this.feedbacksRepository.find(); + async fetchFeedbacks({ + userId, + }: IFeedbacksServiceUserId): Promise { + await this.svc_usersValidate.adminCheck({ userId }); + return await this.repo_feedbacks.find(); } } diff --git a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts index 2704bd4..b48954d 100644 --- a/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts +++ b/src/APIs/feedbacks/interfaces/feedbacks.service.interface.ts @@ -1,10 +1,10 @@ import { Feedback } from '../entities/feedback.entity'; -export interface IFeedbacksServiceCreate +export interface IFeedbacksServiceCreateFeedback extends Pick { - kakaoId: number; + userId: number; } -export interface IFeedbacksServiceKakaoId { - kakaoId: number; +export interface IFeedbacksServiceUserId { + userId: number; } From 62939be6e30e44f0b770014a1c02ee3b6efd1b7a Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 22:22:39 +0900 Subject: [PATCH 216/236] refactor(Like): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/likes/dtos/common/like.dto.ts | 4 ++ .../likes/dtos/fetch-likes-response.dto.ts | 4 -- src/APIs/likes/dtos/fetch-likes.dto.ts | 18 ----- .../dtos/response/likes-get-response.dto.ts | 8 +++ .../likes/dtos/toggle-like-response.dto.ts | 11 --- .../interfaces/likes.repository.interface.ts | 4 +- .../interfaces/likes.service.interface.ts | 4 +- src/APIs/likes/likes.controller.ts | 40 +++++------ src/APIs/likes/likes.module.ts | 5 +- src/APIs/likes/likes.repository.ts | 16 ++--- src/APIs/likes/likes.service.ts | 68 ++++++++++--------- 11 files changed, 83 insertions(+), 99 deletions(-) create mode 100644 src/APIs/likes/dtos/common/like.dto.ts delete mode 100644 src/APIs/likes/dtos/fetch-likes-response.dto.ts delete mode 100644 src/APIs/likes/dtos/fetch-likes.dto.ts create mode 100644 src/APIs/likes/dtos/response/likes-get-response.dto.ts delete mode 100644 src/APIs/likes/dtos/toggle-like-response.dto.ts diff --git a/src/APIs/likes/dtos/common/like.dto.ts b/src/APIs/likes/dtos/common/like.dto.ts new file mode 100644 index 0000000..a8c7d55 --- /dev/null +++ b/src/APIs/likes/dtos/common/like.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Like } from '../../entities/like.entity'; + +export class LikeDto extends OmitType(Like, ['article', 'user']) {} diff --git a/src/APIs/likes/dtos/fetch-likes-response.dto.ts b/src/APIs/likes/dtos/fetch-likes-response.dto.ts deleted file mode 100644 index e93b70e..0000000 --- a/src/APIs/likes/dtos/fetch-likes-response.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Likes } from '../entities/like.entity'; -import { PickType } from '@nestjs/swagger'; - -export class FetchLikesResponseDto extends PickType(Likes, ['id', 'user']) {} diff --git a/src/APIs/likes/dtos/fetch-likes.dto.ts b/src/APIs/likes/dtos/fetch-likes.dto.ts deleted file mode 100644 index cb8b40a..0000000 --- a/src/APIs/likes/dtos/fetch-likes.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; -import { Likes } from '../entities/like.entity'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; - -export class FetchLikesDto { - @ApiProperty({ type: Number, description: 'post_id' }) - postsId: number; -} - -export class FetchLikeResponseDto extends PickType(Likes, ['id', 'postsId']) { - @ApiProperty({ - type: OmitType(Posts, ['user', 'postCategory', 'postBackground']), - }) - posts: Omit; - - @ApiProperty({ type: Number }) - userKakaoId: number; -} diff --git a/src/APIs/likes/dtos/response/likes-get-response.dto.ts b/src/APIs/likes/dtos/response/likes-get-response.dto.ts new file mode 100644 index 0000000..31e3d5e --- /dev/null +++ b/src/APIs/likes/dtos/response/likes-get-response.dto.ts @@ -0,0 +1,8 @@ +import { LikeDto } from '../common/like.dto'; +import { PickType } from '@nestjs/swagger'; + +export class LikesGetResponseDto extends PickType(LikeDto, [ + 'id', + 'articleId', + 'userId', +]) {} diff --git a/src/APIs/likes/dtos/toggle-like-response.dto.ts b/src/APIs/likes/dtos/toggle-like-response.dto.ts deleted file mode 100644 index 254837b..0000000 --- a/src/APIs/likes/dtos/toggle-like-response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Posts } from 'src/APIs/articles/entities/articles.entity'; -import { Likes } from '../entities/like.entity'; - -export class ToggleLikeResponseDto extends OmitType(Posts, [ - 'postBackground', - 'postCategory', - 'user', -]) {} - -export class FetchLikeDto extends OmitType(Likes, ['user', 'posts']) {} diff --git a/src/APIs/likes/interfaces/likes.repository.interface.ts b/src/APIs/likes/interfaces/likes.repository.interface.ts index fb57c90..29f69a1 100644 --- a/src/APIs/likes/interfaces/likes.repository.interface.ts +++ b/src/APIs/likes/interfaces/likes.repository.interface.ts @@ -1,4 +1,4 @@ export interface ILikesRepositoryIds { - id: number; - kakaoId: number; + articleId: number; + userId: number; } diff --git a/src/APIs/likes/interfaces/likes.service.interface.ts b/src/APIs/likes/interfaces/likes.service.interface.ts index 916c1b1..56793d8 100644 --- a/src/APIs/likes/interfaces/likes.service.interface.ts +++ b/src/APIs/likes/interfaces/likes.service.interface.ts @@ -1,4 +1,4 @@ export interface ILikesServiceIds { - id: number; - kakaoId: number; + articleId: number; + userId: number; } diff --git a/src/APIs/likes/likes.controller.ts b/src/APIs/likes/likes.controller.ts index c4d19c7..7644a66 100644 --- a/src/APIs/likes/likes.controller.ts +++ b/src/APIs/likes/likes.controller.ts @@ -20,13 +20,13 @@ import { } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; +import { LikesGetResponseDto } from './dtos/response/likes-get-response.dto'; @ApiTags('게시글 API') -@Controller('posts/:postId') +@Controller('articles/:articleId') export class LikesController { - constructor(private readonly likesService: LikesService) {} + constructor(private readonly svc_likes: LikesService) {} @ApiOperation({ summary: '좋아요', @@ -35,18 +35,18 @@ export class LikesController { @ApiCookieAuth() @ApiCreatedResponse({ description: '좋아요 성공', - type: FetchLikeResponseDto, + type: LikesGetResponseDto, }) @ApiNotFoundResponse({ description: '게시글을 찾을 수 없는 경우' }) @UseGuards(AuthGuardV2) @HttpCode(201) @Post('like') async like( - @Param('postId') id: number, + @Param('articleId') articleId: number, @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.likesService.like({ id, kakaoId }); + ): Promise { + const userId = req.user.userId; + return await this.svc_likes.like({ userId, articleId }); } @ApiOperation({ @@ -62,11 +62,11 @@ export class LikesController { @HttpCode(204) @Delete('like') async deleteLike( - @Param('postId') id: number, + @Param('articleId') articleId: number, @Req() req: Request, ): Promise { - const kakaoId = req.user.userId; - await this.likesService.cancel_like({ id, kakaoId }); + const userId = req.user.userId; + await this.svc_likes.cancleLike({ articleId, userId }); return; } @@ -79,11 +79,11 @@ export class LikesController { @UseGuards(AuthGuardV2) @Get('like') async fetchIfLiked( - @Param('postId') id: number, + @Param('articleId') articleId: number, @Req() req: Request, ): Promise { - const kakaoId = req.user.userId; - return await this.likesService.fetchIfLiked({ kakaoId, id }); + const userId = req.user.userId; + return await this.svc_likes.checkIfLiked({ userId, articleId }); } @ApiOperation({ @@ -92,15 +92,15 @@ export class LikesController { }) @ApiOkResponse({ description: '조회 성공', - type: [UserResponseDtoWithFollowing], + type: [UserFollowingResponseDto], }) @HttpCode(200) @Get('like-users') async fetchLikes( - @Param('postId') id: number, + @Param('articleId') articleId: number, @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.likesService.fetchLikes({ id, kakaoId }); + ): Promise { + const userId = req.user.userId; + return await this.svc_likes.findLikes({ articleId, userId }); } } diff --git a/src/APIs/likes/likes.module.ts b/src/APIs/likes/likes.module.ts index d87b6fd..b3180d1 100644 --- a/src/APIs/likes/likes.module.ts +++ b/src/APIs/likes/likes.module.ts @@ -1,14 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Posts } from '../articles/entities/article.entity'; import { LikesController } from './likes.controller'; import { LikesService } from './likes.service'; -import { Likes } from './entities/like.entity'; import { LikesRepository } from './likes.repository'; import { NotificationsModule } from '../notifications/notifications.module'; +import { Like } from './entities/like.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Posts, Likes]), NotificationsModule], + imports: [TypeOrmModule.forFeature([Like]), NotificationsModule], providers: [LikesService, LikesRepository], controllers: [LikesController], }) diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index 77f7f1b..7b5ff74 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -1,19 +1,19 @@ import { DataSource, Repository } from 'typeorm'; -import { Likes } from './entities/like.entity'; import { Follow } from '../follows/entities/follow.entity'; import { Injectable } from '@nestjs/common'; import { ILikesRepositoryIds } from './interfaces/likes.repository.interface'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; +import { Like } from './entities/like.entity'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; @Injectable() -export class LikesRepository extends Repository { +export class LikesRepository extends Repository { constructor(private dataSource: DataSource) { - super(Likes, dataSource.createEntityManager()); + super(Like, dataSource.createEntityManager()); } async getLikes({ - kakaoId, - id, - }: ILikesRepositoryIds): Promise { + userId, + articleId, + }: ILikesRepositoryIds): Promise { const users = await this.createQueryBuilder('likes') .innerJoin('likes.posts', 'posts') .leftJoin('likes.user', 'user') @@ -44,7 +44,7 @@ export class LikesRepository extends Repository { 'user.date_deleted AS date_deleted', 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) - .setParameters({ id, kakaoId }) + .setParameters({ articleId, userId }) .getRawMany(); return users.map((user) => ({ diff --git a/src/APIs/likes/likes.service.ts b/src/APIs/likes/likes.service.ts index 2947789..56628b2 100644 --- a/src/APIs/likes/likes.service.ts +++ b/src/APIs/likes/likes.service.ts @@ -1,13 +1,13 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { Likes } from './entities/like.entity'; -import { Posts } from '../articles/entities/article.entity'; -import { FetchLikeResponseDto } from './dtos/fetch-likes.dto'; import { LikesRepository } from './likes.repository'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { ILikesServiceIds } from './interfaces/likes.service.interface'; import { NotificationsService } from '../notifications/notifications.service'; import { NotType } from 'src/common/enums/not-type.enum'; +import { LikesGetResponseDto } from './dtos/response/likes-get-response.dto'; +import { Article } from '../articles/entities/article.entity'; +import { Like } from './entities/like.entity'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; @Injectable() export class LikesService { @@ -17,42 +17,48 @@ export class LikesService { private readonly notificationsService: NotificationsService, ) {} - async fetchIfLiked({ kakaoId, id }: ILikesServiceIds): Promise { + async checkIfLiked({ + userId, + articleId, + }: ILikesServiceIds): Promise { const alreadyLiked = await this.likesRepository.findOne({ - where: { posts: { id }, user: { kakaoId } }, + where: { article: { id: articleId }, user: { id: userId } }, }); if (alreadyLiked) return true; return false; } - async like({ id, kakaoId }: ILikesServiceIds): Promise { + async like({ + articleId, + userId, + }: ILikesServiceIds): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - const postData = await queryRunner.manager.findOne(Posts, { - where: { id }, + const articleData = await queryRunner.manager.findOne(Article, { + where: { id: articleId }, }); const alreadyLiked = await this.likesRepository.findOne({ - where: { posts: { id }, user: { kakaoId } }, + where: { articleId, userId }, }); if (alreadyLiked) { throw new ConflictException('이미 좋아요 한 게시글입니다.'); } else { - const likeData = await queryRunner.manager.save(Likes, { - userKakaoId: kakaoId, - posts: postData, + const likeData = await queryRunner.manager.save(Like, { + userId, + articleId, }); - await queryRunner.manager.update(Posts, postData.id, { - like_count: () => 'like_count +1', + await queryRunner.manager.update(Article, articleData.id, { + likeCount: () => 'like_count +1', }); await await queryRunner.commitTransaction(); - if (kakaoId != postData.userKakaoId) { + if (userId != articleData.userId) { await this.notificationsService.emitAlarm({ - userKakaoId: kakaoId, - targetUserKakaoId: postData.userKakaoId, + userId, + targetUserId: articleData.userId, type: NotType.LIKE, - postId: postData.id, + articleId: articleData.id, }); } return likeData; @@ -65,25 +71,25 @@ export class LikesService { } } - async cancel_like({ id, kakaoId }: ILikesServiceIds): Promise { + async cancleLike({ articleId, userId }: ILikesServiceIds): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - const postData = await queryRunner.manager.findOne(Posts, { - where: { id }, + const articleData = await queryRunner.manager.findOne(Article, { + where: { id: articleId }, }); const alreadyLiked = await this.likesRepository.findOne({ - where: { posts: { id }, user: { kakaoId } }, + where: { articleId, userId }, }); if (!alreadyLiked) { throw new ConflictException('좋아요 내역을 찾을 수 없습니다.'); } else { - await queryRunner.manager.delete(Likes, { + await queryRunner.manager.delete(Like, { id: alreadyLiked.id, }); - await queryRunner.manager.update(Posts, postData.id, { - like_count: () => 'like_count -1', + await queryRunner.manager.update(Article, articleData.id, { + likeCount: () => 'like_count -1', }); await queryRunner.commitTransaction(); return; @@ -96,10 +102,10 @@ export class LikesService { } } - async fetchLikes({ - id, - kakaoId, - }: ILikesServiceIds): Promise { - return await this.likesRepository.getLikes({ id, kakaoId }); + async findLikes({ + articleId, + userId, + }: ILikesServiceIds): Promise { + return await this.likesRepository.getLikes({ articleId, userId }); } } From a23e2ad2b48b3dc371ff13041db9c66adf79ad23 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 22:41:32 +0900 Subject: [PATCH 217/236] refactor(Report): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- src/APIs/reports/dtos/common/report.dto.ts | 9 +++ src/APIs/reports/dtos/create-report.dto.ts | 21 ------- src/APIs/reports/dtos/fetch-report.dto.ts | 8 --- .../dtos/request/report-create-request.dto.ts | 4 ++ .../interfaces/reports.service.interface.ts | 6 ++ src/APIs/reports/reports.controller.ts | 42 ++++++------- src/APIs/reports/reports.service.ts | 60 +++++++++++-------- src/common/enums/report-target.enum.ts | 2 +- 8 files changed, 75 insertions(+), 77 deletions(-) create mode 100644 src/APIs/reports/dtos/common/report.dto.ts delete mode 100644 src/APIs/reports/dtos/create-report.dto.ts delete mode 100644 src/APIs/reports/dtos/fetch-report.dto.ts create mode 100644 src/APIs/reports/dtos/request/report-create-request.dto.ts create mode 100644 src/APIs/reports/interfaces/reports.service.interface.ts diff --git a/src/APIs/reports/dtos/common/report.dto.ts b/src/APIs/reports/dtos/common/report.dto.ts new file mode 100644 index 0000000..ee48618 --- /dev/null +++ b/src/APIs/reports/dtos/common/report.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from '@nestjs/swagger'; +import { Report } from '../../entities/report.entity'; + +export class ReportDto extends OmitType(Report, [ + 'article', + 'comment', + 'targetUser', + 'user', +]) {} diff --git a/src/APIs/reports/dtos/create-report.dto.ts b/src/APIs/reports/dtos/create-report.dto.ts deleted file mode 100644 index 875190b..0000000 --- a/src/APIs/reports/dtos/create-report.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Report } from '../entities/report.entity'; - -export class CreateReportDto extends OmitType(Report, [ - 'id', - 'user', - 'post', - 'postId', - 'comment', - 'commentId', - 'date_created', -]) { - @ApiProperty({ type: Number, description: '신고할 게시글/댓글의 아이디' }) - targetId: number; -} - -export class CreateReportInput extends OmitType(CreateReportDto, [ - 'userKakaoId', - 'target', - 'targetId', -]) {} diff --git a/src/APIs/reports/dtos/fetch-report.dto.ts b/src/APIs/reports/dtos/fetch-report.dto.ts deleted file mode 100644 index fb05786..0000000 --- a/src/APIs/reports/dtos/fetch-report.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { OmitType } from '@nestjs/swagger'; -import { Report } from '../entities/report.entity'; - -export class FetchReportResponse extends OmitType(Report, [ - 'user', - 'post', - 'comment', -]) {} diff --git a/src/APIs/reports/dtos/request/report-create-request.dto.ts b/src/APIs/reports/dtos/request/report-create-request.dto.ts new file mode 100644 index 0000000..8e4915a --- /dev/null +++ b/src/APIs/reports/dtos/request/report-create-request.dto.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { Report } from '../../entities/report.entity'; + +export class ReportCreateRequestDto extends PickType(Report, ['content']) {} diff --git a/src/APIs/reports/interfaces/reports.service.interface.ts b/src/APIs/reports/interfaces/reports.service.interface.ts new file mode 100644 index 0000000..f425879 --- /dev/null +++ b/src/APIs/reports/interfaces/reports.service.interface.ts @@ -0,0 +1,6 @@ +import { ReportDto } from '../dtos/common/report.dto'; + +export interface IReportsServiceCreateReport + extends Pick { + targetId: number; +} diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index fe5c763..7a1821c 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -16,11 +16,11 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { CreateReportInput } from './dtos/create-report.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { Request } from 'express'; -import { FetchReportResponse } from './dtos/fetch-report.dto'; import { ReportTarget } from 'src/common/enums/report-target.enum'; +import { ReportDto } from './dtos/common/report.dto'; +import { ReportCreateRequestDto } from './dtos/request/report-create-request.dto'; @Controller('') export class ReportsController { @@ -31,20 +31,20 @@ export class ReportsController { summary: '게시물 신고', }) @ApiCookieAuth() - @ApiCreatedResponse({ type: FetchReportResponse }) + @ApiCreatedResponse({ type: ReportDto }) @UseGuards(AuthGuardV2) - @Post('posts/:postId/report') + @Post('articles/:articleId/report') @HttpCode(201) async reportPost( @Req() req: Request, - @Body() body: CreateReportInput, - @Param('postId') targetId: number, - ): Promise { - const userKakaoId = req.user.userId; - return await this.reportsService.create({ + @Body() body: ReportCreateRequestDto, + @Param('articleId') targetId: number, + ): Promise { + const userId = req.user.userId; + return await this.reportsService.createReport({ targetId, target: ReportTarget.POSTS, - userKakaoId, + userId, ...body, }); } @@ -54,20 +54,20 @@ export class ReportsController { summary: '댓글 신고', }) @ApiCookieAuth() - @ApiCreatedResponse({ type: FetchReportResponse }) + @ApiCreatedResponse({ type: ReportDto }) @UseGuards(AuthGuardV2) - @Post('posts/comments/:commentId/report') + @Post('articles/comments/:commentId/report') @HttpCode(201) async reportComment( @Req() req: Request, - @Body() body: CreateReportInput, + @Body() body: ReportCreateRequestDto, @Param('commentId') targetId: number, - ): Promise { - const userKakaoId = req.user.userId; - return await this.reportsService.create({ + ): Promise { + const userId = req.user.userId; + return await this.reportsService.createReport({ targetId, target: ReportTarget.COMMENTS, - userKakaoId, + userId, ...body, }); } @@ -78,11 +78,11 @@ export class ReportsController { summary: '[어드민용] 신고 내역 조회', }) @ApiCookieAuth() - @ApiOkResponse({ type: [FetchReportResponse] }) + @ApiOkResponse({ type: [ReportDto] }) @UseGuards(AuthGuardV2) @Get('users/admin/reports') - async fetchAll(@Req() req: Request): Promise { - const kakaoId = req.user.userId; - return await this.reportsService.fetchAll({ kakaoId }); + async fetchAll(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.reportsService.findReports({ userId }); } } diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index 288194e..afc8f26 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -6,49 +6,54 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Report } from './entities/report.entity'; import { DataSource, Repository } from 'typeorm'; -import { CreateReportDto } from './dtos/create-report.dto'; -import { UsersService } from '../users/users.service'; -import { FetchReportResponse } from './dtos/fetch-report.dto'; import { ReportTarget } from 'src/common/enums/report-target.enum'; -import { Posts } from '../articles/entities/article.entity'; +import { Article } from '../articles/entities/article.entity'; import { Comment } from '../comments/entities/comment.entity'; +import { IReportsServiceCreateReport } from './interfaces/reports.service.interface'; +import { ReportDto } from './dtos/common/report.dto'; +import { UsersValidateService } from '../users/services/users-validate-service'; @Injectable() export class ReportsService { constructor( @InjectRepository(Report) - private readonly reportsRepository: Repository, - private readonly usersService: UsersService, - private readonly dataSource: DataSource, + private readonly repo_reports: Repository, + private readonly svc_userValidate: UsersValidateService, + private readonly db_dataSource: DataSource, ) {} - async create(dto: CreateReportDto): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + async createReport( + dto_createReport: IReportsServiceCreateReport, + ): Promise { + const { target, userId, content, targetId } = dto_createReport; + const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - const { targetId, ...rest } = dto; try { let data; - switch (dto.target) { + switch (target) { case ReportTarget.POSTS: - const postData = await queryRunner.manager.findOne(Posts, { + const articleData = await queryRunner.manager.findOne(Article, { where: { id: targetId }, }); - if (!postData) + if (!articleData) throw new BadRequestException('게시글이 존재하지 않습니다.'); - const reportPost = await this.reportsRepository.findOne({ - where: { userKakaoId: dto.userKakaoId, postId: targetId }, + const reportPost = await this.repo_reports.findOne({ + where: { userId, articleId: targetId }, }); if (reportPost) throw new ConflictException('이미 신고한 게시물입니다.'); - await queryRunner.manager.update(Posts, postData.id, { - report_count: () => 'report_count +1', + await queryRunner.manager.update(Article, articleData.id, { + reportCount: () => 'report_count +1', }); data = await queryRunner.manager.save(Report, { - ...rest, - postId: targetId, + target, + userId, + targetUserId: articleData.userId, + content, + articleId: targetId, }); break; @@ -59,17 +64,20 @@ export class ReportsService { if (!commentData) throw new BadRequestException('댓글이 존재하지 않습니다.'); - const reportComment = await this.reportsRepository.findOne({ - where: { userKakaoId: dto.userKakaoId, commentId: targetId }, + const reportComment = await this.repo_reports.findOne({ + where: { userId, commentId: targetId }, }); if (reportComment) throw new ConflictException('이미 신고한 게시물입니다.'); await queryRunner.manager.update(Comment, commentData.id, { - report_count: () => 'report_count +1', + reportCount: () => 'report_count +1', }); data = await queryRunner.manager.save(Report, { - ...rest, + target, + userId, + targetUserId: commentData.userId, + content, commentId: targetId, }); break; @@ -87,9 +95,9 @@ export class ReportsService { } } - async fetchAll({ kakaoId }): Promise { - await this.usersService.adminCheck({ kakaoId }); - const result = await this.reportsRepository.find(); + async findReports({ userId }): Promise { + await this.svc_userValidate.adminCheck({ userId }); + const result = await this.repo_reports.find(); return result; } } diff --git a/src/common/enums/report-target.enum.ts b/src/common/enums/report-target.enum.ts index 0ebc712..fd1f998 100644 --- a/src/common/enums/report-target.enum.ts +++ b/src/common/enums/report-target.enum.ts @@ -1,4 +1,4 @@ export enum ReportTarget { - POSTS = 'POSTS', + POSTS = 'ARTICLES', COMMENTS = 'COMMENTS', } From c0b822e933f144c22c141df536f399b312a1d8cb Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 23:03:37 +0900 Subject: [PATCH 218/236] refactor(StickerBlock): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- ...ate.dto.ts => stickerBlocks-create.dto.ts} | 0 .../stickerBlock-create-request.dto.ts | 8 --- .../stickerBlocks-create-request.dto.ts | 2 +- .../stickerBlocks.service.interface.ts | 18 +++++-- .../stickerBlocks/stickerBlocks.controller.ts | 51 +++++-------------- .../stickerBlocks/stickerBlocks.service.ts | 51 +++++++++---------- 6 files changed, 52 insertions(+), 78 deletions(-) rename src/APIs/stickerBlocks/dtos/common/{stickerBlocks.create.dto.ts => stickerBlocks-create.dto.ts} (100%) diff --git a/src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts b/src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto.ts similarity index 100% rename from src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto.ts rename to src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto.ts diff --git a/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts b/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts index 9a7df85..8bac340 100644 --- a/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts +++ b/src/APIs/stickerBlocks/dtos/request/stickerBlock-create-request.dto.ts @@ -1,5 +1,4 @@ import { OmitType } from '@nestjs/swagger'; -import { StickerBlock } from '../entities/stickerblock.entity'; import { StickerBlockDto } from '../common/stickerBlock.dto'; export class StickerBlockCreateRequestDto extends OmitType(StickerBlockDto, [ @@ -10,10 +9,3 @@ export class StickerBlockCreateRequestDto extends OmitType(StickerBlockDto, [ 'dateDeleted', 'dateUpdated', ]) {} - -//인터페이스화? -export class CreateStickerBlockDto extends StickerBlockCreateRequestDto { - articleId: number; - stickerId: number; - userId: number; -} diff --git a/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts b/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts index bd36715..ecb1f37 100644 --- a/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts +++ b/src/APIs/stickerBlocks/dtos/request/stickerBlocks-create-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { StickerBlocksCreateDto } from '../common/stickerBlocks.create.dto'; +import { StickerBlocksCreateDto } from '../common/stickerBlocks-create.dto'; import { IsArray, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; diff --git a/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts b/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts index 91c72e5..11ab234 100644 --- a/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts +++ b/src/APIs/stickerBlocks/interfaces/stickerBlocks.service.interface.ts @@ -1,8 +1,20 @@ +import { StickerBlockCreateRequestDto } from '../dtos/request/stickerBlock-create-request.dto'; +import { StickerBlocksCreateRequestDto } from '../dtos/request/stickerBlocks-create-request.dto'; + export interface IStikcerBlocksServiceFetchBlocks { - postsId: number; + articleId: number; } export interface IStikcerBlocksServiceDeleteBlocks { - kakaoId: number; - postsId: number; + userId: number; + articleId: number; +} +export interface IStickerBlocksServiceCreateStickerBlock + extends StickerBlockCreateRequestDto { + stickerId: number; + articleId: number; +} +export interface IStickerBlocksServiceCreateStickerBlocks + extends StickerBlocksCreateRequestDto { + articleId: number; } diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index f1479f8..c5c5d97 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common'; import { ApiCookieAuth, ApiCreatedResponse, @@ -6,41 +6,15 @@ import { ApiTags, } from '@nestjs/swagger'; import { StickerBlocksService } from './stickerBlocks.service'; -import { - CreateStickerBlocksInput, - CreateStickerBlocksResponseDto, -} from './dtos/create-stickerBlocks.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { Request } from 'express'; +import { StickerBlockDto } from './dtos/common/stickerBlock.dto'; +import { StickerBlocksCreateRequestDto } from './dtos/request/stickerBlocks-create-request.dto'; @ApiTags('게시글 API') -@Controller('posts/:postId/stickers') +@Controller('articles/:articleId/stickers') export class StickerBlocksController { constructor(private readonly stickerBlocksService: StickerBlocksService) {} - // @ApiOperation({ - // summary: '게시글 속 스티커 생성', - // description: - // '게시글과 스티커 아이디를 매핑한 스티커 블록을 생성한다. 세부 스타일 좌표값을 저장한다.', - // }) - // @ApiCookieAuth() - // @UseGuards(AuthGuardV2) - // @Post(':stickerId') - // async createStickerBlock( - // @Body() body: CreateStickerBlockInput, - // @Param('postId') postsId: number, - // @Param('stickerId') stickerId: number, - // @Req() req: Request, - // ) { - // const kakaoId = req.user.userId; - // return await this.stickerBlocksService.create({ - // ...body, - // kakaoId, - // postsId, - // stickerId, - // }); - // } - @ApiOperation({ summary: '게시글 속 스티커 생성', description: @@ -48,18 +22,17 @@ export class StickerBlocksController { }) @ApiCookieAuth() @UseGuards(AuthGuardV2) - @ApiCreatedResponse({ type: [CreateStickerBlocksResponseDto] }) + @ApiCreatedResponse({ type: [StickerBlockDto] }) @Post('bulk') async createStickerBlocks( - @Body() body: CreateStickerBlocksInput, - @Param('postId') postsId: number, - @Req() req: Request, - ): Promise { - const kakaoId = req.user.userId; - return await this.stickerBlocksService.bulkInsert({ + @Body() body: StickerBlocksCreateRequestDto, + @Param('articleId') articleId: number, + // @Req() req: Request, + ): Promise { + // const userId = req.user.userId; + return await this.stickerBlocksService.createStickerBlocks({ ...body, - postsId, - kakaoId, + articleId, }); } } diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 39030f8..ca7f831 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -2,16 +2,14 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { StickerBlock } from './entities/stickerblock.entity'; import { Repository } from 'typeorm'; -import { CreateStickerBlockDto } from './dtos/create-stickerBlock.dto'; import { StickersService } from '../stickers/stickers.service'; import { - CreateStickerBlocksDto, - CreateStickerBlocksResponseDto, -} from './dtos/create-stickerBlocks.dto'; -import { + IStickerBlocksServiceCreateStickerBlock, + IStickerBlocksServiceCreateStickerBlocks, IStikcerBlocksServiceDeleteBlocks, IStikcerBlocksServiceFetchBlocks, } from './interfaces/stickerBlocks.service.interface'; +import { StickerBlockDto } from './dtos/common/stickerBlock.dto'; @Injectable() export class StickerBlocksService { @@ -21,35 +19,34 @@ export class StickerBlocksService { private readonly stickerBlocksRepository: Repository, ) {} - async create( - createStickerBlockDto: CreateStickerBlockDto, - ): Promise { - // 순환참조 막기 위해 자체 에러 헨들링 - // await this.postsService.existCheck({ - // id: createStickerBlockDto.postsId, - // }); + async createStickerBlock({ + stickerId, + articleId, + ...rest + }: IStickerBlocksServiceCreateStickerBlock): Promise { try { await this.stickersService.existCheck({ - id: createStickerBlockDto.stickerId, + id: stickerId, }); - const data = await this.stickerBlocksRepository.save( - createStickerBlockDto, - ); + const data = await this.stickerBlocksRepository.save({ + ...rest, + articleId, + stickerId, + }); return data; } catch (e) { throw new NotFoundException('게시글을 찾을 수 없습니다.'); } } - async bulkInsert({ + async createStickerBlocks({ stickerBlocks, - postsId, - kakaoId, - }: CreateStickerBlocksDto): Promise { + articleId, + }: IStickerBlocksServiceCreateStickerBlocks): Promise { const stickerBlocksToInsert = stickerBlocks.map((stickerBlock) => ({ ...stickerBlock, - postsId, + articleId, })); stickerBlocksToInsert.forEach(async (stickerBlock) => { await this.stickersService.existCheck({ @@ -59,15 +56,15 @@ export class StickerBlocksService { return await this.stickerBlocksRepository.save(stickerBlocksToInsert); } - async fetchBlocks({ - postsId, - }: IStikcerBlocksServiceFetchBlocks): Promise { + async findStickerBlocks({ + articleId, + }: IStikcerBlocksServiceFetchBlocks): Promise { return await this.stickerBlocksRepository.find({ - where: { postsId }, + where: { articleId }, }); } - async deleteBlocks({ + async deleteStickerBlocks({ userId, articleId, }: IStikcerBlocksServiceDeleteBlocks): Promise { @@ -83,5 +80,5 @@ export class StickerBlocksService { return; } - async updateBlock() {} + async updateStickerBlocks() {} } From 760aa70925630bc14a3bc3e21ff864be90583643 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 23:18:48 +0900 Subject: [PATCH 219/236] refactor(Sticker): Apply naming conventions and update linked values with article & user - Restructure and rename DTOs according to naming conventions - Update variable names to follow naming conventions - Rename methods to adhere to naming conventions - Adjust linked values to reflect changes in user & article modules --- .../stickerBlocks/stickerBlocks.controller.ts | 4 +- .../stickerBlocks/stickerBlocks.service.ts | 20 +-- src/APIs/stickers/dtos/common/sticker.dto.ts | 4 + src/APIs/stickers/dtos/create-sticker.dto.ts | 8 -- src/APIs/stickers/dtos/find-sticker.dto.ts | 13 -- src/APIs/stickers/dtos/remove-bg.dto.ts | 11 -- .../request/sticker-create-request.dto.ts | 8 ++ .../sticker-patch-request.dto.ts} | 5 +- .../interfaces/stickers.service.interface.ts | 24 +++- src/APIs/stickers/stickers.controller.ts | 62 ++++----- src/APIs/stickers/stickers.service.ts | 127 +++++++++--------- 11 files changed, 137 insertions(+), 149 deletions(-) create mode 100644 src/APIs/stickers/dtos/common/sticker.dto.ts delete mode 100644 src/APIs/stickers/dtos/create-sticker.dto.ts delete mode 100644 src/APIs/stickers/dtos/find-sticker.dto.ts delete mode 100644 src/APIs/stickers/dtos/remove-bg.dto.ts create mode 100644 src/APIs/stickers/dtos/request/sticker-create-request.dto.ts rename src/APIs/stickers/dtos/{update-sticker.dto.ts => request/sticker-patch-request.dto.ts} (89%) diff --git a/src/APIs/stickerBlocks/stickerBlocks.controller.ts b/src/APIs/stickerBlocks/stickerBlocks.controller.ts index c5c5d97..414dcb6 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.controller.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.controller.ts @@ -13,7 +13,7 @@ import { StickerBlocksCreateRequestDto } from './dtos/request/stickerBlocks-crea @ApiTags('게시글 API') @Controller('articles/:articleId/stickers') export class StickerBlocksController { - constructor(private readonly stickerBlocksService: StickerBlocksService) {} + constructor(private readonly svc_stickerBlocks: StickerBlocksService) {} @ApiOperation({ summary: '게시글 속 스티커 생성', @@ -30,7 +30,7 @@ export class StickerBlocksController { // @Req() req: Request, ): Promise { // const userId = req.user.userId; - return await this.stickerBlocksService.createStickerBlocks({ + return await this.svc_stickerBlocks.createStickerBlocks({ ...body, articleId, }); diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index ca7f831..48c2dc0 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -14,9 +14,9 @@ import { StickerBlockDto } from './dtos/common/stickerBlock.dto'; @Injectable() export class StickerBlocksService { constructor( - private readonly stickersService: StickersService, + private readonly svc_stickers: StickersService, @InjectRepository(StickerBlock) - private readonly stickerBlocksRepository: Repository, + private readonly repo_stickerBlocks: Repository, ) {} async createStickerBlock({ @@ -25,11 +25,11 @@ export class StickerBlocksService { ...rest }: IStickerBlocksServiceCreateStickerBlock): Promise { try { - await this.stickersService.existCheck({ + await this.svc_stickers.existCheck({ id: stickerId, }); - const data = await this.stickerBlocksRepository.save({ + const data = await this.repo_stickerBlocks.save({ ...rest, articleId, stickerId, @@ -49,17 +49,17 @@ export class StickerBlocksService { articleId, })); stickerBlocksToInsert.forEach(async (stickerBlock) => { - await this.stickersService.existCheck({ + await this.svc_stickers.existCheck({ id: stickerBlock.stickerId, }); }); - return await this.stickerBlocksRepository.save(stickerBlocksToInsert); + return await this.repo_stickerBlocks.save(stickerBlocksToInsert); } async findStickerBlocks({ articleId, }: IStikcerBlocksServiceFetchBlocks): Promise { - return await this.stickerBlocksRepository.find({ + return await this.repo_stickerBlocks.find({ where: { articleId }, }); } @@ -68,14 +68,14 @@ export class StickerBlocksService { userId, articleId, }: IStikcerBlocksServiceDeleteBlocks): Promise { - const blocksToDelete = await this.stickerBlocksRepository.find({ + const blocksToDelete = await this.repo_stickerBlocks.find({ relations: ['sticker'], where: { articleId }, }); for (const block of blocksToDelete) { if (block.sticker.isReusable === false) - await this.stickersService.delete({ userId, id: block.id }); - await this.stickerBlocksRepository.remove(block); + await this.svc_stickers.delete({ userId, id: block.id }); + await this.repo_stickerBlocks.remove(block); } return; } diff --git a/src/APIs/stickers/dtos/common/sticker.dto.ts b/src/APIs/stickers/dtos/common/sticker.dto.ts new file mode 100644 index 0000000..a26b983 --- /dev/null +++ b/src/APIs/stickers/dtos/common/sticker.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Sticker } from '../../entities/sticker.entity'; + +export class StickerDto extends OmitType(Sticker, ['user', 'stickerBlocks']) {} diff --git a/src/APIs/stickers/dtos/create-sticker.dto.ts b/src/APIs/stickers/dtos/create-sticker.dto.ts deleted file mode 100644 index 30ca164..0000000 --- a/src/APIs/stickers/dtos/create-sticker.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateStickerDto { - @ApiProperty({ description: '유저의 카카오 아이디', type: Number }) - userKakaoId: number; - - file: Express.Multer.File; -} diff --git a/src/APIs/stickers/dtos/find-sticker.dto.ts b/src/APIs/stickers/dtos/find-sticker.dto.ts deleted file mode 100644 index 88d87dd..0000000 --- a/src/APIs/stickers/dtos/find-sticker.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber } from 'class-validator'; - -export class FindStickerInput { - @ApiProperty({ description: '찾을 스티커의 id', type: Number }) - @IsNumber() - id: number; -} - -export class FindStickerDto extends FindStickerInput { - @IsNumber() - kakaoId: number; -} diff --git a/src/APIs/stickers/dtos/remove-bg.dto.ts b/src/APIs/stickers/dtos/remove-bg.dto.ts deleted file mode 100644 index 969c5a9..0000000 --- a/src/APIs/stickers/dtos/remove-bg.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class RemoveBgDto { - @ApiProperty({ - description: '이미지가 저장된 url', - type: String, - }) - @IsString() - url: string; -} diff --git a/src/APIs/stickers/dtos/request/sticker-create-request.dto.ts b/src/APIs/stickers/dtos/request/sticker-create-request.dto.ts new file mode 100644 index 0000000..9653296 --- /dev/null +++ b/src/APIs/stickers/dtos/request/sticker-create-request.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StickerCreateRequestDto { + @ApiProperty({ description: '유저의 아이디', type: Number }) + userId: number; + + file: Express.Multer.File; +} diff --git a/src/APIs/stickers/dtos/update-sticker.dto.ts b/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts similarity index 89% rename from src/APIs/stickers/dtos/update-sticker.dto.ts rename to src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts index c715c6d..a472b12 100644 --- a/src/APIs/stickers/dtos/update-sticker.dto.ts +++ b/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsNumber, IsOptional, IsUrl } from 'class-validator'; -export class UpdateStickerInput { +export class StickerPatchRequestDto { @ApiProperty({ description: '변경할 url', type: String, required: false }) @IsUrl() @IsOptional() - image_url?: string; + imageUrl?: string; @ApiProperty({ description: '재사용 가능 여부 설정', @@ -16,6 +16,7 @@ export class UpdateStickerInput { @IsOptional() isReusable?: boolean; } + export class UpdateStickerDto extends UpdateStickerInput { @IsNumber() kakaoId: number; diff --git a/src/APIs/stickers/interfaces/stickers.service.interface.ts b/src/APIs/stickers/interfaces/stickers.service.interface.ts index 0a513ee..4f40319 100644 --- a/src/APIs/stickers/interfaces/stickers.service.interface.ts +++ b/src/APIs/stickers/interfaces/stickers.service.interface.ts @@ -1,12 +1,24 @@ +import { StickerPatchRequestDto } from '../dtos/request/sticker-patch-request.dto'; + +export interface IStickersServiceCreateSticker { + file: Express.Multer.File; + userId: number; +} + export interface IStickersServiceId { - id: number; + stickerId: number; +} + +export interface IStickersServiceDeleteSticker { + stickerId: number; + userId: number; } -export interface IStickersServiceDelete { - id: number; - kakaoId: number; +export interface IStickersServiceFindUserStickers { + userId: number; } -export interface IStickersServiceFetchUserStickers { - userKakaoId: number; +export interface IStickersServiceUpdateSticker extends StickerPatchRequestDto { + userId: number; + stickerId: number; } diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index f192727..095f3c0 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -23,12 +23,12 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; -import { Sticker } from './entities/sticker.entity'; -import { UpdateStickerInput } from './dtos/update-sticker.dto'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; +import { StickerDto } from './dtos/common/sticker.dto'; +import { StickerPatchRequestDto } from './dtos/request/sticker-patch-request.dto'; @ApiTags('스티커 API') @Controller() @@ -42,11 +42,11 @@ export class StickersController { @ApiConsumes('multipart/form-data') @ApiBody({ description: '업로드 할 파일', - type: ImageUploadDto, + type: ImageUploadRequestDto, }) @ApiCreatedResponse({ description: '이미지 서버에 파일 업로드 완료', - type: Sticker, + type: StickerDto, }) @UseGuards(AuthGuardV2) @ApiCookieAuth() @@ -56,10 +56,10 @@ export class StickersController { async createPrivateSticker( @Req() req: Request, @UploadedFile() file: Express.Multer.File, - ): Promise { - const userKakaoId = req.user.userId; + ): Promise { + const userId = req.user.userId; return await this.stickersService.createPrivateSticker({ - userKakaoId, + userId, file, }); } @@ -69,14 +69,14 @@ export class StickersController { description: '본인이 만든 재사용 가능한 스티커들을 fetch한다. toggle이 우선적으로 이루어져야함.', }) - @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) + @ApiOkResponse({ description: '조회 성공', type: [StickerDto] }) @UseGuards(AuthGuardV2) @ApiCookieAuth() @HttpCode(200) @Get('stickers/private') - async fetchPrivateStickers(@Req() req: Request): Promise { - const userKakaoId = req.user.userId; - return await this.stickersService.fetchUserStickers({ userKakaoId }); + async fetchPrivateStickers(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.stickersService.findUserStickers({ userId }); } @ApiOperation({ @@ -87,17 +87,17 @@ export class StickersController { @Patch('stickers/:id') @UseGuards(AuthGuardV2) @ApiCookieAuth() - @ApiOkResponse({ type: Sticker }) + @ApiOkResponse({ type: StickerDto }) @HttpCode(200) async patchSticker( @Req() req: Request, - @Param('id') id: number, - @Body() body: UpdateStickerInput, - ): Promise { - const kakaoId = req.user.userId; + @Param('stickerId') stickerId: number, + @Body() body: StickerPatchRequestDto, + ): Promise { + const userId = req.user.userId; return await this.stickersService.updateSticker({ - kakaoId, - id, + userId, + stickerId, ...body, }); } @@ -106,11 +106,11 @@ export class StickersController { summary: 'public 스티커를 fetch한다.', description: '블꾸가 만든 스티커들을 fetch한다.', }) - @ApiOkResponse({ description: '조회 성공', type: [Sticker] }) + @ApiOkResponse({ description: '조회 성공', type: [StickerDto] }) @Get('stickers') @HttpCode(200) - async fetchPublicStickers(): Promise { - return await this.stickersService.fetchPublicStickers(); + async fetchPublicStickers(): Promise { + return await this.stickersService.findPublicStickers(); } @ApiTags('어드민 API') @@ -122,11 +122,11 @@ export class StickersController { @ApiConsumes('multipart/form-data') @ApiBody({ description: '업로드 할 파일', - type: ImageUploadDto, + type: ImageUploadRequestDto, }) @ApiCreatedResponse({ description: '이미지 서버에 파일 업로드 완료', - type: Sticker, + type: StickerDto, }) @UseGuards(AuthGuardV2) @ApiCookieAuth() @@ -136,10 +136,10 @@ export class StickersController { async createPublicSticker( @Req() req: Request, @UploadedFile() file: Express.Multer.File, - ): Promise { - const userKakaoId = req.user.userId; + ): Promise { + const userId = req.user.userId; return await this.stickersService.createPublicSticker({ - userKakaoId, + userId, file, }); } @@ -148,12 +148,12 @@ export class StickersController { @UseGuards(AuthGuardV2) @ApiCookieAuth() @ApiNoContentResponse({ description: '삭제 성공' }) - @Delete('stickers/:id') + @Delete('stickers/:stickerId') async deleteSticker( @Req() req: Request, - @Param('id') id: number, + @Param('stickerId') stickerId: number, ): Promise { - const kakaoId = req.user.userId; - return await this.stickersService.delete({ id, kakaoId }); + const userId = req.user.userId; + return await this.stickersService.deleteSticker({ stickerId, userId }); } } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 3c14307..7791da5 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -2,30 +2,28 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Sticker } from './entities/sticker.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { CreateStickerDto } from './dtos/create-sticker.dto'; -import { UtilsService } from 'src/modules/utils/utils.service'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; -import { UsersService } from '../users/users.service'; -import { UpdateStickerDto } from './dtos/update-sticker.dto'; +import { UsersValidateService } from '../users/services/users-validate-service'; import { - IStickersServiceDelete, - IStickersServiceFetchUserStickers, + IStickersServiceCreateSticker, + IStickersServiceDeleteSticker, + IStickersServiceFindUserStickers, IStickersServiceId, + IStickersServiceUpdateSticker, } from './interfaces/stickers.service.interface'; -import { AwsService } from 'src/modules/aws/aws.service'; +import { ImagesService } from 'src/modules/images/images.service'; +import { StickerDto } from './dtos/common/sticker.dto'; @Injectable() export class StickersService { constructor( - private readonly awsService: AwsService, - private readonly utilsService: UtilsService, @InjectRepository(Sticker) - private readonly stickersRepository: Repository, - private readonly usersService: UsersService, + private readonly repo_stickers: Repository, + private readonly svc_usersValidate: UsersValidateService, + private readonly svc_images: ImagesService, ) {} - async findStickerById({ id }: IStickersServiceId): Promise { - return await this.stickersRepository.findOne({ where: { id } }); + async findStickerById({ id }: IStickersServiceId): Promise { + return await this.repo_stickers.findOne({ where: { id } }); } async existCheck({ id }: IStickersServiceId): Promise { @@ -33,110 +31,107 @@ export class StickersService { if (!data) throw new NotFoundException('스티커를 찾을 수 없습니다.'); } - async imageUpload( - file: Express.Multer.File, - ): Promise { - const imageName = this.utilsService.getUUID(); - const ext = file.originalname.split('.').pop(); - - const image_url = await this.awsService.imageUploadToS3( - `${imageName}.${ext}`, - file, - ext, - 1600, - ); - return { image_url }; - } async createPrivateSticker({ - userKakaoId, + userId, file, - }: CreateStickerDto): Promise { - const { image_url } = await this.imageUpload(file); - const insertData = await this.stickersRepository + }: IStickersServiceCreateSticker): Promise { + const { imageUrl } = await this.svc_images.imageUpload({ + file, + resize: 1600, + ext: 'png', + }); + const insertData = await this.repo_stickers .createQueryBuilder() .insert() - .into(Sticker, ['userKakaoId', 'image_url', 'isDefault']) - .values({ userKakaoId, image_url, isDefault: false }) + .into(Sticker, ['user_id', 'image_url', 'is_default']) + .values({ userId, imageUrl, isDefault: false }) .orUpdate(['image_url', 'isDefault'], ['id'], { skipUpdateIfNoValuesChanged: true, }) .execute(); const id = insertData.identifiers[0].id; - const data = await this.stickersRepository.findOne({ where: { id } }); + const data = await this.repo_stickers.findOne({ where: { id } }); return data; } async createPublicSticker({ - userKakaoId, + userId, file, - }: CreateStickerDto): Promise { - await this.usersService.adminCheck({ kakaoId: userKakaoId }); - const { image_url } = await this.imageUpload(file); - const insertData = await this.stickersRepository + }: IStickersServiceCreateSticker): Promise { + await this.svc_usersValidate.adminCheck({ userId }); + const { imageUrl } = await this.svc_images.imageUpload({ + file, + resize: 1600, + ext: 'png', + }); + const insertData = await this.repo_stickers .createQueryBuilder() .insert() - .into(Sticker, ['userKakaoId', 'image_url', 'isDefault', 'isReusable']) - .values({ userKakaoId, image_url, isDefault: true, isReusable: true }) + .into(Sticker, ['user_id', 'image_url', 'is_default', 'is_reusable']) + .values({ userId, imageUrl, isDefault: true, isReusable: true }) .orUpdate(['image_url', 'isDefault', 'isReusable'], ['id'], { skipUpdateIfNoValuesChanged: true, }) .execute(); const id = insertData.identifiers[0].id; - const data = await this.stickersRepository.findOne({ where: { id } }); + const data = await this.repo_stickers.findOne({ where: { id } }); return data; } - async fetchUserStickers({ - userKakaoId, - }: IStickersServiceFetchUserStickers): Promise { - return await this.stickersRepository.find({ - where: { userKakaoId, isReusable: true, isDefault: false }, + async findUserStickers({ + userId, + }: IStickersServiceFindUserStickers): Promise { + return await this.repo_stickers.find({ + where: { userId, isReusable: true, isDefault: false }, }); } - async fetchPublicStickers(): Promise { - return await this.stickersRepository.find({ + async findPublicStickers(): Promise { + return await this.repo_stickers.find({ where: { isDefault: true }, }); } async updateSticker({ - image_url, + imageUrl, isReusable, - kakaoId, - id, - }: UpdateStickerDto): Promise { + userId, + stickerId, + }: IStickersServiceUpdateSticker): Promise { try { - const sticker = await this.stickersRepository.findOne({ - where: { id, user: { kakaoId } }, + const sticker = await this.repo_stickers.findOne({ + where: { id: stickerId, userId }, }); if (!sticker) throw new NotFoundException( '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', ); if (isReusable) sticker.isReusable = isReusable; - if (image_url) { - await this.awsService.deleteImageFromS3({ url: sticker.image_url }); - sticker.image_url = image_url; + if (imageUrl) { + await this.svc_images.deleteImage({ url: sticker.imageUrl }); + sticker.imageUrl = imageUrl; } - const result = await this.stickersRepository.save(sticker); + const result = await this.repo_stickers.save(sticker); return result; } catch (e) { throw e; } } - async delete({ id, kakaoId }: IStickersServiceDelete): Promise { - const sticker = await this.stickersRepository.findOne({ - where: { id, user: { kakaoId } }, + async deleteSticker({ + stickerId, + userId, + }: IStickersServiceDeleteSticker): Promise { + const sticker = await this.repo_stickers.findOne({ + where: { id: stickerId, userId }, }); if (!sticker) throw new NotFoundException( '스티커가 존재하지 않거나 제작자 본인이 아닙니다.', ); - await this.awsService.deleteImageFromS3({ - url: sticker.image_url, + await this.svc_images.deleteImage({ + url: sticker.imageUrl, }); - await this.stickersRepository.remove(sticker); + await this.repo_stickers.remove(sticker); return; } } From aec0e4e8be19aaac117575f6707b7a78b4de2948 Mon Sep 17 00:00:00 2001 From: do-huni Date: Sat, 6 Jul 2024 23:37:55 +0900 Subject: [PATCH 220/236] fix(*): bugs & errors occured while refactoring --- .../agreements/entities/agreement.entity.ts | 2 +- .../controllers/articles-create.controller.ts | 7 ++-- .../article-create-draft-request.dto.ts | 2 +- .../request/article-create-request.dto.ts | 2 +- .../response/article-detail-response.dto.ts | 2 +- .../articles-paginate.repository.ts.ts | 22 +++++------ .../repositories/articles-read.repository.ts | 6 +-- .../services/articles-create.service.ts | 29 +++++++------- .../services/articles-delete.service.ts | 18 ++++----- .../services/articles-read.service.ts | 11 +++--- .../services/articles-update.service.ts | 3 -- src/APIs/comments/comments.service.ts | 25 +++++++----- .../feedbacks/dtos/common/feedback.dto.ts | 6 +-- src/APIs/follows/entities/follow.entity.ts | 6 +-- src/APIs/follows/follows.repository.ts | 6 +-- src/APIs/follows/follows.service.ts | 10 ++--- .../stickerBlocks/stickerBlocks.service.ts | 9 +++-- .../stickerCategories.service.interface.ts | 6 +-- .../stickerCategories.controller.ts | 12 +++--- .../stickerCategories.service.ts | 39 ++++++++++++------- .../dtos/request/sticker-patch-request.dto.ts | 9 +---- src/APIs/stickers/stickers.service.ts | 10 +++-- 22 files changed, 125 insertions(+), 117 deletions(-) diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 6a665c9..93a59de 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -20,7 +20,7 @@ export class Agreement extends CommonEntity { id: number; @JoinColumn() - @ManyToOne(() => User, (user) => user.kakaoId, { + @ManyToOne(() => User, (user) => user.id, { nullable: false, onUpdate: 'NO ACTION', onDelete: 'CASCADE', diff --git a/src/APIs/articles/controllers/articles-create.controller.ts b/src/APIs/articles/controllers/articles-create.controller.ts index 46af0e1..a350648 100644 --- a/src/APIs/articles/controllers/articles-create.controller.ts +++ b/src/APIs/articles/controllers/articles-create.controller.ts @@ -22,9 +22,9 @@ import { ArticleCreateResponseDto } from '../dtos/response/article-create-respon import { AuthGuardV2 } from 'src/common/guards/auth.guard'; import { ArticleCreateRequestDto } from '../dtos/request/article-create-request.dto'; import { ArticleCreateDraftRequestDto } from '../dtos/request/article-create-draft-request.dto'; -import { ImageUploadDto } from 'src/common/dtos/image-upload.dto'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { FileInterceptor } from '@nestjs/platform-express'; +import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; @ApiTags('게시글 API') @Controller('articles') @@ -81,7 +81,7 @@ export class ArticlesCreateController { @ApiConsumes('multipart/form-data') @ApiBody({ description: '업로드 할 파일', - type: ImageUploadDto, + type: ImageUploadRequestDto, }) @ApiCreatedResponse({ description: '이미지 서버에 파일 업로드 완료', @@ -99,4 +99,3 @@ export class ArticlesCreateController { return await this.svc_articlesCreate.imageUpload(file); } } -} diff --git a/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts b/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts index e09df10..1fc254e 100644 --- a/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts +++ b/src/APIs/articles/dtos/request/article-create-draft-request.dto.ts @@ -1,6 +1,6 @@ import { PartialType } from '@nestjs/swagger'; import { ArticleCreateRequestDto } from './article-create-request.dto'; -import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto'; +import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto'; export class ArticleCreateDraftRequestDto extends PartialType( ArticleCreateRequestDto, diff --git a/src/APIs/articles/dtos/request/article-create-request.dto.ts b/src/APIs/articles/dtos/request/article-create-request.dto.ts index d19959c..23a150c 100644 --- a/src/APIs/articles/dtos/request/article-create-request.dto.ts +++ b/src/APIs/articles/dtos/request/article-create-request.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, ValidateNested } from 'class-validator'; import { ArticleDto } from '../common/article.dto'; -import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks.create.dto'; +import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto'; export class ArticleCreateRequestDto extends OmitType(ArticleDto, [ 'id', diff --git a/src/APIs/articles/dtos/response/article-detail-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-response.dto.ts index 7450c60..ec36fd9 100644 --- a/src/APIs/articles/dtos/response/article-detail-response.dto.ts +++ b/src/APIs/articles/dtos/response/article-detail-response.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/user-response.dto'; import { Article } from '../../entities/article.entity'; +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/response/user-primary-response.dto'; export class ArticleDetailResponseDto extends OmitType(Article, [ 'comments', diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts.ts index f4b3957..b1e45f9 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts.ts @@ -49,7 +49,7 @@ export class ArticlesPaginateRepository extends Repository
{ async fetchArticlesCursor({ cursorOption, - date_filter, + dateFilter, }: IArticlesRepoFetchArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -58,9 +58,9 @@ export class ArticlesPaginateRepository extends Repository
{ scopes: [OpenScope.PUBLIC], }); - if (date_filter) { - queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, + if (dateFilter) { + queryBuilder.andWhere('p.date_created > :dateFilter', { + dateFilter, }); } @@ -72,7 +72,7 @@ export class ArticlesPaginateRepository extends Repository
{ async fetchFriendsArticlesCursor({ cursorOption, userId, - date_filter, + dateFilter, }: IArticlesRepoFetchFriendsArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -105,9 +105,9 @@ export class ArticlesPaginateRepository extends Repository
{ }), ); - if (date_filter) { - queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, + if (dateFilter) { + queryBuilder.andWhere('p.date_created > :dateFilter', { + dateFilter, }); } @@ -120,7 +120,7 @@ export class ArticlesPaginateRepository extends Repository
{ cursorOption, scope, userId, - date_filter, + dateFilter, }: IArticlesRepoFetchUserArticlesCursor) { const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); @@ -136,9 +136,9 @@ export class ArticlesPaginateRepository extends Repository
{ }) .andWhere('p.scope IN (:scope)', { scope }); - if (date_filter) { + if (dateFilter) { queryBuilder.andWhere('p.date_created > :date_filter', { - date_filter: date_filter, + date_filter: dateFilter, }); } diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index 26a3c66..eb40767 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -1,14 +1,14 @@ import { DataSource, Repository } from 'typeorm'; import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; -import { ArticleDetailResponse } from '../dtos/response/article-detail-response.dto'; +import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; @Injectable() export class ArticlesReadRepository extends Repository
{ constructor(private dataSource: DataSource) { super(Article, dataSource.createEntityManager()); } - async readDetail({ articleId, scope }): Promise { + async readDetail({ articleId, scope }): Promise { await this.update(articleId, { viewCount: () => 'view_count +1', }); @@ -46,7 +46,7 @@ export class ArticlesReadRepository extends Repository
{ .getOne(); } - async readTemp({ userId }): Promise { + async readTemp({ userId }): Promise { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') .leftJoinAndSelect('p.article_background', 'article_background') diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts index 8bf4a96..d64ca8e 100644 --- a/src/APIs/articles/services/articles-create.service.ts +++ b/src/APIs/articles/services/articles-create.service.ts @@ -5,18 +5,18 @@ import { Article } from '../entities/article.entity'; import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesReadRepository } from '../repositories/articles-read.repository'; -import { AwsService } from 'src/modules/aws/aws.service'; import { ArticleCreateResponseDto } from '../dtos/response/article-create-response.dto'; -import { ImageUploadResponseDto } from 'src/common/dtos/image-upload-response.dto'; import { getUUID } from 'src/utils/uuidUtils'; +import { ImagesService } from 'src/modules/images/images.service'; +import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; @Injectable() export class ArticlesCreateService { constructor( private readonly db_dataSource: DataSource, private readonly svc_articlesValidate: ArticlesValidateService, + private readonly svc_images: ImagesService, private readonly svc_stickerBlocks: StickerBlocksService, - private readonly svc_aws: AwsService, private readonly repo_articlesRead: ArticlesReadRepository, ) {} @@ -48,11 +48,12 @@ export class ArticlesCreateService { const articleData = await this.repo_articlesRead.findOne({ where: { id: queryResult.identifiers[0].id }, }); - const stickerBlockData = await this.svc_stickerBlocks.bulkInsert({ - articleId: articleData.id, - userId: createArticleDto.userId, - stickerBlocks: createArticleDto.stickerBlocks, - }); + const stickerBlockData = await this.svc_stickerBlocks.createStickerBlocks( + { + articleId: articleData.id, + stickerBlocks: createArticleDto.stickerBlocks, + }, + ); return { articleData, stickerBlockData }; } catch (e) { await queryRunner.rollbackTransaction(); @@ -65,15 +66,11 @@ export class ArticlesCreateService { async imageUpload( file: Express.Multer.File, ): Promise { - const imageName = getUUID(); - const ext = file.originalname.split('.').pop(); - - const imageUrl = await this.svc_aws.imageUploadToS3( - `${imageName}.${ext}`, + const { imageUrl } = await this.svc_images.imageUpload({ file, - ext, - 1280, - ); + ext: 'png', + resize: 1280, + }); return { imageUrl }; } diff --git a/src/APIs/articles/services/articles-delete.service.ts b/src/APIs/articles/services/articles-delete.service.ts index 7b8bb9c..2373fe6 100644 --- a/src/APIs/articles/services/articles-delete.service.ts +++ b/src/APIs/articles/services/articles-delete.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesDeleteRepository } from '../repositories/articles-delete.repository'; import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; -import { AwsService } from 'src/modules/aws/aws.service'; import { IArticlesServiceArticleUserIdPair } from '../interfaces/articles.service.interface'; import { ArticlesReadRepository } from '../repositories/articles-read.repository'; +import { ImagesService } from 'src/modules/images/images.service'; @Injectable() export class ArticlesDeleteService { @@ -12,7 +12,7 @@ export class ArticlesDeleteService { // private readonly dataSource: DataSource, private readonly svc_articlesValidate: ArticlesValidateService, private readonly svc_stickerBlocks: StickerBlocksService, - private readonly svc_aws: AwsService, + private readonly svc_images: ImagesService, private readonly repo_articlesRead: ArticlesReadRepository, private readonly repo_articlesDelete: ArticlesDeleteRepository, ) {} @@ -22,9 +22,9 @@ export class ArticlesDeleteService { where: { user: { id: userId }, id: articleId }, }); if (data) { - await this.svc_aws.deleteImageFromS3({ url: data.imageUrl }); - await this.svc_aws.deleteImageFromS3({ url: data.mainImageUrl }); - await this.svc_stickerBlocks.deleteBlocks({ userId, articleId }); + await this.svc_images.deleteImage({ url: data.imageUrl }); + await this.svc_images.deleteImage({ url: data.mainImageUrl }); + await this.svc_stickerBlocks.deleteStickerBlocks({ userId, articleId }); } return await this.repo_articlesDelete.softDelete({ user: { id: userId }, @@ -34,12 +34,12 @@ export class ArticlesDeleteService { async hardDelete({ userId, articleId }: IArticlesServiceArticleUserIdPair) { const data = await this.repo_articlesRead.findOne({ - where: { user: { userId }, articleId }, + where: { userId, id: articleId }, }); if (data) { - await this.svc_aws.deleteImageFromS3({ url: data.imageUrl }); - await this.svc_aws.deleteImageFromS3({ url: data.mainImageUrl }); - await this.svc_stickerBlocks.deleteBlocks({ userId, articleId }); + await this.svc_images.deleteImage({ url: data.imageUrl }); + await this.svc_images.deleteImage({ url: data.mainImageUrl }); + await this.svc_stickerBlocks.deleteStickerBlocks({ userId, articleId }); } return await this.repo_articlesDelete.delete({ user: { id: userId }, diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts index 1f0e7f0..f30adaf 100644 --- a/src/APIs/articles/services/articles-read.service.ts +++ b/src/APIs/articles/services/articles-read.service.ts @@ -10,6 +10,7 @@ import { } from '../interfaces/articles.service.interface'; import { ArticleDetailForUpdateResponseDto } from '../dtos/response/article-detail-for-update-response.dto'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; +import { ArticleDto } from '../dtos/common/article.dto'; @Injectable() export class ArticlesReadService { @@ -36,16 +37,16 @@ export class ArticlesReadService { }); if (data.userId !== userId) throw new UnauthorizedException('본인이 아닙니다.'); - const article = await this.repo_articlesRead.readUpdateDetail(id); - const stickerBlocks = await this.svc_stickerBlocks.fetchBlocks({ + const article = await this.repo_articlesRead.readUpdateDetail({ + articleId, + }); + const stickerBlocks = await this.svc_stickerBlocks.findStickerBlocks({ articleId, }); return { article, stickerBlocks }; } - async readTempArticles({ - userId, - }): Promise { + async readTempArticles({ userId }): Promise { return await this.repo_articlesRead.readTemp(userId); } diff --git a/src/APIs/articles/services/articles-update.service.ts b/src/APIs/articles/services/articles-update.service.ts index a0354cb..254ad9a 100644 --- a/src/APIs/articles/services/articles-update.service.ts +++ b/src/APIs/articles/services/articles-update.service.ts @@ -1,8 +1,5 @@ import { ForbiddenException } from '@nestjs/common'; import { ArticlesValidateService } from './articles-validate.service'; -import { ArticlesReadRepository } from '../repositories/articles-read.repository'; -import { DataSource } from 'typeorm'; -import { ArticlesUpdateRepository } from '../repositories/articles-update.repository'; import { ArticlesCreateRepository } from '../repositories/articles-create.repository'; import { IArticlesServicePatchArticle } from '../interfaces/articles.service.interface'; import { ArticleDto } from '../dtos/common/article.dto'; diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index b58470a..09022d0 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -13,7 +13,6 @@ import { ICommentsServiceFindComments, ICommentsServiceId, ICommentsServicePatchComment, -, } from './interfaces/comments.service.interface'; import { Comment } from './entities/comment.entity'; import { NotificationsService } from '../notifications/notifications.service'; @@ -45,7 +44,9 @@ export class CommentsService { } async existCheck({ commentId }: ICommentsServiceId): Promise { - const comment = await this.repo_comments.findOne({ where: { id: commentId} }); + const comment = await this.repo_comments.findOne({ + where: { id: commentId }, + }); if (!comment) { throw new NotFoundException( '댓글의 아이디를 찾을 수 없습니다. 존재하지 않거나 이미 삭제되었습니다.', @@ -54,7 +55,9 @@ export class CommentsService { return comment; } - async createComment(createCommentDto: ICommentsServiceCreateComment): Promise { + async createComment( + createCommentDto: ICommentsServiceCreateComment, + ): Promise { const post = await this.db_dataSource.manager.findOne(Article, { where: { id: createCommentDto.articleId }, }); @@ -65,16 +68,20 @@ export class CommentsService { parentId: createCommentDto.parentId, articleId: createCommentDto.articleId, }); - await this.db_dataSource.manager.update(Article, createCommentDto.articleId, { - commentCount: () => 'comment_count +1', - }); + await this.db_dataSource.manager.update( + Article, + createCommentDto.articleId, + { + commentCount: () => 'comment_count +1', + }, + ); const commentData = await this.repo_comments.insertComment({ createCommentDto, }); - const { id } = commentData.identifiers[0]; + const { commentId } = commentData.identifiers[0]; const { article, parent, ...result } = - await this.repo_comments.fetchCommentWithNotiInfo({ id }); + await this.repo_comments.fetchCommentWithNotiInfo({ commentId }); if (result.parentId && parent.userId != result.userId) { await this.svc_notifications.emitAlarm({ @@ -103,7 +110,7 @@ export class CommentsService { commentId, content, }: ICommentsServicePatchComment): Promise { - const commentData = await this.existCheck({ id }); + const commentData = await this.existCheck({ commentId }); if (!commentData) throw new NotFoundException('댓글을 찾을 수 없습니다.'); if (commentData.articleId != articleId) throw new NotFoundException('루트 게시글의 아이디가 일치하지 않습니다.'); diff --git a/src/APIs/feedbacks/dtos/common/feedback.dto.ts b/src/APIs/feedbacks/dtos/common/feedback.dto.ts index 0f60e25..e278113 100644 --- a/src/APIs/feedbacks/dtos/common/feedback.dto.ts +++ b/src/APIs/feedbacks/dtos/common/feedback.dto.ts @@ -1,4 +1,4 @@ -import { OmitType } from "@nestjs/swagger"; -import { Feedback } from "../../entities/feedback.entity"; +import { OmitType } from '@nestjs/swagger'; +import { Feedback } from '../../entities/feedback.entity'; -export class FeedbackDto extends OmitType(Feedback, ['user']) \ No newline at end of file +export class FeedbackDto extends OmitType(Feedback, ['user']) {} diff --git a/src/APIs/follows/entities/follow.entity.ts b/src/APIs/follows/entities/follow.entity.ts index 9e6d5e5..e7ead2d 100644 --- a/src/APIs/follows/entities/follow.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNumber } from 'class-validator'; -import { UserResponseDto } from 'src/APIs/users/dtos/user-response.dto'; +import { UserDto } from 'src/APIs/users/dtos/common/user.dto'; import { User } from 'src/APIs/users/entities/user.entity'; import { CommonEntity } from 'src/common/entities/common.entity'; import { @@ -19,7 +19,7 @@ export class Follow extends CommonEntity { @IsNumber() id: number; - @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 받은 유저' }) + @ApiProperty({ type: UserDto, description: '이웃 추가를 받은 유저' }) @JoinColumn() @ManyToOne(() => User, (users) => users.id, { nullable: false, @@ -28,7 +28,7 @@ export class Follow extends CommonEntity { }) toUser: User; - @ApiProperty({ type: UserResponseDto, description: '이웃 추가를 한 유저' }) + @ApiProperty({ type: UserDto, description: '이웃 추가를 한 유저' }) @JoinColumn() @ManyToOne(() => User, (users) => users.id, { nullable: false, diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 9458be9..50a8a6e 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -1,8 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { IFollowsRepositoryFindList } from './interfaces/follows.repository.interface'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; @Injectable() export class FollowsRepository extends Repository { @@ -13,7 +13,7 @@ export class FollowsRepository extends Repository { async getFollowers({ userId, loggedUser, - }: IFollowsRepositoryFindList): Promise { + }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.from_user', 'user') .where('user.date_deleted IS NULL') @@ -65,7 +65,7 @@ export class FollowsRepository extends Repository { async getFollowings({ userId, loggedUser, - }: IFollowsRepositoryFindList): Promise { + }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.to_user', 'user') .where('user.date_deleted IS NULL') diff --git a/src/APIs/follows/follows.service.ts b/src/APIs/follows/follows.service.ts index f814113..ffe85d5 100644 --- a/src/APIs/follows/follows.service.ts +++ b/src/APIs/follows/follows.service.ts @@ -1,9 +1,7 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { FollowUserDto } from './dtos/follow-user.dto'; import { OpenScope } from 'src/common/enums/open-scope.enum'; import { FollowsRepository } from './follows.repository'; -import { UserResponseDtoWithFollowing } from '../users/dtos/user-response.dto'; import { User } from '../users/entities/user.entity'; import { IFollowsServiceFindList, @@ -11,6 +9,8 @@ import { } from './interfaces/follows.service.interface'; import { NotificationsService } from '../notifications/notifications.service'; import { NotType } from 'src/common/enums/not-type.enum'; +import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; +import { FollowDto } from './dtos/common/follow.dto'; @Injectable() export class FollowsService { @@ -76,7 +76,7 @@ export class FollowsService { async followUser({ fromUser, toUser, - }: IFollowsServiceUsers): Promise { + }: IFollowsServiceUsers): Promise { const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -171,7 +171,7 @@ export class FollowsService { async findFollowings({ userId, loggedUser, - }: IFollowsServiceFindList): Promise { + }: IFollowsServiceFindList): Promise { const follows = await this.repo_follows.getFollowings({ userId, loggedUser, @@ -182,7 +182,7 @@ export class FollowsService { async findFollowers({ userId, loggedUser, - }: IFollowsServiceFindList): Promise { + }: IFollowsServiceFindList): Promise { const follows = await this.repo_follows.getFollowers({ userId, loggedUser, diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 48c2dc0..634451b 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -26,7 +26,7 @@ export class StickerBlocksService { }: IStickerBlocksServiceCreateStickerBlock): Promise { try { await this.svc_stickers.existCheck({ - id: stickerId, + stickerId: stickerId, }); const data = await this.repo_stickerBlocks.save({ @@ -50,7 +50,7 @@ export class StickerBlocksService { })); stickerBlocksToInsert.forEach(async (stickerBlock) => { await this.svc_stickers.existCheck({ - id: stickerBlock.stickerId, + stickerId: stickerBlock.stickerId, }); }); return await this.repo_stickerBlocks.save(stickerBlocksToInsert); @@ -74,7 +74,10 @@ export class StickerBlocksService { }); for (const block of blocksToDelete) { if (block.sticker.isReusable === false) - await this.svc_stickers.delete({ userId, id: block.id }); + await this.svc_stickers.deleteSticker({ + userId, + stickerId: block.stickerId, + }); await this.repo_stickerBlocks.remove(block); } return; diff --git a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts index efaaf2c..3693e57 100644 --- a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts +++ b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts @@ -1,12 +1,12 @@ import { MapCategoryDto } from '../dtos/map-category.dto'; export interface IStickerCategoriesServiceMapCategory { - kakaoId: number; + userId: number; maps: MapCategoryDto[]; } export interface IStickerCategoriesServiceId { - id: number; + stickerCategoryId: number; } export interface IStickerCategoriesServiceName { @@ -14,6 +14,6 @@ export interface IStickerCategoriesServiceName { } export interface IStickerCategoriesServiceCreateCategory { - kakaoId: number; + userId: number; name: string; } diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index e3f329e..88c0521 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -45,10 +45,10 @@ export class StickerCategoriesController { @ApiOkResponse({ type: [StickerCategoryMapper] }) @Get('stickers/categories/:id') async fetchStickersByCategoryName( - @Param('id') id: number, + @Param('stickerCategoryId') stickerCategoryId: number, ): Promise { return await this.stickerCategoriesService.fetchStickersByCategoryId({ - id, + stickerCategoryId, }); } @@ -65,9 +65,9 @@ export class StickerCategoriesController { @Req() req: Request, @Body() body: CreateStickerCategoryInput, ): Promise { - const kakaoId = req.user.userId; + const userId = req.user.userId; return await this.stickerCategoriesService.createCategory({ - kakaoId, + userId, ...body, }); } @@ -85,9 +85,9 @@ export class StickerCategoriesController { @Req() req: Request, @Body() mapCategoryDto: BulkMapCategoryDto, ): Promise { - const kakaoId = req.user.userId; + const userId = req.user.userId; return await this.stickerCategoriesService.mapCategory({ - kakaoId, + userId, ...mapCategoryDto, }); } diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index f911afc..7f2dba6 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -3,7 +3,6 @@ import { Repository } from 'typeorm'; import { StickerCategory } from './entities/stickerCategory.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; -import { UsersService } from '../users/users.service'; import { StickersService } from '../stickers/stickers.service'; import { IStickerCategoriesServiceCreateCategory, @@ -11,6 +10,7 @@ import { IStickerCategoriesServiceMapCategory, IStickerCategoriesServiceName, } from './interfaces/stickerCategories.service.interface'; +import { UsersValidateService } from '../users/services/users-validate-service'; @Injectable() export class StickerCategoriesService { @@ -19,7 +19,7 @@ export class StickerCategoriesService { private readonly stickerCategoriesRepository: Repository, @InjectRepository(StickerCategoryMapper) private readonly stickerCategoryMappersRepository: Repository, - private readonly usersService: UsersService, + private readonly svc_usersValidate: UsersValidateService, private readonly stickersService: StickersService, ) {} @@ -30,9 +30,11 @@ export class StickerCategoriesService { } async findCategoryById({ - id, + stickerCategoryId, }: IStickerCategoriesServiceId): Promise { - return await this.stickerCategoriesRepository.findOne({ where: { id } }); + return await this.stickerCategoriesRepository.findOne({ + where: { id: stickerCategoryId }, + }); } async existCheckByName({ name, @@ -40,8 +42,10 @@ export class StickerCategoriesService { const data = await this.findCategoryByName({ name }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } - async existCheckById({ id }: IStickerCategoriesServiceId): Promise { - const data = await this.findCategoryById({ id }); + async existCheckById({ + stickerCategoryId, + }: IStickerCategoriesServiceId): Promise { + const data = await this.findCategoryById({ stickerCategoryId }); if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } @@ -50,32 +54,37 @@ export class StickerCategoriesService { } async createCategory({ - kakaoId, + userId, name, }: IStickerCategoriesServiceCreateCategory): Promise { - await this.usersService.adminCheck({ kakaoId }); + await this.svc_usersValidate.adminCheck({ userId }); return await this.stickerCategoriesRepository.save({ name }); } async mapCategory({ - kakaoId, + userId, maps, }: IStickerCategoriesServiceMapCategory): Promise { - await this.usersService.adminCheck({ kakaoId }); + await this.svc_usersValidate.adminCheck({ userId }); maps.forEach(async (map) => { - await this.existCheckById({ id: map.stickerCategoryId }); - await this.stickersService.existCheck({ id: map.stickerId }); + await this.existCheckById({ stickerCategoryId: map.stickerCategoryId }); + await this.stickersService.existCheck({ + stickerId: map.stickerId, + }); }); return await this.stickerCategoryMappersRepository.save(maps); } async fetchStickersByCategoryId({ - id, + stickerCategoryId, }: IStickerCategoriesServiceId): Promise { - await this.existCheckById({ id }); + await this.existCheckById({ stickerCategoryId }); return await this.stickerCategoryMappersRepository.find({ relations: { sticker: true, stickerCategory: true }, - where: { stickerCategory: { id }, sticker: { isDefault: true } }, + where: { + stickerCategory: { id: stickerCategoryId }, + sticker: { isDefault: true }, + }, }); } } diff --git a/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts b/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts index a472b12..76c3c59 100644 --- a/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts +++ b/src/APIs/stickers/dtos/request/sticker-patch-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsNumber, IsOptional, IsUrl } from 'class-validator'; +import { IsBoolean, IsOptional, IsUrl } from 'class-validator'; export class StickerPatchRequestDto { @ApiProperty({ description: '변경할 url', type: String, required: false }) @@ -16,10 +16,3 @@ export class StickerPatchRequestDto { @IsOptional() isReusable?: boolean; } - -export class UpdateStickerDto extends UpdateStickerInput { - @IsNumber() - kakaoId: number; - - id: number; -} diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 7791da5..040195e 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -22,12 +22,14 @@ export class StickersService { private readonly svc_images: ImagesService, ) {} - async findStickerById({ id }: IStickersServiceId): Promise { - return await this.repo_stickers.findOne({ where: { id } }); + async findStickerById({ + stickerId, + }: IStickersServiceId): Promise { + return await this.repo_stickers.findOne({ where: { id: stickerId } }); } - async existCheck({ id }: IStickersServiceId): Promise { - const data = await this.findStickerById({ id }); + async existCheck({ stickerId }: IStickersServiceId): Promise { + const data = await this.findStickerById({ stickerId }); if (!data) throw new NotFoundException('스티커를 찾을 수 없습니다.'); } From 7ac75d3c6ba41335088ca846f3910ac35f005f23 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 8 Jul 2024 16:36:56 +0900 Subject: [PATCH 221/236] fix(entities): swagger circular dependencies --- .../agreements/entities/agreement.entity.ts | 1 + .../articleCategories.controller.ts | 2 +- .../entities/articleCategory.entity.ts | 2 +- src/APIs/articles/articles.module.ts | 17 ++-- .../controllers/articles-create.controller.ts | 4 +- src/APIs/articles/entities/article.entity.ts | 10 +-- .../interfaces/articles.service.interface.ts | 6 ++ ....ts.ts => articles-paginate.repository.ts} | 8 +- .../services/articles-create.service.ts | 49 +++++++++++- .../services/articles-paginate.service.ts | 2 +- src/APIs/comments/entities/comment.entity.ts | 8 +- .../feedbacks/entities/feedback.entity.ts | 2 +- src/APIs/follows/entities/follow.entity.ts | 5 +- src/APIs/follows/follows.repository.ts | 77 ++++++------------- src/APIs/likes/entities/like.entity.ts | 4 +- src/APIs/likes/likes.repository.ts | 40 ++++------ .../entities/notification.entity.ts | 6 +- src/APIs/reports/entities/report.entity.ts | 16 +++- .../entities/stickerblock.entity.ts | 4 +- .../dtos/common/stickerCategory.dto.ts | 6 ++ .../dtos/common/stickerCategoryMapper.dto.ts | 7 ++ .../dtos/create-sticker-category.dto.ts | 6 -- .../dtos/map-category.dto.ts | 17 ---- .../stickerCategories-map-request.dto.ts | 10 +++ .../stickerCategory-create-request.dto.ts | 7 ++ .../stickerCategories.service.interface.ts | 4 +- .../stickerCategories.controller.ts | 8 +- src/APIs/stickers/entities/sticker.entity.ts | 2 +- src/APIs/stickers/stickers.module.ts | 7 +- src/APIs/users/dtos/common/user.dto.ts | 2 +- src/APIs/users/users.module.ts | 3 + src/APIs/users/users.repository.ts | 4 +- 32 files changed, 186 insertions(+), 160 deletions(-) rename src/APIs/articles/repositories/{articles-paginate.repository.ts.ts => articles-paginate.repository.ts} (95%) create mode 100644 src/APIs/stickerCategories/dtos/common/stickerCategory.dto.ts create mode 100644 src/APIs/stickerCategories/dtos/common/stickerCategoryMapper.dto.ts delete mode 100644 src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts delete mode 100644 src/APIs/stickerCategories/dtos/map-category.dto.ts create mode 100644 src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts create mode 100644 src/APIs/stickerCategories/dtos/request/stickerCategory-create-request.dto.ts diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index 93a59de..ab490e0 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -19,6 +19,7 @@ export class Agreement extends CommonEntity { @IsNumber() id: number; + @ApiProperty({ type: () => User, description: '약관 동의를 한 유저' }) @JoinColumn() @ManyToOne(() => User, (user) => user.id, { nullable: false, diff --git a/src/APIs/articleCategories/articleCategories.controller.ts b/src/APIs/articleCategories/articleCategories.controller.ts index 514132b..5b5dc2d 100644 --- a/src/APIs/articleCategories/articleCategories.controller.ts +++ b/src/APIs/articleCategories/articleCategories.controller.ts @@ -96,7 +96,7 @@ export class ArticleCategoriesController { @ApiCookieAuth() @ApiOkResponse({ type: ArticleCategoryDto }) @UseGuards(AuthGuardV2) - @Patch('me/categories/:categoryId') + @Patch('me/categories/:articleCategoryId') async patchArticleCategory( @Req() req: Request, @Param('articleCategoryId') articleCategoryId: string, diff --git a/src/APIs/articleCategories/entities/articleCategory.entity.ts b/src/APIs/articleCategories/entities/articleCategory.entity.ts index 6a2183c..105395f 100644 --- a/src/APIs/articleCategories/entities/articleCategory.entity.ts +++ b/src/APIs/articleCategories/entities/articleCategory.entity.ts @@ -25,7 +25,7 @@ export class ArticleCategory extends CommonEntity { @IsString() name: string; - @ApiProperty({ type: User, description: '연결된 유저' }) + @ApiProperty({ type: () => User, description: '연결된 유저' }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; diff --git a/src/APIs/articles/articles.module.ts b/src/APIs/articles/articles.module.ts index 7d23754..9693103 100644 --- a/src/APIs/articles/articles.module.ts +++ b/src/APIs/articles/articles.module.ts @@ -1,12 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; -import { UtilsModule } from 'src/modules/utils/utils.module'; import { ArticleBackground } from '../articleBackgrounds/entities/articleBackground.entity'; import { ArticleCategory } from '../articleCategories/entities/articleCategory.entity'; import { StickerBlocksModule } from '../stickerBlocks/stickerBlocks.module'; import { FollowsModule } from '../follows/follows.module'; -import { AwsModule } from 'src/modules/aws/aws.module'; import { Article } from './entities/article.entity'; import { ArticlesReadService } from './services/articles-read.service'; import { ArticlesCreateService } from './services/articles-create.service'; @@ -17,22 +15,18 @@ import { ArticlesCreateRepository } from './repositories/articles-create.reposit import { ArticlesReadRepository } from './repositories/articles-read.repository'; import { ArticlesUpdateRepository } from './repositories/articles-update.repository'; import { ArticlesDeleteRepository } from './repositories/articles-delete.repository'; -import { ArticlesPaginateRepository } from './repositories/articles-paginate.repository.ts'; +import { ArticlesPaginateRepository } from './repositories/articles-paginate.repository'; import { ArticlesCreateController } from './controllers/articles-create.controller'; import { ArticlesReadController } from './controllers/articles-read.controller'; import { ArticlesUpdateController } from './controllers/articles-update.controller'; import { ArticlesDeleteController } from './controllers/articles-delete.controller'; +import { ArticlesValidateService } from './services/articles-validate.service'; +import { ImagesModule } from 'src/modules/images/images.module'; @Module({ imports: [ - TypeOrmModule.forFeature([ - Article, - User, - ArticleBackground, - ArticleCategory, - ]), - UtilsModule, - AwsModule, + TypeOrmModule.forFeature([Article, ArticleBackground, ArticleCategory]), + ImagesModule, FollowsModule, StickerBlocksModule, ], @@ -41,6 +35,7 @@ import { ArticlesDeleteController } from './controllers/articles-delete.controll ArticlesReadService, ArticlesUpdateService, ArticlesDeleteService, + ArticlesValidateService, ArticlesPaginateService, ArticlesCreateRepository, ArticlesReadRepository, diff --git a/src/APIs/articles/controllers/articles-create.controller.ts b/src/APIs/articles/controllers/articles-create.controller.ts index a350648..f5aebf3 100644 --- a/src/APIs/articles/controllers/articles-create.controller.ts +++ b/src/APIs/articles/controllers/articles-create.controller.ts @@ -64,13 +64,13 @@ export class ArticlesCreateController { }) @UseGuards(AuthGuardV2) @HttpCode(201) - async updateArticle( + async createDraft( @Req() req: Request, @Body() body: ArticleCreateDraftRequestDto, ) { const userId = req.user.userId; const dto = { ...body, userId, isPublished: false }; - return await this.svc_articlesCreate.save(dto); + return await this.svc_articlesCreate.createDraft(dto); } @ApiOperation({ diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 597998d..935a63d 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -113,8 +113,8 @@ export class Article extends IndexedCommonEntity { @IsEnum(OpenScope) scope: OpenScope; - @ApiProperty({ description: '게시글 내용', type: String, default: '' }) - @Column('longtext', { default: '' }) + @ApiProperty({ description: '게시글 내용', type: String, nullable: true }) + @Column('longtext', { nullable: true }) @IsString() content: string; @@ -137,7 +137,7 @@ export class Article extends IndexedCommonEntity { @IsUrl() mainImageUrl: string; - @ApiProperty({ description: '연결된 카테고리', type: ArticleCategory }) + @ApiProperty({ description: '연결된 카테고리', type: () => ArticleCategory }) @ManyToOne(() => ArticleCategory, { nullable: true, onUpdate: 'CASCADE', @@ -146,7 +146,7 @@ export class Article extends IndexedCommonEntity { @JoinColumn() articleCategory: ArticleCategory; - @ApiProperty({ description: '연결된 내지', type: ArticleBackground }) + @ApiProperty({ description: '연결된 내지', type: () => ArticleBackground }) @JoinColumn() @ManyToOne(() => ArticleBackground, { nullable: true, @@ -155,7 +155,7 @@ export class Article extends IndexedCommonEntity { }) articleBackground: ArticleBackground; - @ApiProperty({ description: '작성자', type: User }) + @ApiProperty({ description: '작성자', type: () => User }) @JoinColumn() @ManyToOne(() => User, { nullable: false, diff --git a/src/APIs/articles/interfaces/articles.service.interface.ts b/src/APIs/articles/interfaces/articles.service.interface.ts index 8e4676d..fed5287 100644 --- a/src/APIs/articles/interfaces/articles.service.interface.ts +++ b/src/APIs/articles/interfaces/articles.service.interface.ts @@ -1,3 +1,4 @@ +import { ArticleCreateDraftRequestDto } from '../dtos/request/article-create-draft-request.dto'; import { ArticleCreateRequestDto } from '../dtos/request/article-create-request.dto'; import { ArticlePatchRequestDto } from '../dtos/request/article-patch-request.dto'; import { ArticlesGetRequestDto } from '../dtos/request/articles-get-request.dto'; @@ -17,6 +18,11 @@ export interface IArticlesServiceCreate extends ArticleCreateRequestDto { userId: number; isPublished: boolean; } +export interface IArticlesServiceCreateDraft + extends ArticleCreateDraftRequestDto { + userId: number; + isPublished: boolean; +} export interface IArticlesServiceCreateCursorResponse { cursorOption: ArticlesGetRequestDto; diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts similarity index 95% rename from src/APIs/articles/repositories/articles-paginate.repository.ts.ts rename to src/APIs/articles/repositories/articles-paginate.repository.ts index b1e45f9..4b19c08 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts @@ -10,10 +10,12 @@ import { IArticlesRepoGetCursorQuery, } from '../interfaces/articles.repository.interface'; import { Follow } from 'src/APIs/follows/entities/follow.entity'; +import { Injectable } from '@nestjs/common'; +@Injectable() export class ArticlesPaginateRepository extends Repository
{ - constructor(private dataSource: DataSource) { - super(Article, dataSource.createEntityManager()); + constructor(private db_dataSource: DataSource) { + super(Article, db_dataSource.createEntityManager()); } getCursorQuery({ order, sort, take, cursor }: IArticlesRepoGetCursorQuery) { const _order = ArticleOrderOption[order]; @@ -77,7 +79,7 @@ export class ArticlesPaginateRepository extends Repository
{ const { order, cursor, take, sort } = cursorOption; const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); - const mutualFollows = await this.dataSource + const mutualFollows = await this.db_dataSource .createQueryBuilder(Follow, 'f1') .select('f1.from_user_id', 'user1') .addSelect('f1.to_user_id', 'user2') diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts index d64ca8e..448c5f0 100644 --- a/src/APIs/articles/services/articles-create.service.ts +++ b/src/APIs/articles/services/articles-create.service.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { IArticlesServiceCreate } from '../interfaces/articles.service.interface'; +import { + IArticlesServiceCreate, + IArticlesServiceCreateDraft, +} from '../interfaces/articles.service.interface'; import { DataSource } from 'typeorm'; import { Article } from '../entities/article.entity'; import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.service'; @@ -63,6 +66,50 @@ export class ArticlesCreateService { } } + async createDraft( + dto_createDraft: IArticlesServiceCreateDraft, + ): Promise { + const queryRunner = this.db_dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const article = {}; + try { + Object.keys(dto_createDraft).map((el) => { + const value = dto_createDraft[el]; + if (dto_createDraft[el] != null) { + article[el] = value; + } + }); + await this.svc_articlesValidate.fkValidCheck({ + articles: article, + passNonEssentail: !dto_createDraft.isPublished, + }); + const queryResult = await queryRunner.manager + .createQueryBuilder() + .insert() + .into(Article, Object.keys(article)) + .values(article) + .execute(); + await queryRunner.commitTransaction(); + const articleData = await this.repo_articlesRead.findOne({ + where: { id: queryResult.identifiers[0].id }, + }); + const stickerBlockData = await this.svc_stickerBlocks.createStickerBlocks( + { + articleId: articleData.id, + stickerBlocks: dto_createDraft.stickerBlocks, + }, + ); + return { articleData, stickerBlockData }; + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + } + async imageUpload( file: Express.Multer.File, ): Promise { diff --git a/src/APIs/articles/services/articles-paginate.service.ts b/src/APIs/articles/services/articles-paginate.service.ts index 1f14dd7..1a85b97 100644 --- a/src/APIs/articles/services/articles-paginate.service.ts +++ b/src/APIs/articles/services/articles-paginate.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { ArticlesValidateService } from './articles-validate.service'; -import { ArticlesPaginateRepository } from '../repositories/articles-paginate.repository.ts'; +import { ArticlesPaginateRepository } from '../repositories/articles-paginate.repository'; import { ArticleOrderOption } from 'src/common/enums/article-order-option'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 2b09e62..9decee1 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -27,7 +27,7 @@ export class Comment extends IndexedCommonEntity { @IsNumber() userId: number; - @ApiProperty({ type: User, description: '사용자 정보' }) + @ApiProperty({ type: () => User, description: '사용자 정보' }) @ManyToOne(() => User, (users) => users.id, { nullable: false }) @JoinColumn() user: User; @@ -38,7 +38,7 @@ export class Comment extends IndexedCommonEntity { @IsNumber() articleId: number; - @ApiProperty({ type: Article, description: '게시글 정보' }) + @ApiProperty({ type: () => Article, description: '게시글 정보' }) @ManyToOne(() => Article, (article) => article.id, { nullable: false, onUpdate: 'NO ACTION', @@ -57,7 +57,7 @@ export class Comment extends IndexedCommonEntity { @IsNumber() reportCount: number; - @ApiProperty({ type: Comment, description: '루트 댓글 정보' }) + @ApiProperty({ type: () => Comment, description: '루트 댓글 정보' }) @ManyToOne(() => Comment, (comment) => comment.children, { nullable: true, onUpdate: 'NO ACTION', @@ -71,7 +71,7 @@ export class Comment extends IndexedCommonEntity { @RelationId((comment: Comment) => comment.parent) parentId: number; - @ApiProperty({ type: [Comment], description: '자식 댓글 정보' }) + @ApiProperty({ type: () => [Comment], description: '자식 댓글 정보' }) @OneToMany(() => Comment, (comment) => comment.parent) children: Comment[]; diff --git a/src/APIs/feedbacks/entities/feedback.entity.ts b/src/APIs/feedbacks/entities/feedback.entity.ts index 704006f..f609c35 100644 --- a/src/APIs/feedbacks/entities/feedback.entity.ts +++ b/src/APIs/feedbacks/entities/feedback.entity.ts @@ -33,7 +33,7 @@ export class Feedback extends CommonEntity { @IsNumber() userId: number; - @ApiProperty({ type: User, description: '사용자 정보' }) + @ApiProperty({ type: () => User, description: '사용자 정보' }) @ManyToOne(() => User, (users) => users.id, { nullable: true, onUpdate: 'NO ACTION', diff --git a/src/APIs/follows/entities/follow.entity.ts b/src/APIs/follows/entities/follow.entity.ts index e7ead2d..4675101 100644 --- a/src/APIs/follows/entities/follow.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -1,6 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNumber } from 'class-validator'; -import { UserDto } from 'src/APIs/users/dtos/common/user.dto'; import { User } from 'src/APIs/users/entities/user.entity'; import { CommonEntity } from 'src/common/entities/common.entity'; import { @@ -19,7 +18,7 @@ export class Follow extends CommonEntity { @IsNumber() id: number; - @ApiProperty({ type: UserDto, description: '이웃 추가를 받은 유저' }) + @ApiProperty({ type: () => User, description: '이웃 추가를 받은 유저' }) @JoinColumn() @ManyToOne(() => User, (users) => users.id, { nullable: false, @@ -28,7 +27,7 @@ export class Follow extends CommonEntity { }) toUser: User; - @ApiProperty({ type: UserDto, description: '이웃 추가를 한 유저' }) + @ApiProperty({ type: () => User, description: '이웃 추가를 한 유저' }) @JoinColumn() @ManyToOne(() => User, (users) => users.id, { nullable: false, diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 50a8a6e..9ba0074 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -3,6 +3,9 @@ import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; import { IFollowsRepositoryFindList } from './interfaces/follows.repository.interface'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; +import { UserDto } from '../users/dtos/common/user.dto'; +import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; +import { plainToClass } from 'class-transformer'; @Injectable() export class FollowsRepository extends Repository { @@ -29,37 +32,21 @@ export class FollowsRepository extends Repository { 'follow2.toUserKakaoId = user.kakaoId', ) .select([ - 'user.username AS username', - 'user.kakaoId AS kakaoId', - 'user.handle AS handle', - 'user.follower_count AS follower_count', - 'user.following_count AS following_count', - 'user.isAdmin AS isAdmin', - 'user.username AS username', - 'user.description AS description', - 'user.profile_image AS profile_image', - 'user.background_image AS background_image', - 'user.date_created AS date_created', - 'user.date_deleted AS date_deleted', - 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ...getClassFields(UserDto).map( + (column) => `user.${column} AS ${column}`, + ), + 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId, loggedUser }) .getRawMany(); - return followings.map((follower) => ({ - username: follower.username, - kakaoId: follower.kakaoId, - handle: follower.handle, - follower_count: follower.follower_count, - following_count: follower.following_count, - isAdmin: follower.isAdmin === 1, - description: follower.description, - profile_image: follower.profile_image, - background_image: follower.background_image, - date_created: follower.date_created, - date_deleted: follower.date_deleted, - isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 - })); + return followings.map((follower) => + plainToClass(UserFollowingResponseDto, { + ...convertToCamelCase(follower), + isAdmin: follower.is_admin === 1, + isFollowing: follower.is_following === 1, + }), + ); } async getFollowings({ @@ -81,36 +68,20 @@ export class FollowsRepository extends Repository { 'follow2.toUserKakaoId = user.kakaoId', ) .select([ - 'user.username AS username', - 'user.kakaoId AS kakaoId', - 'user.handle AS handle', - 'user.follower_count AS follower_count', - 'user.following_count AS following_count', - 'user.isAdmin AS isAdmin', - 'user.username AS username', - 'user.description AS description', - 'user.profile_image AS profile_image', - 'user.background_image AS background_image', - 'user.date_created AS date_created', - 'user.date_deleted AS date_deleted', + ...getClassFields(UserDto).map( + (column) => `user.${column} AS ${column}`, + ), 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', ]) .setParameters({ userId, loggedUser }) .getRawMany(); - return followings.map((follower) => ({ - username: follower.username, - kakaoId: follower.kakaoId, - handle: follower.handle, - follower_count: follower.follower_count, - following_count: follower.following_count, - isAdmin: follower.isAdmin === 1, - description: follower.description, - profile_image: follower.profile_image, - background_image: follower.background_image, - date_created: follower.date_created, - date_deleted: follower.date_deleted, - isFollowing: follower.isFollowing === 1, // MySQL에서는 boolean 값이 1 또는 0으로 반환될 수 있음 - })); + return followings.map((follower) => + plainToClass(UserFollowingResponseDto, { + ...convertToCamelCase(follower), + isAdmin: follower.is_admin === 1, + isFollowing: follower.is_following === 1, + }), + ); } } diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index f8f988f..b3fc149 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -19,7 +19,7 @@ export class Like extends CommonEntity { @IsNumber() id: number; - @ApiProperty({ description: '좋아요를 누른 유저', type: User }) + @ApiProperty({ description: '좋아요를 누른 유저', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; @@ -32,7 +32,7 @@ export class Like extends CommonEntity { @ApiProperty({ description: '좋아요를 누른 게시글', - type: Article, + type: () => Article, }) @JoinColumn() @ManyToOne(() => Article, { diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index 7b5ff74..1b8f951 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -4,6 +4,9 @@ import { Injectable } from '@nestjs/common'; import { ILikesRepositoryIds } from './interfaces/likes.repository.interface'; import { Like } from './entities/like.entity'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; +import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; +import { UserDto } from '../users/dtos/common/user.dto'; +import { plainToClass } from 'class-transformer'; @Injectable() export class LikesRepository extends Repository { @@ -31,35 +34,20 @@ export class LikesRepository extends Repository { .andWhere('posts.date_deleted IS NULL') .andWhere('likes.postsId = :id') .select([ - 'user.username AS username', - 'user.kakaoId AS kakaoId', - 'user.handle AS handle', - 'user.follower_count AS follower_count', - 'user.following_count AS following_count', - 'user.isAdmin AS isAdmin', - 'user.description AS description', - 'user.profile_image AS profile_image', - 'user.background_image AS background_image', - 'user.date_created AS date_created', - 'user.date_deleted AS date_deleted', - 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ...getClassFields(UserDto).map( + (column) => `user.${column} AS ${column}`, + ), + 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ articleId, userId }) .getRawMany(); - return users.map((user) => ({ - username: user.username, - kakaoId: user.kakaoId, - handle: user.handle, - follower_count: user.follower_count, - following_count: user.following_count, - isAdmin: user.isAdmin === 1, - description: user.description, - profile_image: user.profile_image, - background_image: user.background_image, - date_created: user.date_created, - date_deleted: user.date_deleted, - isFollowing: user.isFollowing === 1, - })); + return users.map((user) => + plainToClass(UserFollowingResponseDto, { + ...convertToCamelCase(user), + isAdmin: user.is_admin === 1, + isFollowing: user.is_following === 1, + }), + ); } } diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 9130da7..3645870 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -20,7 +20,7 @@ export class Notification extends CommonEntity { @IsNumber() id: number; - @ApiProperty({ description: '알림을 생성한 유저 정보', type: User }) + @ApiProperty({ description: '알림을 생성한 유저 정보', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @@ -31,7 +31,7 @@ export class Notification extends CommonEntity { @IsNumber() userId: number; - @ApiProperty({ description: '알림을 받는 유저 정보', type: User }) + @ApiProperty({ description: '알림을 받는 유저 정보', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @@ -62,7 +62,7 @@ export class Notification extends CommonEntity { articleId: number; @ApiProperty({ - type: Article, + type: () => Article, description: '알림이 발생한 게시물', nullable: true, }) diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 4053caa..0acb607 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -33,7 +33,7 @@ export class Report extends CommonEntity { @IsNumber() userId: number; - @ApiProperty({ description: '신고를 한 유저', type: User }) + @ApiProperty({ description: '신고를 한 유저', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @@ -44,7 +44,7 @@ export class Report extends CommonEntity { @IsNumber() targetUserId: number; - @ApiProperty({ description: '신고를 당한 유저', type: User }) + @ApiProperty({ description: '신고를 당한 유저', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @@ -66,7 +66,11 @@ export class Report extends CommonEntity { @IsNumber() articleId: number; - @ApiProperty({ type: Article, description: '신고된 게시물', nullable: true }) + @ApiProperty({ + type: () => Article, + description: '신고된 게시물', + nullable: true, + }) @JoinColumn() @ManyToOne(() => Article, { nullable: true, @@ -81,7 +85,11 @@ export class Report extends CommonEntity { @IsNumber() commentId: number; - @ApiProperty({ type: Comment, description: '신고된 댓글', nullable: true }) + @ApiProperty({ + type: () => Comment, + description: '신고된 댓글', + nullable: true, + }) @JoinColumn() @ManyToOne(() => Comment, { nullable: true, diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 02221de..01eee06 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -25,7 +25,7 @@ export class StickerBlock extends CommonEntity { @IsNumber() stickerId: number; - @ApiProperty({ description: '참조하는 스티커', type: Sticker }) + @ApiProperty({ description: '참조하는 스티커', type: () => Sticker }) @JoinColumn() @ManyToOne(() => Sticker, (stickers) => stickers.id, { onDelete: 'CASCADE', @@ -39,7 +39,7 @@ export class StickerBlock extends CommonEntity { @IsNumber() articleId: number; - @ApiProperty({ description: '참조하는 포스트', type: Article }) + @ApiProperty({ description: '참조하는 포스트', type: () => Article }) @JoinColumn() @ManyToOne(() => Article, (article) => article.id, { onDelete: 'CASCADE', diff --git a/src/APIs/stickerCategories/dtos/common/stickerCategory.dto.ts b/src/APIs/stickerCategories/dtos/common/stickerCategory.dto.ts new file mode 100644 index 0000000..316ba4a --- /dev/null +++ b/src/APIs/stickerCategories/dtos/common/stickerCategory.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerCategory } from '../../entities/stickerCategory.entity'; + +export class StickerCategoryDto extends OmitType(StickerCategory, [ + 'stickerCategoryMappers', +]) {} diff --git a/src/APIs/stickerCategories/dtos/common/stickerCategoryMapper.dto.ts b/src/APIs/stickerCategories/dtos/common/stickerCategoryMapper.dto.ts new file mode 100644 index 0000000..38a205b --- /dev/null +++ b/src/APIs/stickerCategories/dtos/common/stickerCategoryMapper.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerCategoryMapper } from '../../entities/stickerCategoryMapper.entity'; + +export class StickerCategoryMapperDto extends OmitType(StickerCategoryMapper, [ + 'sticker', + 'stickerCategory', +]) {} diff --git a/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts b/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts deleted file mode 100644 index 5c16f74..0000000 --- a/src/APIs/stickerCategories/dtos/create-sticker-category.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateStickerCategoryInput { - @ApiProperty({ type: String, description: '스티커 이름' }) - name: string; -} diff --git a/src/APIs/stickerCategories/dtos/map-category.dto.ts b/src/APIs/stickerCategories/dtos/map-category.dto.ts deleted file mode 100644 index 764d237..0000000 --- a/src/APIs/stickerCategories/dtos/map-category.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class MapCategoryDto { - @ApiProperty({ description: '매핑 하고자 하는 스티커의 id', type: Number }) - stickerId: number; - - @ApiProperty({ description: '매핑 하고자 하는 카테고리의 id', type: Number }) - stickerCategoryId: number; -} - -export class BulkMapCategoryDto { - @ApiProperty({ - description: '매핑할 카테고리 및 스티커 배열', - type: [MapCategoryDto], - }) - maps: MapCategoryDto[]; -} diff --git a/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts b/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts new file mode 100644 index 0000000..1b53cfc --- /dev/null +++ b/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StickerCategoryMapperDto } from '../common/stickerCategoryMapper.dto'; + +export class StickerCategoriesMapDto { + @ApiProperty({ + description: '매핑할 카테고리 및 스티커 배열', + type: [StickerCategoryMapperDto], + }) + maps: StickerCategoryMapperDto[]; +} diff --git a/src/APIs/stickerCategories/dtos/request/stickerCategory-create-request.dto.ts b/src/APIs/stickerCategories/dtos/request/stickerCategory-create-request.dto.ts new file mode 100644 index 0000000..25f7b0b --- /dev/null +++ b/src/APIs/stickerCategories/dtos/request/stickerCategory-create-request.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { StickerCategoryDto } from '../common/stickerCategory.dto'; + +export class StickerCategoryCreateRequestDto extends PickType( + StickerCategoryDto, + ['name'], +) {} diff --git a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts index 3693e57..12d5409 100644 --- a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts +++ b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts @@ -1,8 +1,8 @@ -import { MapCategoryDto } from '../dtos/map-category.dto'; +import { StickerCategoryMapperDto } from '../dtos/common/stickerCategoryMapper.dto'; export interface IStickerCategoriesServiceMapCategory { userId: number; - maps: MapCategoryDto[]; + maps: StickerCategoryMapperDto[]; } export interface IStickerCategoriesServiceId { diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 88c0521..6512825 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -15,11 +15,11 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { BulkMapCategoryDto } from './dtos/map-category.dto'; import { StickerCategory } from './entities/stickerCategory.entity'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { CreateStickerCategoryInput } from './dtos/create-sticker-category.dto'; import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; +import { StickerCategoryCreateRequestDto } from './dtos/request/stickerCategory-create-request.dto'; +import { StickerCategoriesMapDto } from './dtos/request/stickerCategories-map-request.dto'; @ApiTags('스티커 API') @Controller() @@ -63,7 +63,7 @@ export class StickerCategoriesController { @Post('users/admin/stickers/categories') async createCategory( @Req() req: Request, - @Body() body: CreateStickerCategoryInput, + @Body() body: StickerCategoryCreateRequestDto, ): Promise { const userId = req.user.userId; return await this.stickerCategoriesService.createCategory({ @@ -83,7 +83,7 @@ export class StickerCategoriesController { @Post('users/admin/stickers/map') async mapCategory( @Req() req: Request, - @Body() mapCategoryDto: BulkMapCategoryDto, + @Body() mapCategoryDto: StickerCategoriesMapDto, ): Promise { const userId = req.user.userId; return await this.stickerCategoriesService.mapCategory({ diff --git a/src/APIs/stickers/entities/sticker.entity.ts b/src/APIs/stickers/entities/sticker.entity.ts index b502324..bd3a1a8 100644 --- a/src/APIs/stickers/entities/sticker.entity.ts +++ b/src/APIs/stickers/entities/sticker.entity.ts @@ -26,7 +26,7 @@ export class Sticker extends CommonEntity { @IsNumber() userId: number; - @ApiProperty({ description: '제작한 유저', type: User }) + @ApiProperty({ description: '제작한 유저', type: () => User }) @JoinColumn() @ManyToOne(() => User, { onUpdate: 'CASCADE', diff --git a/src/APIs/stickers/stickers.module.ts b/src/APIs/stickers/stickers.module.ts index fca1f6a..5ab9e6e 100644 --- a/src/APIs/stickers/stickers.module.ts +++ b/src/APIs/stickers/stickers.module.ts @@ -3,13 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Sticker } from './entities/sticker.entity'; import { StickersController } from './stickers.controller'; import { StickersService } from './stickers.service'; -import { UtilsService } from 'src/modules/utils/utils.service'; import { UsersModule } from '../users/users.module'; -import { AwsService } from 'src/modules/aws/aws.service'; +import { ImagesModule } from 'src/modules/images/images.module'; @Module({ - imports: [TypeOrmModule.forFeature([Sticker]), UsersModule], - providers: [StickersService, AwsService, UtilsService], + imports: [TypeOrmModule.forFeature([Sticker]), UsersModule, ImagesModule], + providers: [StickersService], controllers: [StickersController], exports: [StickersService], }) diff --git a/src/APIs/users/dtos/common/user.dto.ts b/src/APIs/users/dtos/common/user.dto.ts index 1c3ed62..fe1a2e0 100644 --- a/src/APIs/users/dtos/common/user.dto.ts +++ b/src/APIs/users/dtos/common/user.dto.ts @@ -1,6 +1,6 @@ import { OmitType } from '@nestjs/swagger'; -import { User } from '../../entities/user.entity'; import { getClassFields } from 'src/utils/classUtils'; +import { User } from '../../entities/user.entity'; // exclude refreshtoken!! export class UserDto extends OmitType(User, [ diff --git a/src/APIs/users/users.module.ts b/src/APIs/users/users.module.ts index 1a2ca04..aae16f2 100644 --- a/src/APIs/users/users.module.ts +++ b/src/APIs/users/users.module.ts @@ -11,6 +11,7 @@ import { UsersCreateController } from './controllers/users-create.controller'; import { UsersReadController } from './controllers/users-read.controller'; import { UsersUpdateController } from './controllers/users-update.controller'; import { UsersDeleteController } from './controllers/users-delete.controller'; +import { UsersValidateService } from './services/users-validate-service'; @Module({ imports: [TypeOrmModule.forFeature([User]), ImagesModule], @@ -19,6 +20,7 @@ import { UsersDeleteController } from './controllers/users-delete.controller'; UsersReadService, UsersUpdateService, UsersDeleteService, + UsersValidateService, UsersRepository, ], controllers: [ @@ -32,6 +34,7 @@ import { UsersDeleteController } from './controllers/users-delete.controller'; UsersReadService, UsersUpdateService, UsersDeleteService, + UsersValidateService, ], }) export class UsersModule {} diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index 3f3fddd..4b5ef18 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -30,7 +30,7 @@ export class UsersRepository extends Repository { ...getClassFields(UserDto).map( (column) => `user.${column} AS ${column}`, ), - 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS isFollowing', + 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId }); @@ -55,7 +55,7 @@ export class UsersRepository extends Repository { plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(user), isAdmin: user.is_admin === 1, - isFollowing: user.isFollowing === 1, + isFollowing: user.is_following === 1, }), ); } From 788b59b6c86518dfae90e3b03bbb19a7d326a9c4 Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 8 Jul 2024 16:51:42 +0900 Subject: [PATCH 222/236] fix(entities): some non-snake cases columns to snake --- src/APIs/agreements/entities/agreement.entity.ts | 2 +- .../entities/articleBackground.entity.ts | 2 +- .../entities/articleCategory.entity.ts | 2 +- src/APIs/articles/entities/article.entity.ts | 6 +++--- src/APIs/comments/entities/comment.entity.ts | 6 +++--- src/APIs/feedbacks/entities/feedback.entity.ts | 2 +- src/APIs/follows/entities/follow.entity.ts | 4 ++-- src/APIs/likes/entities/like.entity.ts | 4 ++-- .../notifications/entities/notification.entity.ts | 6 +++--- src/APIs/reports/entities/report.entity.ts | 8 ++++---- .../stickerBlocks/entities/stickerblock.entity.ts | 12 ++++++------ .../entities/stickerCategoryMapper.entity.ts | 4 ++-- src/APIs/stickers/entities/sticker.entity.ts | 2 +- src/common/entities/indexed-common.entity.ts | 5 ++++- 14 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/APIs/agreements/entities/agreement.entity.ts b/src/APIs/agreements/entities/agreement.entity.ts index ab490e0..bc6e354 100644 --- a/src/APIs/agreements/entities/agreement.entity.ts +++ b/src/APIs/agreements/entities/agreement.entity.ts @@ -20,7 +20,7 @@ export class Agreement extends CommonEntity { id: number; @ApiProperty({ type: () => User, description: '약관 동의를 한 유저' }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, (user) => user.id, { nullable: false, onUpdate: 'NO ACTION', diff --git a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts index 248e50e..2b1dac5 100644 --- a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts +++ b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts @@ -10,7 +10,7 @@ export class ArticleBackground extends CommonEntity { id: number; @ApiProperty({ type: String, description: '이미지가 저장된 url' }) - @Column({ nullable: false }) + @Column({ nullable: false, name: 'image_url' }) imageUrl: string; @ApiProperty({ diff --git a/src/APIs/articleCategories/entities/articleCategory.entity.ts b/src/APIs/articleCategories/entities/articleCategory.entity.ts index 105395f..0d0d876 100644 --- a/src/APIs/articleCategories/entities/articleCategory.entity.ts +++ b/src/APIs/articleCategories/entities/articleCategory.entity.ts @@ -26,7 +26,7 @@ export class ArticleCategory extends CommonEntity { name: string; @ApiProperty({ type: () => User, description: '연결된 유저' }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 935a63d..087ffcb 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -143,11 +143,11 @@ export class Article extends IndexedCommonEntity { onUpdate: 'CASCADE', onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ name: 'article_category_id' }) articleCategory: ArticleCategory; @ApiProperty({ description: '연결된 내지', type: () => ArticleBackground }) - @JoinColumn() + @JoinColumn({ name: 'article_background_id' }) @ManyToOne(() => ArticleBackground, { nullable: true, onUpdate: 'SET NULL', @@ -156,7 +156,7 @@ export class Article extends IndexedCommonEntity { articleBackground: ArticleBackground; @ApiProperty({ description: '작성자', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { nullable: false, onUpdate: 'CASCADE', diff --git a/src/APIs/comments/entities/comment.entity.ts b/src/APIs/comments/entities/comment.entity.ts index 9decee1..16f6917 100644 --- a/src/APIs/comments/entities/comment.entity.ts +++ b/src/APIs/comments/entities/comment.entity.ts @@ -29,7 +29,7 @@ export class Comment extends IndexedCommonEntity { @ApiProperty({ type: () => User, description: '사용자 정보' }) @ManyToOne(() => User, (users) => users.id, { nullable: false }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) user: User; @ApiProperty({ type: Number, description: '게시글 id' }) @@ -44,7 +44,7 @@ export class Comment extends IndexedCommonEntity { onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ name: 'article_id' }) article: Article; @ApiProperty({ type: String, description: '내용 정보', maxLength: 1500 }) @@ -63,7 +63,7 @@ export class Comment extends IndexedCommonEntity { onUpdate: 'NO ACTION', onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ name: 'parent_id' }) parent: Comment; @ApiProperty({ type: Number, description: '루트 댓글 아이디' }) diff --git a/src/APIs/feedbacks/entities/feedback.entity.ts b/src/APIs/feedbacks/entities/feedback.entity.ts index f609c35..e6373e9 100644 --- a/src/APIs/feedbacks/entities/feedback.entity.ts +++ b/src/APIs/feedbacks/entities/feedback.entity.ts @@ -39,7 +39,7 @@ export class Feedback extends CommonEntity { onUpdate: 'NO ACTION', onDelete: 'SET NULL', }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) user: User; @ApiProperty({ description: '피드백 종류', type: 'enum', enum: FeedbackType }) diff --git a/src/APIs/follows/entities/follow.entity.ts b/src/APIs/follows/entities/follow.entity.ts index 4675101..b9957b8 100644 --- a/src/APIs/follows/entities/follow.entity.ts +++ b/src/APIs/follows/entities/follow.entity.ts @@ -19,7 +19,7 @@ export class Follow extends CommonEntity { id: number; @ApiProperty({ type: () => User, description: '이웃 추가를 받은 유저' }) - @JoinColumn() + @JoinColumn({ name: 'to_user_id' }) @ManyToOne(() => User, (users) => users.id, { nullable: false, onUpdate: 'NO ACTION', @@ -28,7 +28,7 @@ export class Follow extends CommonEntity { toUser: User; @ApiProperty({ type: () => User, description: '이웃 추가를 한 유저' }) - @JoinColumn() + @JoinColumn({ name: 'from_user_id' }) @ManyToOne(() => User, (users) => users.id, { nullable: false, onUpdate: 'NO ACTION', diff --git a/src/APIs/likes/entities/like.entity.ts b/src/APIs/likes/entities/like.entity.ts index b3fc149..5914aff 100644 --- a/src/APIs/likes/entities/like.entity.ts +++ b/src/APIs/likes/entities/like.entity.ts @@ -20,7 +20,7 @@ export class Like extends CommonEntity { id: number; @ApiProperty({ description: '좋아요를 누른 유저', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { onUpdate: 'NO ACTION', onDelete: 'CASCADE' }) user: User; @@ -34,7 +34,7 @@ export class Like extends CommonEntity { description: '좋아요를 누른 게시글', type: () => Article, }) - @JoinColumn() + @JoinColumn({ name: 'article_id' }) @ManyToOne(() => Article, { nullable: false, onUpdate: 'NO ACTION', diff --git a/src/APIs/notifications/entities/notification.entity.ts b/src/APIs/notifications/entities/notification.entity.ts index 3645870..6458b0f 100644 --- a/src/APIs/notifications/entities/notification.entity.ts +++ b/src/APIs/notifications/entities/notification.entity.ts @@ -21,7 +21,7 @@ export class Notification extends CommonEntity { id: number; @ApiProperty({ description: '알림을 생성한 유저 정보', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @@ -32,7 +32,7 @@ export class Notification extends CommonEntity { userId: number; @ApiProperty({ description: '알림을 받는 유저 정보', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'target_user_id' }) @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @@ -66,7 +66,7 @@ export class Notification extends CommonEntity { description: '알림이 발생한 게시물', nullable: true, }) - @JoinColumn() + @JoinColumn({ name: 'aritcle_id' }) @ManyToOne(() => Article, { nullable: true, onUpdate: 'CASCADE', diff --git a/src/APIs/reports/entities/report.entity.ts b/src/APIs/reports/entities/report.entity.ts index 0acb607..b665808 100644 --- a/src/APIs/reports/entities/report.entity.ts +++ b/src/APIs/reports/entities/report.entity.ts @@ -34,7 +34,7 @@ export class Report extends CommonEntity { userId: number; @ApiProperty({ description: '신고를 한 유저', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user: User; @@ -45,7 +45,7 @@ export class Report extends CommonEntity { targetUserId: number; @ApiProperty({ description: '신고를 당한 유저', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'target_user_id' }) @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) targetUser: User; @@ -71,7 +71,7 @@ export class Report extends CommonEntity { description: '신고된 게시물', nullable: true, }) - @JoinColumn() + @JoinColumn({ name: 'article_id' }) @ManyToOne(() => Article, { nullable: true, onUpdate: 'CASCADE', @@ -90,7 +90,7 @@ export class Report extends CommonEntity { description: '신고된 댓글', nullable: true, }) - @JoinColumn() + @JoinColumn({ name: 'comment_id' }) @ManyToOne(() => Comment, { nullable: true, onUpdate: 'CASCADE', diff --git a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts index 01eee06..25a74ca 100644 --- a/src/APIs/stickerBlocks/entities/stickerblock.entity.ts +++ b/src/APIs/stickerBlocks/entities/stickerblock.entity.ts @@ -26,7 +26,7 @@ export class StickerBlock extends CommonEntity { stickerId: number; @ApiProperty({ description: '참조하는 스티커', type: () => Sticker }) - @JoinColumn() + @JoinColumn({ name: 'sticker_id' }) @ManyToOne(() => Sticker, (stickers) => stickers.id, { onDelete: 'CASCADE', onUpdate: 'CASCADE', @@ -40,7 +40,7 @@ export class StickerBlock extends CommonEntity { articleId: number; @ApiProperty({ description: '참조하는 포스트', type: () => Article }) - @JoinColumn() + @JoinColumn({ name: 'article_id' }) @ManyToOne(() => Article, (article) => article.id, { onDelete: 'CASCADE', onUpdate: 'CASCADE', @@ -48,11 +48,11 @@ export class StickerBlock extends CommonEntity { article: Article; @ApiProperty({ description: '스티커의 posX', type: Number }) - @Column({ type: 'float' }) + @Column({ type: 'float', name: 'pox_x' }) posX: number; @ApiProperty({ description: '스티커의 posY', type: Number }) - @Column({ type: 'float' }) + @Column({ type: 'float', name: 'pos_y' }) posY: number; @ApiProperty({ description: '스티커의 scale', type: Number }) @@ -64,10 +64,10 @@ export class StickerBlock extends CommonEntity { angle: number; @ApiProperty({ description: '스티커의 zindex', type: Number }) - @Column({ type: 'float' }) + @Column({ type: 'float', name: 'z_index' }) zindex: number; @ApiProperty({ description: '스티커의 clientId', type: String }) - @Column() + @Column({ name: 'client_id' }) clientId: string; } diff --git a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts index f97c6c2..3846081 100644 --- a/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts +++ b/src/APIs/stickerCategories/entities/stickerCategoryMapper.entity.ts @@ -22,7 +22,7 @@ export class StickerCategoryMapper extends CommonEntity { @IsNumber() stickerId: number; - @JoinColumn() + @JoinColumn({ name: 'sticker_id' }) @ManyToOne(() => Sticker, (stickers) => stickers.id, { onUpdate: 'CASCADE', onDelete: 'CASCADE', @@ -38,7 +38,7 @@ export class StickerCategoryMapper extends CommonEntity { @IsNumber() stickerCategoryId: number; - @JoinColumn() + @JoinColumn({ name: 'sticker_category_id' }) @ManyToOne( () => StickerCategory, (stickerCategories) => stickerCategories.id, diff --git a/src/APIs/stickers/entities/sticker.entity.ts b/src/APIs/stickers/entities/sticker.entity.ts index bd3a1a8..144f9b8 100644 --- a/src/APIs/stickers/entities/sticker.entity.ts +++ b/src/APIs/stickers/entities/sticker.entity.ts @@ -27,7 +27,7 @@ export class Sticker extends CommonEntity { userId: number; @ApiProperty({ description: '제작한 유저', type: () => User }) - @JoinColumn() + @JoinColumn({ name: 'user_id' }) @ManyToOne(() => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE', diff --git a/src/common/entities/indexed-common.entity.ts b/src/common/entities/indexed-common.entity.ts index f0794bc..b70d98b 100644 --- a/src/common/entities/indexed-common.entity.ts +++ b/src/common/entities/indexed-common.entity.ts @@ -9,7 +9,10 @@ import { export abstract class IndexedCommonEntity { @Index() @ApiProperty({ type: Date, description: '생성된 날짜' }) - @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP(6)' }) + @CreateDateColumn({ + default: () => 'CURRENT_TIMESTAMP(6)', + name: 'date_created', + }) dateCreated: Date; @ApiProperty({ type: Date, description: '수정된 날짜' }) From c666f225498be89deb4b1ce1c6f1e57644f4386e Mon Sep 17 00:00:00 2001 From: do-huni Date: Mon, 8 Jul 2024 18:48:51 +0900 Subject: [PATCH 223/236] fix(user): user search logic with following data --- package-lock.json | 8 ++--- package.json | 2 +- src/APIs/follows/follows.repository.ts | 35 ++++++++----------- src/APIs/likes/likes.repository.ts | 7 ++-- src/APIs/users/dtos/common/user.dto.ts | 29 +++++++++------ .../response/user-primary-response.dto.ts | 22 +++++++----- src/APIs/users/users.repository.ts | 14 ++++---- src/utils/classUtils.ts | 20 ++++++++--- 8 files changed, 76 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc42b1a..f86125e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "passport-kakao": "^1.0.1", "prom-client": "^15.1.2", "redis": "^4.6.14", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sharp": "^0.32.6", "typeorm": "^0.3.20", @@ -10383,9 +10383,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/repeat-string": { "version": "1.6.1", diff --git a/package.json b/package.json index 37c6bb9..50e16ab 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "passport-kakao": "^1.0.1", "prom-client": "^15.1.2", "redis": "^4.6.14", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sharp": "^0.32.6", "typeorm": "^0.3.20", diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 9ba0074..8802fa8 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -3,8 +3,7 @@ import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; import { IFollowsRepositoryFindList } from './interfaces/follows.repository.interface'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; -import { UserDto } from '../users/dtos/common/user.dto'; -import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; +import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; import { plainToClass } from 'class-transformer'; @Injectable() @@ -18,24 +17,22 @@ export class FollowsRepository extends Repository { loggedUser, }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') - .innerJoin('follow.from_user', 'user') + .innerJoin('follow.from_user_id', 'user') .where('user.date_deleted IS NULL') - .andWhere('follow.toUserKakaoId = :kakaoId') + .andWhere('follow.to_user_id = :userId') .leftJoinAndSelect( (subQuery) => { return subQuery - .select('follow2.toUserKakaoId', 'toUserKakaoId') + .select('follow2.to_user_id', 'to_user_id') .from(Follow, 'follow2') - .where('follow2.fromUserKakaoId = :loggedUser'); + .where('follow2.from_user_id = :loggedUser'); }, 'follow2', - 'follow2.toUserKakaoId = user.kakaoId', + 'follow2.to_user_id = user.id', ) .select([ - ...getClassFields(UserDto).map( - (column) => `user.${column} AS ${column}`, - ), - 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS is_following', + ...getUserFields().map((column) => `user.${column} AS ${column}`), + 'CASE WHEN follow2.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId, loggedUser }) .getRawMany(); @@ -54,24 +51,22 @@ export class FollowsRepository extends Repository { loggedUser, }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') - .innerJoin('follow.to_user', 'user') + .innerJoin('follow.to_user_id', 'user') .where('user.date_deleted IS NULL') - .andWhere('follow.fromUserKakaoId = :kakaoId') + .andWhere('follow.from_user_id = :userId') .leftJoinAndSelect( (subQuery) => { return subQuery - .select('follow2.toUserKakaoId', 'toUserKakaoId') + .select('follow2.to_user_id', 'to_user_id') .from(Follow, 'follow2') - .where('follow2.fromUserKakaoId = :loggedUser'); + .where('follow2.from_user_id = :loggedUser'); }, 'follow2', - 'follow2.toUserKakaoId = user.kakaoId', + 'follow2.to_user_id = user.id', ) .select([ - ...getClassFields(UserDto).map( - (column) => `user.${column} AS ${column}`, - ), - 'CASE WHEN follow2.toUserKakaoId IS NOT NULL THEN true ELSE false END AS isFollowing', + ...getUserFields().map((column) => `user.${column} AS ${column}`), + 'CASE WHEN follow2.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId, loggedUser }) .getRawMany(); diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index 1b8f951..ace8d11 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -4,8 +4,7 @@ import { Injectable } from '@nestjs/common'; import { ILikesRepositoryIds } from './interfaces/likes.repository.interface'; import { Like } from './entities/like.entity'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; -import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; -import { UserDto } from '../users/dtos/common/user.dto'; +import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; import { plainToClass } from 'class-transformer'; @Injectable() @@ -34,9 +33,7 @@ export class LikesRepository extends Repository { .andWhere('posts.date_deleted IS NULL') .andWhere('likes.postsId = :id') .select([ - ...getClassFields(UserDto).map( - (column) => `user.${column} AS ${column}`, - ), + ...getUserFields().map((column) => `user.${column} AS ${column}`), 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ articleId, userId }) diff --git a/src/APIs/users/dtos/common/user.dto.ts b/src/APIs/users/dtos/common/user.dto.ts index fe1a2e0..841395a 100644 --- a/src/APIs/users/dtos/common/user.dto.ts +++ b/src/APIs/users/dtos/common/user.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { getClassFields } from 'src/utils/classUtils'; +import { getUserFields } from 'src/utils/classUtils'; import { User } from '../../entities/user.entity'; // exclude refreshtoken!! @@ -17,14 +17,23 @@ export class UserDto extends OmitType(User, [ 'sentNotifications', 'sentReports', 'stickers', + 'likes', ] as const) {} -export const USER_SELECT_OPTION: { [key: string]: boolean } = getClassFields( - UserDto, -).reduce( - (options, field) => { - options[field] = true; - return options; - }, - {} as { [key: string]: boolean }, -); +export const USER_SELECT_OPTION: { [key: string]: boolean } = + getUserFields().reduce( + (options, field) => { + options[field] = true; + return options; + }, + {} as { [key: string]: boolean }, + ); +// getClassFields( +// UserDto, +// ).reduce( +// (options, field) => { +// options[field] = true; +// return options; +// }, +// {} as { [key: string]: boolean }, +// ); diff --git a/src/APIs/users/dtos/response/user-primary-response.dto.ts b/src/APIs/users/dtos/response/user-primary-response.dto.ts index 246c6f2..4db3071 100644 --- a/src/APIs/users/dtos/response/user-primary-response.dto.ts +++ b/src/APIs/users/dtos/response/user-primary-response.dto.ts @@ -1,6 +1,5 @@ import { PickType } from '@nestjs/swagger'; import { User } from '../../entities/user.entity'; -import { getClassFields } from 'src/utils/classUtils'; export class UserPrimaryResponseDto extends PickType(User, [ 'id', @@ -9,11 +8,16 @@ export class UserPrimaryResponseDto extends PickType(User, [ 'handle', ]) {} -export const USER_PRIMARY_SELECT_OPTION: { [key: string]: boolean } = - getClassFields(UserPrimaryResponseDto).reduce( - (options, field) => { - options[field] = true; - return options; - }, - {} as { [key: string]: boolean }, - ); +export const USER_PRIMARY_SELECT_OPTION: { [key: string]: boolean } = { + id: true, + username: true, + profileImage: true, + handle: true, +}; +// getEntityFields(User).reduce( +// (options, field) => { +// options[field] = true; +// return options; +// }, +// {} as { [key: string]: boolean }, +// ); diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index 4b5ef18..c4f77f0 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -3,8 +3,7 @@ import { User } from './entities/user.entity'; import { DataSource, Repository } from 'typeorm'; import { Follow } from '../follows/entities/follow.entity'; import { plainToClass } from 'class-transformer'; -import { convertToCamelCase, getClassFields } from 'src/utils/classUtils'; -import { UserDto } from './dtos/common/user.dto'; +import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; import { UserFollowingResponseDto } from './dtos/response/user-following-response.dto'; @Injectable() @@ -18,7 +17,7 @@ export class UsersRepository extends Repository { .leftJoinAndSelect( (subQuery) => { return subQuery - .select('follow.to_user', 'to_user_id') + .select('follow.to_user_id', 'to_user_id') .from(Follow, 'follow') .where('follow.from_user_id = :userId', { userId }); }, @@ -27,13 +26,13 @@ export class UsersRepository extends Repository { ) .where('user.date_deleted IS NULL') .select([ - ...getClassFields(UserDto).map( - (column) => `user.${column} AS ${column}`, - ), + ...getUserFields().map((column) => { + console.log(column); + return `user.${column} AS ${column}`; + }), 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId }); - return queryBuilder; } // 팔로잉 유무 포함 조회 @@ -51,6 +50,7 @@ export class UsersRepository extends Repository { }) .getRawMany(); + console.log(users); return users.map((user) => plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(user), diff --git a/src/utils/classUtils.ts b/src/utils/classUtils.ts index ae63c19..1cb552a 100644 --- a/src/utils/classUtils.ts +++ b/src/utils/classUtils.ts @@ -1,11 +1,21 @@ +import 'reflect-metadata'; +import { User } from 'src/APIs/users/entities/user.entity'; import { getMetadataArgsStorage } from 'typeorm'; -export function getClassFields(dto: any): string[] { - const fields = getMetadataArgsStorage() - .filterColumns(dto) - .map((column) => column.propertyName); - return fields; +export function getUserFields(): string[] { + const metadata = getMetadataArgsStorage(); + + // 클래스의 모든 멤버 변수를 담을 배열 + const members: string[] = []; + + const entityMetadata = metadata.filterColumns(User); + entityMetadata.forEach((meta) => { + members.push(meta.propertyName); + }); + + return members; } + export function toCamelCase(snakeCase: string): string { return snakeCase.replace(/_([a-z])/g, (group) => group[1].toUpperCase()); } From e92d101218cb721de999e7174e1efb57ae00c740 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 9 Jul 2024 14:11:16 +0900 Subject: [PATCH 224/236] docs(readme): add mermaid erd --- README.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++ deploy/deploy.sh | 2 +- 2 files changed, 197 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1841be2..e23ea53 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,199 @@ - Nest.js - mysql/typeORM + +## ERD + +```mermaid +erDiagram + notification { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string type + bool is_checked + int article_id FK + int user_id FK + int target_user_id FK + } + follow { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + int to_user_id FK + int from_user_id FK + } + agreement { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string agreement_type + bool is_agreed + int user_id FK + } + feedback { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string content + string type + int user_id FK + } + article { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + int article_category_id FK + int article_background_id FK + string html_title + bool is_published + int like_count + int view_count + int comment_count + int report_count + bool allow_comment + string scope + string content + string main_description + string image_url + string main_image_url + int user_id FK + } + article_category { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string name + int user_id FK + } + article_background { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string image_url + } + user { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string handle + string current_refresh_token + bool is_admin + int following_count + int follower_count + string username + string description + string profile_image + string background_image + } + comment { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + int article_id FK + int content + int report_count + int parent_id + int user_id FK + } + report { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string content + string type + string target + int article_id FK + int comment_id FK + int user_id FK + int target_user_id FK + } + like { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + int article_id FK + int user_id FK + } + sticker_block { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + int sticker_id FK + int article_id FK + float scale + float angle + float pos_x + float pos_y + float z_index + int client_id + } + sticker { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string image_url + bool is_default + bool is_reusable + int user_id FK + } + sticker_category { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string name + } + sticker_category_mapper { + int sticker_id FK + int sticker_category_id FK + datetime date_created + datetime date_updated + datetime date_deleted + } + announcement { + int id PK + datetime date_created + datetime date_updated + datetime date_deleted + string title + string content + } + + notification ||--o{ article : relates + notification ||--o{ user : notifies + notification ||--o{ user : targets + follow ||--o{ user : follows + follow ||--o{ user : followed_by + agreement ||--o{ user : belongs_to + feedback ||--o{ user : given_by + article ||--o{ article_category : belongs_to + article ||--o{ article_background : has_background + article ||--o{ user : written_by + comment ||--o{ article : related_to + comment ||--o{ user : authored_by + report ||--o{ article : reports + report ||--o{ comment : reports + report ||--o{ user : filed_by + report ||--o{ user : targets + like ||--o{ article : liked + like ||--o{ user : liked_by + sticker_block ||--o{ sticker : contains + sticker_block ||--o{ article : placed_on + sticker ||--o{ user : owned_by + sticker_category_mapper ||--o{ sticker : maps_to + sticker_category_mapper ||--o{ sticker_category : categorized_as +``` diff --git a/deploy/deploy.sh b/deploy/deploy.sh index f7e2e66..4d3ab63 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -88,6 +88,6 @@ ssh -i $PEM_PATH $SERVER "sudo docker stop $OLD_SERVICE_NAME" ssh -i $PEM_PATH $SERVER "sudo docker rm $OLD_SERVICE_NAME" echo -e "$ECR_URL/$SERVICE_NAME:$DOCKER_TAG" ssh -i $PEM_PATH $SERVER "docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi" -ssh -i $PEM_PATH $SERVER "yes | sudo docker system prune -a" +ssh -i $PEM_PATH $SERVER "y | sudo docker system prune -a" echo -e "\n## 배포 완료. $NEW_SERVICE_NAME ##\n" \ No newline at end of file From e8056a29a4ee0c545fd857f5e01d5bf12e420d16 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 9 Jul 2024 14:15:21 +0900 Subject: [PATCH 225/236] fix(follow): follower list finding query - fixed repository's joining logic --- src/APIs/follows/follows.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 8802fa8..658c5d0 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -17,7 +17,7 @@ export class FollowsRepository extends Repository { loggedUser, }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') - .innerJoin('follow.from_user_id', 'user') + .innerJoin('follow.fromUser', 'user') .where('user.date_deleted IS NULL') .andWhere('follow.to_user_id = :userId') .leftJoinAndSelect( @@ -51,7 +51,7 @@ export class FollowsRepository extends Repository { loggedUser, }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') - .innerJoin('follow.to_user_id', 'user') + .innerJoin('follow.toUser', 'user') .where('user.date_deleted IS NULL') .andWhere('follow.from_user_id = :userId') .leftJoinAndSelect( From 50219db5a7be8a31097bf9727c449dcff76d7c1f Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 9 Jul 2024 17:58:47 +0900 Subject: [PATCH 226/236] fix(*): repositorys' db rows naming --- .../announcements/announcements.controller.ts | 4 +- .../announcements/announcements.service.ts | 4 +- .../articleBackgrounds.controller.ts | 9 ++- .../articleBackgrounds.service.ts | 4 +- .../entities/articleBackground.entity.ts | 2 + .../articleCategories.repository.ts | 24 ++++--- .../articleCategories.service.ts | 2 +- .../articleCategories-response.dto.ts | 2 +- .../controllers/articles-read.controller.ts | 2 +- .../dtos/common/article-with-user.dto.ts | 8 +++ .../request/article-create-request.dto.ts | 5 +- .../dtos/request/article-patch-request.dto.ts | 4 +- .../article-detail-for-update-response.dto.ts | 7 ++ .../response/article-detail-response.dto.ts | 18 ++--- .../response/articles-get-response.dto.ts | 6 +- src/APIs/articles/entities/article.entity.ts | 5 +- .../articles-paginate.repository.ts | 6 +- .../repositories/articles-read.repository.ts | 17 ++--- .../services/articles-create.service.ts | 1 - .../services/articles-read.service.ts | 2 +- .../services/articles-update.service.ts | 3 +- src/APIs/comments/comments.repository.ts | 9 ++- src/APIs/comments/comments.service.ts | 4 +- src/APIs/likes/likes.repository.ts | 18 ++--- .../dtos/request/report-create-request.dto.ts | 5 +- .../interfaces/reports.service.interface.ts | 2 +- src/APIs/reports/reports.controller.ts | 2 +- src/APIs/reports/reports.service.ts | 6 +- .../stickerCategories-map-request.dto.ts | 6 +- ...tickerCategoryMapper-create-request.dto.ts | 7 ++ .../stickerCategories.service.interface.ts | 9 ++- .../stickerCategories.controller.ts | 22 +++---- .../stickerCategories.service.ts | 65 +++++++++++++------ src/APIs/stickers/stickers.controller.ts | 2 +- src/APIs/stickers/stickers.service.ts | 6 -- .../users/services/users-delete.service.ts | 6 +- src/common/enums/report-target.enum.ts | 2 +- src/common/filter/http-exception.filter.ts | 29 ++++++++- .../validators/custom-number.decorator.ts | 25 +++++++ .../validators/custom-string.decorator.ts | 25 +++++++ src/utils/classUtils.ts | 17 ++--- 41 files changed, 271 insertions(+), 131 deletions(-) create mode 100644 src/APIs/articles/dtos/common/article-with-user.dto.ts create mode 100644 src/APIs/stickerCategories/dtos/request/stickerCategoryMapper-create-request.dto.ts create mode 100644 src/common/validators/custom-number.decorator.ts create mode 100644 src/common/validators/custom-string.decorator.ts diff --git a/src/APIs/announcements/announcements.controller.ts b/src/APIs/announcements/announcements.controller.ts index 0a2f8cd..c93f025 100644 --- a/src/APIs/announcements/announcements.controller.ts +++ b/src/APIs/announcements/announcements.controller.ts @@ -57,14 +57,14 @@ export class AnnouncementsController { @ApiTags('어드민 API') @ApiOperation({ summary: '[어드민용] 공지사항 수정' }) @ApiCookieAuth() - @ApiOkResponse({ type: [AnnouncementDto] }) + @ApiOkResponse({ type: AnnouncementDto }) @UseGuards(AuthGuardV2) @Patch('users/admin/anmts/:announcementId') async patchAnmt( @Req() req: Request, @Body() body: AnnouncementPatchRequestDto, @Param('announcementId') announcementId: number, - ): Promise { + ): Promise { const userId = req.user.userId; return await this.announcementsService.patchAnnouncement({ ...body, diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index 96c9493..822446e 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -36,7 +36,7 @@ export class AnnouncementsService { announcementId, title, content, - }: IAnnouncementsSercicePatchAnnouncement): Promise { + }: IAnnouncementsSercicePatchAnnouncement): Promise { await this.svc_usersValidate.adminCheck({ userId }); const anmt = await this.repo_announcements.findOne({ where: { id: announcementId }, @@ -45,7 +45,7 @@ export class AnnouncementsService { if (title) anmt.title = title; if (content) anmt.content = content; await this.repo_announcements.save(anmt); - return await this.repo_announcements.find({ where: { id: anmt.id } }); + return await this.repo_announcements.findOne({ where: { id: anmt.id } }); } async removeAnnouncement({ diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index 5337c80..ea01cfd 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -17,7 +17,6 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { ArticleBackground } from './entities/articleBackground.entity'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; @@ -55,7 +54,7 @@ export class ArticleBackgroundsController { @ApiOperation({ summary: '내지 모두 불러오기' }) @ApiOkResponse({ description: '모든 내지 fetch 완료', - type: [ArticleBackground], + type: [ArticleBackgroundDto], }) @Get('article/backgrounds') async getArticleBackgrounds(): Promise { @@ -64,10 +63,10 @@ export class ArticleBackgroundsController { @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) - @Delete('users/admin/article/background/:articleId') - async delete(@Param('articleId') articleId: string) { + @Delete('users/admin/article/background/:articleBackgroundId') + async delete(@Param('articleBackgroundId') articleBackgroundId: string) { return await this.articleBackgroundsService.deleteArticleBackground({ - articleId, + articleBackgroundId, }); } } diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts index 7c6d6a1..58171a1 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -30,9 +30,9 @@ export class ArticleBackgroundsService { return await this.repo_articleBackgrounds.find(); } - async deleteArticleBackground({ articleId }) { + async deleteArticleBackground({ articleBackgroundId }) { const articleBackground = await this.repo_articleBackgrounds.findOne({ - where: { id: articleId }, + where: { id: articleBackgroundId }, }); await this.repo_articleBackgrounds.remove(articleBackground); await this.svc_images.deleteImage({ url: articleBackground.imageUrl }); diff --git a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts index 2b1dac5..58e77d7 100644 --- a/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts +++ b/src/APIs/articleBackgrounds/entities/articleBackground.entity.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; import { Article } from 'src/APIs/articles/entities/article.entity'; import { CommonEntity } from 'src/common/entities/common.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; @@ -7,6 +8,7 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; export class ArticleBackground extends CommonEntity { @ApiProperty({ description: 'PK: A_I_', type: Number }) @PrimaryGeneratedColumn() + @IsNumber() id: number; @ApiProperty({ type: String, description: '이미지가 저장된 url' }) diff --git a/src/APIs/articleCategories/articleCategories.repository.ts b/src/APIs/articleCategories/articleCategories.repository.ts index c421f1b..d2954e1 100644 --- a/src/APIs/articleCategories/articleCategories.repository.ts +++ b/src/APIs/articleCategories/articleCategories.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { ArticleCategory } from './entities/articleCategory.entity'; +import { ArticleCategoriesResponseDto } from './dtos/response/articleCategories-response.dto'; @Injectable() export class ArticleCategoriesRepository extends Repository { @@ -8,21 +9,24 @@ export class ArticleCategoriesRepository extends Repository { super(ArticleCategory, db_dataSource.createEntityManager()); } - async fetchUserCategory({ scope, userId }) { - const query = this.createQueryBuilder('pc') + async fetchUserCategory({ + scope, + userId, + }): Promise { + const query = this.createQueryBuilder('ac') .select([ - 'COALESCE(COUNT(p.id), 0) as postCount', // postCategory당 posts의 개수를 집계 - 'pc.id as categoryId', // postCategory에 대한 그룹화를 위해 id 열을 추가 - 'pc.name as categoryName', + 'COALESCE(COUNT(a.id), 0) as articleCount', // postCategory당 posts의 개수를 집계 + 'ac.id as categoryId', // postCategory에 대한 그룹화를 위해 id 열을 추가 + 'ac.name as categoryName', ]) .leftJoin( - 'pc.posts', - 'p', - 'p.scope IN (:scope) AND p.isPublished = true', + 'ac.articles', + 'a', + 'a.scope IN (:scope) AND a.is_published = true', { scope }, ) // LEFT JOIN으로 연결된 엔티티의 조건을 추가 - .where('pc.userKakaoId = :userKakaoId', { userId }) - .groupBy('pc.id'); // postCategory.id를 기준으로 그룹화 + .where('ac.userId = :userId', { userId }) + .groupBy('ac.id'); // postCategory.id를 기준으로 그룹화 console.log('??', userId); return await query.getRawMany(); diff --git a/src/APIs/articleCategories/articleCategories.service.ts b/src/APIs/articleCategories/articleCategories.service.ts index 122367b..7f8be41 100644 --- a/src/APIs/articleCategories/articleCategories.service.ts +++ b/src/APIs/articleCategories/articleCategories.service.ts @@ -30,7 +30,7 @@ export class ArticleCategoriesService { throw new BadRequestException('이미 동명의 카테고리가 존재합니다.'); } const result = await this.repo_articleCategories.save({ - user: { id: userId }, + userId, name, }); return result; diff --git a/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts index 87fec79..51a5d19 100644 --- a/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts +++ b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class ArticleCategoriesResponseDto { @ApiProperty({ type: Number }) - postCount: number; + articleCount: number; @ApiProperty({ type: String }) categoryId: string; diff --git a/src/APIs/articles/controllers/articles-read.controller.ts b/src/APIs/articles/controllers/articles-read.controller.ts index f49daa6..2c16ba3 100644 --- a/src/APIs/articles/controllers/articles-read.controller.ts +++ b/src/APIs/articles/controllers/articles-read.controller.ts @@ -147,7 +147,7 @@ export class ArticlesReadController { '로그인 된 유저의 경우 private/protected 게시글 조회 권한 체크 후 조회. 카테고리 이름으로 필터링 가능', }) @Get('/cursor/user/:userId') - @ApiOkResponse({ type: ArticlesGetRequestDto }) + @ApiOkResponse({ type: ArticlesGetResponseDto }) async fetchUserArticles( @Param('userId') targetUserId: number, @Req() req: Request, diff --git a/src/APIs/articles/dtos/common/article-with-user.dto.ts b/src/APIs/articles/dtos/common/article-with-user.dto.ts new file mode 100644 index 0000000..2640b3c --- /dev/null +++ b/src/APIs/articles/dtos/common/article-with-user.dto.ts @@ -0,0 +1,8 @@ +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/response/user-primary-response.dto'; +import { ArticleDto } from './article.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ArticleWithUserDto extends ArticleDto { + @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) + user: UserPrimaryResponseDto; +} diff --git a/src/APIs/articles/dtos/request/article-create-request.dto.ts b/src/APIs/articles/dtos/request/article-create-request.dto.ts index 23a150c..d946415 100644 --- a/src/APIs/articles/dtos/request/article-create-request.dto.ts +++ b/src/APIs/articles/dtos/request/article-create-request.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, ValidateNested } from 'class-validator'; +import { IsArray, IsOptional, ValidateNested } from 'class-validator'; import { ArticleDto } from '../common/article.dto'; import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto'; @@ -17,8 +17,9 @@ export class ArticleCreateRequestDto extends OmitType(ArticleDto, [ 'dateUpdated', ]) { @ApiProperty({ type: [StickerBlocksCreateDto] }) - @IsArray() + @IsArray({ message: 'stickerBlocks는 배열이여야 합니다.' }) @ValidateNested({ each: true }) @Type(() => StickerBlocksCreateDto) + @IsOptional() stickerBlocks: StickerBlocksCreateDto[]; } diff --git a/src/APIs/articles/dtos/request/article-patch-request.dto.ts b/src/APIs/articles/dtos/request/article-patch-request.dto.ts index 7c48058..1ebb26f 100644 --- a/src/APIs/articles/dtos/request/article-patch-request.dto.ts +++ b/src/APIs/articles/dtos/request/article-patch-request.dto.ts @@ -1,6 +1,6 @@ -import { PartialType } from '@nestjs/swagger'; +import { OmitType, PartialType } from '@nestjs/swagger'; import { ArticleCreateRequestDto } from './article-create-request.dto'; export class ArticlePatchRequestDto extends PartialType( - ArticleCreateRequestDto, + OmitType(ArticleCreateRequestDto, ['stickerBlocks']), ) {} diff --git a/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts index 58fb9b7..0ab1f80 100644 --- a/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts +++ b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts @@ -1,4 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArticleDetailResponseDto } from './article-detail-response.dto'; +import { StickerBlockDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlock.dto'; + export class ArticleDetailForUpdateResponseDto { + @ApiProperty({ type: ArticleDetailResponseDto }) article; + + @ApiProperty({ type: [StickerBlockDto] }) stickerBlocks; } diff --git a/src/APIs/articles/dtos/response/article-detail-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-response.dto.ts index ec36fd9..d0fdf9c 100644 --- a/src/APIs/articles/dtos/response/article-detail-response.dto.ts +++ b/src/APIs/articles/dtos/response/article-detail-response.dto.ts @@ -1,15 +1,17 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Article } from '../../entities/article.entity'; import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/response/user-primary-response.dto'; +import { ArticleDto } from '../common/article.dto'; +import { ArticleCategoryDto } from 'src/APIs/articleCategories/dtos/common/articleCategory.dto'; +import { ArticleBackgroundDto } from 'src/APIs/articleBackgrounds/dtos/common/articleBackground.dto'; -export class ArticleDetailResponseDto extends OmitType(Article, [ - 'comments', - 'user', - 'stickerBlocks', - 'reports', - 'notifications', -]) { +export class ArticleDetailResponseDto extends OmitType(ArticleDto, []) { @ApiProperty({ description: '작성자의 정보', type: UserPrimaryResponseDto }) user: UserPrimaryResponseDto; + + @ApiProperty({ description: '카테고리 정보', type: ArticleCategoryDto }) + articleCategory: ArticleCategoryDto; + + @ApiProperty({ description: '배경 정보', type: ArticleBackgroundDto }) + articleBackground: ArticleBackgroundDto; } diff --git a/src/APIs/articles/dtos/response/articles-get-response.dto.ts b/src/APIs/articles/dtos/response/articles-get-response.dto.ts index be8bab6..1be7061 100644 --- a/src/APIs/articles/dtos/response/articles-get-response.dto.ts +++ b/src/APIs/articles/dtos/response/articles-get-response.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { CustomCursorPageMetaDto } from 'src/utils/cursor-pages/dtos/cursor-page-meta.dto'; -import { ArticleDto } from '../common/article.dto'; +import { ArticleWithUserDto } from '../common/article-with-user.dto'; export class ArticlesGetResponseDto { - @ApiProperty({ description: '조회된 데이터', type: [ArticleDto] }) - readonly data: ArticleDto[]; + @ApiProperty({ description: '조회된 데이터', type: [ArticleWithUserDto] }) + readonly data: ArticleWithUserDto[]; @ApiProperty({ description: '페이지네이션 메타 데이터', diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 087ffcb..01686e8 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -36,15 +36,14 @@ export class Article extends IndexedCommonEntity { @ApiProperty({ description: '연결된 카테고리 fk', type: Number }) @Column({ name: 'article_category_id', nullable: true }) @RelationId((article: Article) => article.articleCategory) - @IsString() + @IsNumber() @IsOptional() articleCategoryId: number; - @IsString() @ApiProperty({ description: '연결된 내지 fk', type: Number }) @Column({ name: 'article_background_id', nullable: true }) @RelationId((article: Article) => article.articleBackground) - @IsString() + @IsNumber() @IsOptional() articleBackgroundId: number; diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts index 4b19c08..a33ab9b 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts @@ -29,8 +29,8 @@ export class ArticlesPaginateRepository extends Repository
{ queryBuilder .take(take + 1) .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.article_background', 'article_background') - .leftJoinAndSelect('p.article_category', 'article_category') + // .leftJoinAndSelect('p.articleBackground', 'article_background') + // .leftJoinAndSelect('p.articleCategory', 'article_category') .addSelect([ 'user.handle', 'user.id', @@ -133,7 +133,7 @@ export class ArticlesPaginateRepository extends Repository
{ }); } queryBuilder - .andWhere('p.user_id = :user_id', { + .andWhere('p.user_id = :userId', { userId, }) .andWhere('p.scope IN (:scope)', { scope }); diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index eb40767..fc5d67f 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -2,6 +2,7 @@ import { DataSource, Repository } from 'typeorm'; import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; +import { ArticleDto } from '../dtos/common/article.dto'; @Injectable() export class ArticlesReadRepository extends Repository
{ @@ -14,8 +15,8 @@ export class ArticlesReadRepository extends Repository
{ }); return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.article_background', 'article_background') - .leftJoinAndSelect('p.article_category', 'article_category') + .leftJoinAndSelect('p.articleBackground', 'article_background') + .leftJoinAndSelect('p.articleCategory', 'article_category') .addSelect([ 'user.handle', 'user.id', @@ -29,11 +30,11 @@ export class ArticlesReadRepository extends Repository
{ .getOne(); } - async readUpdateDetail({ articleId }) { + async readUpdateDetail({ articleId }): Promise { return await this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.article_background', 'article_background') - .leftJoinAndSelect('p.article_category', 'article_category') + .leftJoinAndSelect('p.articleBackground', 'article_background') + .leftJoinAndSelect('p.articleCategory', 'article_category') .addSelect([ 'user.handle', 'user.id', @@ -46,11 +47,11 @@ export class ArticlesReadRepository extends Repository
{ .getOne(); } - async readTemp({ userId }): Promise { + async readTemp({ userId }): Promise { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') - .leftJoinAndSelect('p.article_background', 'article_background') - .leftJoinAndSelect('p.article_category', 'article_category') + .leftJoinAndSelect('p.articleBackground', 'article_background') + .leftJoinAndSelect('p.articleCategory', 'article_category') .addSelect([ 'user.handle', 'user.id', diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts index 448c5f0..1dd2d5f 100644 --- a/src/APIs/articles/services/articles-create.service.ts +++ b/src/APIs/articles/services/articles-create.service.ts @@ -9,7 +9,6 @@ import { StickerBlocksService } from 'src/APIs/stickerBlocks/stickerBlocks.servi import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesReadRepository } from '../repositories/articles-read.repository'; import { ArticleCreateResponseDto } from '../dtos/response/article-create-response.dto'; -import { getUUID } from 'src/utils/uuidUtils'; import { ImagesService } from 'src/modules/images/images.service'; import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts index f30adaf..c605cbb 100644 --- a/src/APIs/articles/services/articles-read.service.ts +++ b/src/APIs/articles/services/articles-read.service.ts @@ -47,7 +47,7 @@ export class ArticlesReadService { } async readTempArticles({ userId }): Promise { - return await this.repo_articlesRead.readTemp(userId); + return await this.repo_articlesRead.readTemp({ userId }); } async readArticleDetail({ diff --git a/src/APIs/articles/services/articles-update.service.ts b/src/APIs/articles/services/articles-update.service.ts index 254ad9a..28e3b15 100644 --- a/src/APIs/articles/services/articles-update.service.ts +++ b/src/APIs/articles/services/articles-update.service.ts @@ -1,9 +1,10 @@ -import { ForbiddenException } from '@nestjs/common'; +import { ForbiddenException, Injectable } from '@nestjs/common'; import { ArticlesValidateService } from './articles-validate.service'; import { ArticlesCreateRepository } from '../repositories/articles-create.repository'; import { IArticlesServicePatchArticle } from '../interfaces/articles.service.interface'; import { ArticleDto } from '../dtos/common/article.dto'; +@Injectable() export class ArticlesUpdateService { constructor( private readonly svc_articlesValidate: ArticlesValidateService, diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 7412668..17afd9c 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -27,7 +27,14 @@ export class CommentsRepository extends Repository { commentId, }: ICommentsRepositoryId): Promise { return await this.createQueryBuilder('c') - .leftJoinAndSelect('c.user', 'user') + .leftJoin('c.user', 'user') + .addSelect([ + 'user.handle', + 'user.id', + 'user.description', + 'user.profile_image', + 'user.username', + ]) .leftJoinAndSelect('c.article', 'article') .leftJoinAndSelect('c.parent', 'parent') .where('c.id = :commentId', { commentId }) diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 09022d0..9dce56e 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -79,7 +79,9 @@ export class CommentsService { const commentData = await this.repo_comments.insertComment({ createCommentDto, }); - const { commentId } = commentData.identifiers[0]; + console.log(commentData); + const commentId = commentData.identifiers[0].id; + const { article, parent, ...result } = await this.repo_comments.fetchCommentWithNotiInfo({ commentId }); diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index ace8d11..a071925 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -16,25 +16,25 @@ export class LikesRepository extends Repository { userId, articleId, }: ILikesRepositoryIds): Promise { - const users = await this.createQueryBuilder('likes') - .innerJoin('likes.posts', 'posts') - .leftJoin('likes.user', 'user') + const users = await this.createQueryBuilder('like') + .innerJoin('like.article', 'article') + .leftJoin('like.user', 'user') .leftJoinAndSelect( (subQuery) => { return subQuery - .select('follow.toUserKakaoId', 'toUserKakaoId') + .select('follow.to_user_id', 'to_user_id') .from(Follow, 'follow') - .where('follow.fromUserKakaoId = :kakaoId'); + .where('follow.from_user_id = :userId'); }, 'follow', - 'follow.toUserKakaoId = user.kakaoId', + 'follow.to_user_id = user.id', ) .where('user.date_deleted IS NULL') - .andWhere('posts.date_deleted IS NULL') - .andWhere('likes.postsId = :id') + .andWhere('article.date_deleted IS NULL') + .andWhere('like.articleId = :articleId') .select([ ...getUserFields().map((column) => `user.${column} AS ${column}`), - 'CASE WHEN follow.toUserKakaoId IS NOT NULL THEN true ELSE false END AS is_following', + 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ articleId, userId }) .getRawMany(); diff --git a/src/APIs/reports/dtos/request/report-create-request.dto.ts b/src/APIs/reports/dtos/request/report-create-request.dto.ts index 8e4915a..157b11c 100644 --- a/src/APIs/reports/dtos/request/report-create-request.dto.ts +++ b/src/APIs/reports/dtos/request/report-create-request.dto.ts @@ -1,4 +1,7 @@ import { PickType } from '@nestjs/swagger'; import { Report } from '../../entities/report.entity'; -export class ReportCreateRequestDto extends PickType(Report, ['content']) {} +export class ReportCreateRequestDto extends PickType(Report, [ + 'content', + 'type', +]) {} diff --git a/src/APIs/reports/interfaces/reports.service.interface.ts b/src/APIs/reports/interfaces/reports.service.interface.ts index f425879..d5d9f0f 100644 --- a/src/APIs/reports/interfaces/reports.service.interface.ts +++ b/src/APIs/reports/interfaces/reports.service.interface.ts @@ -1,6 +1,6 @@ import { ReportDto } from '../dtos/common/report.dto'; export interface IReportsServiceCreateReport - extends Pick { + extends Pick { targetId: number; } diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 7a1821c..5f94190 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -43,7 +43,7 @@ export class ReportsController { const userId = req.user.userId; return await this.reportsService.createReport({ targetId, - target: ReportTarget.POSTS, + target: ReportTarget.ARTICLES, userId, ...body, }); diff --git a/src/APIs/reports/reports.service.ts b/src/APIs/reports/reports.service.ts index afc8f26..6f9bf2f 100644 --- a/src/APIs/reports/reports.service.ts +++ b/src/APIs/reports/reports.service.ts @@ -25,14 +25,14 @@ export class ReportsService { async createReport( dto_createReport: IReportsServiceCreateReport, ): Promise { - const { target, userId, content, targetId } = dto_createReport; + const { target, userId, content, targetId, type } = dto_createReport; const queryRunner = this.db_dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { let data; switch (target) { - case ReportTarget.POSTS: + case ReportTarget.ARTICLES: const articleData = await queryRunner.manager.findOne(Article, { where: { id: targetId }, }); @@ -50,6 +50,7 @@ export class ReportsService { }); data = await queryRunner.manager.save(Report, { target, + type, userId, targetUserId: articleData.userId, content, @@ -75,6 +76,7 @@ export class ReportsService { }); data = await queryRunner.manager.save(Report, { target, + type, userId, targetUserId: commentData.userId, content, diff --git a/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts b/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts index 1b53cfc..74cc896 100644 --- a/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts +++ b/src/APIs/stickerCategories/dtos/request/stickerCategories-map-request.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; -import { StickerCategoryMapperDto } from '../common/stickerCategoryMapper.dto'; +import { StickerCategoryMapperCreateRequestDto } from './stickerCategoryMapper-create-request.dto'; export class StickerCategoriesMapDto { @ApiProperty({ description: '매핑할 카테고리 및 스티커 배열', - type: [StickerCategoryMapperDto], + type: [StickerCategoryMapperCreateRequestDto], }) - maps: StickerCategoryMapperDto[]; + maps: StickerCategoryMapperCreateRequestDto[]; } diff --git a/src/APIs/stickerCategories/dtos/request/stickerCategoryMapper-create-request.dto.ts b/src/APIs/stickerCategories/dtos/request/stickerCategoryMapper-create-request.dto.ts new file mode 100644 index 0000000..ea6ad4a --- /dev/null +++ b/src/APIs/stickerCategories/dtos/request/stickerCategoryMapper-create-request.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { StickerCategoryMapperDto } from '../common/stickerCategoryMapper.dto'; + +export class StickerCategoryMapperCreateRequestDto extends OmitType( + StickerCategoryMapperDto, + ['dateCreated', 'dateDeleted', 'dateUpdated'], +) {} diff --git a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts index 12d5409..391fbe7 100644 --- a/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts +++ b/src/APIs/stickerCategories/interfaces/stickerCategories.service.interface.ts @@ -1,8 +1,8 @@ -import { StickerCategoryMapperDto } from '../dtos/common/stickerCategoryMapper.dto'; +import { StickerCategoryMapperCreateRequestDto } from '../dtos/request/stickerCategoryMapper-create-request.dto'; export interface IStickerCategoriesServiceMapCategory { userId: number; - maps: StickerCategoryMapperDto[]; + maps: StickerCategoryMapperCreateRequestDto[]; } export interface IStickerCategoriesServiceId { @@ -17,3 +17,8 @@ export interface IStickerCategoriesServiceCreateCategory { userId: number; name: string; } + +export interface IStickerCategoriesServiceIds { + stickerId: number; + stickerCategoryId: number; +} diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 6512825..35d091b 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -15,11 +15,11 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; -import { StickerCategory } from './entities/stickerCategory.entity'; import { AuthGuardV2 } from 'src/common/guards/auth.guard'; -import { StickerCategoryMapper } from './entities/stickerCategoryMapper.entity'; import { StickerCategoryCreateRequestDto } from './dtos/request/stickerCategory-create-request.dto'; import { StickerCategoriesMapDto } from './dtos/request/stickerCategories-map-request.dto'; +import { StickerCategoryMapperDto } from './dtos/common/stickerCategoryMapper.dto'; +import { StickerCategoryDto } from './dtos/common/stickerCategory.dto'; @ApiTags('스티커 API') @Controller() @@ -32,9 +32,9 @@ export class StickerCategoriesController { summary: '카테고리 fetchAll', description: '카테고리를 모두 조회한다.', }) - @ApiOkResponse({ type: [StickerCategory] }) + @ApiOkResponse({ type: [StickerCategoryDto] }) @Get('stickers/categories') - async fetchCategories(): Promise { + async fetchCategories(): Promise { return await this.stickerCategoriesService.fetchCategories(); } @@ -42,11 +42,11 @@ export class StickerCategoriesController { summary: '카테고리 id에 해당하는 스티커를 fetchAll', description: '카테고리를 id로 찾고, 이에 매핑된 스티커들을 가져온다', }) - @ApiOkResponse({ type: [StickerCategoryMapper] }) - @Get('stickers/categories/:id') + @ApiOkResponse({ type: [StickerCategoryMapperDto] }) + @Get('stickers/categories/:stickerCategoryId') async fetchStickersByCategoryName( @Param('stickerCategoryId') stickerCategoryId: number, - ): Promise { + ): Promise { return await this.stickerCategoriesService.fetchStickersByCategoryId({ stickerCategoryId, }); @@ -57,14 +57,14 @@ export class StickerCategoriesController { summary: '[어드민용] 스티커 카테고리 생성', description: '[어드민 전용] 스티커 카테고리를 만든다.', }) - @ApiOkResponse({ type: StickerCategory }) + @ApiOkResponse({ type: StickerCategoryDto }) @ApiCookieAuth() @UseGuards(AuthGuardV2) @Post('users/admin/stickers/categories') async createCategory( @Req() req: Request, @Body() body: StickerCategoryCreateRequestDto, - ): Promise { + ): Promise { const userId = req.user.userId; return await this.stickerCategoriesService.createCategory({ userId, @@ -78,13 +78,13 @@ export class StickerCategoriesController { description: '[어드민 전용] 스티커에 카테고리를 매핑한다.', }) @ApiCookieAuth() - @ApiOkResponse({ type: [StickerCategoryMapper] }) + @ApiOkResponse({ type: [StickerCategoryMapperDto] }) @UseGuards(AuthGuardV2) @Post('users/admin/stickers/map') async mapCategory( @Req() req: Request, @Body() mapCategoryDto: StickerCategoriesMapDto, - ): Promise { + ): Promise { const userId = req.user.userId; return await this.stickerCategoriesService.mapCategory({ userId, diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 7f2dba6..1b83e57 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { Repository } from 'typeorm'; import { StickerCategory } from './entities/stickerCategory.entity'; import { InjectRepository } from '@nestjs/typeorm'; @@ -7,32 +11,35 @@ import { StickersService } from '../stickers/stickers.service'; import { IStickerCategoriesServiceCreateCategory, IStickerCategoriesServiceId, + IStickerCategoriesServiceIds, IStickerCategoriesServiceMapCategory, IStickerCategoriesServiceName, } from './interfaces/stickerCategories.service.interface'; import { UsersValidateService } from '../users/services/users-validate-service'; +import { StickerCategoryDto } from './dtos/common/stickerCategory.dto'; +import { StickerCategoryMapperDto } from './dtos/common/stickerCategoryMapper.dto'; @Injectable() export class StickerCategoriesService { constructor( @InjectRepository(StickerCategory) - private readonly stickerCategoriesRepository: Repository, + private readonly repo_stickerCategories: Repository, @InjectRepository(StickerCategoryMapper) - private readonly stickerCategoryMappersRepository: Repository, + private readonly repo_stickerCategoryMappers: Repository, private readonly svc_usersValidate: UsersValidateService, - private readonly stickersService: StickersService, + private readonly svc_stickers: StickersService, ) {} async findCategoryByName({ name, - }: IStickerCategoriesServiceName): Promise { - return await this.stickerCategoriesRepository.findOne({ where: { name } }); + }: IStickerCategoriesServiceName): Promise { + return await this.repo_stickerCategories.findOne({ where: { name } }); } async findCategoryById({ stickerCategoryId, - }: IStickerCategoriesServiceId): Promise { - return await this.stickerCategoriesRepository.findOne({ + }: IStickerCategoriesServiceId): Promise { + return await this.repo_stickerCategories.findOne({ where: { id: stickerCategoryId }, }); } @@ -40,7 +47,7 @@ export class StickerCategoriesService { name, }: IStickerCategoriesServiceName): Promise { const data = await this.findCategoryByName({ name }); - if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); + if (data) throw new ConflictException('동명의 카테고리가 존재합니다.'); } async existCheckById({ stickerCategoryId, @@ -49,38 +56,54 @@ export class StickerCategoriesService { if (!data) throw new NotFoundException('스티커 카테고리가 없습니다.'); } - async fetchCategories(): Promise { - return await this.stickerCategoriesRepository.find(); + async existCheckMapper({ + stickerId, + stickerCategoryId, + }: IStickerCategoriesServiceIds): Promise { + const data = await this.repo_stickerCategoryMappers.findOne({ + where: { stickerId, stickerCategoryId }, + }); + if (data) throw new ConflictException('이미 매핑 된 카테고리입니다.'); + } + async fetchCategories(): Promise { + return await this.repo_stickerCategories.find(); } async createCategory({ userId, name, - }: IStickerCategoriesServiceCreateCategory): Promise { + }: IStickerCategoriesServiceCreateCategory): Promise { await this.svc_usersValidate.adminCheck({ userId }); - return await this.stickerCategoriesRepository.save({ name }); + await this.existCheckByName({ name }); + return await this.repo_stickerCategories.save({ name }); } async mapCategory({ userId, maps, - }: IStickerCategoriesServiceMapCategory): Promise { + }: IStickerCategoriesServiceMapCategory): Promise< + StickerCategoryMapperDto[] + > { await this.svc_usersValidate.adminCheck({ userId }); - maps.forEach(async (map) => { + for (const map of maps) { await this.existCheckById({ stickerCategoryId: map.stickerCategoryId }); - await this.stickersService.existCheck({ + await this.svc_stickers.existCheck({ stickerId: map.stickerId, }); - }); - return await this.stickerCategoryMappersRepository.save(maps); + await this.existCheckMapper({ + stickerCategoryId: map.stickerCategoryId, + stickerId: map.stickerId, + }); + } + return await this.repo_stickerCategoryMappers.save(maps); } async fetchStickersByCategoryId({ stickerCategoryId, - }: IStickerCategoriesServiceId): Promise { + }: IStickerCategoriesServiceId): Promise { await this.existCheckById({ stickerCategoryId }); - return await this.stickerCategoryMappersRepository.find({ - relations: { sticker: true, stickerCategory: true }, + return await this.repo_stickerCategoryMappers.find({ + // relations: { sticker: true, stickerCategory: true }, where: { stickerCategory: { id: stickerCategoryId }, sticker: { isDefault: true }, diff --git a/src/APIs/stickers/stickers.controller.ts b/src/APIs/stickers/stickers.controller.ts index 095f3c0..eb9f403 100644 --- a/src/APIs/stickers/stickers.controller.ts +++ b/src/APIs/stickers/stickers.controller.ts @@ -84,7 +84,7 @@ export class StickersController { description: '본인이 만든 스티커를 patch한다. image_url 변경 시 기존의 이미지는 s3에서 제거된다.', }) - @Patch('stickers/:id') + @Patch('stickers/:stickerId') @UseGuards(AuthGuardV2) @ApiCookieAuth() @ApiOkResponse({ type: StickerDto }) diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 040195e..7d733ef 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -47,9 +47,6 @@ export class StickersService { .insert() .into(Sticker, ['user_id', 'image_url', 'is_default']) .values({ userId, imageUrl, isDefault: false }) - .orUpdate(['image_url', 'isDefault'], ['id'], { - skipUpdateIfNoValuesChanged: true, - }) .execute(); const id = insertData.identifiers[0].id; const data = await this.repo_stickers.findOne({ where: { id } }); @@ -70,9 +67,6 @@ export class StickersService { .insert() .into(Sticker, ['user_id', 'image_url', 'is_default', 'is_reusable']) .values({ userId, imageUrl, isDefault: true, isReusable: true }) - .orUpdate(['image_url', 'isDefault', 'isReusable'], ['id'], { - skipUpdateIfNoValuesChanged: true, - }) .execute(); const id = insertData.identifiers[0].id; const data = await this.repo_stickers.findOne({ where: { id } }); diff --git a/src/APIs/users/services/users-delete.service.ts b/src/APIs/users/services/users-delete.service.ts index 0525005..4a76165 100644 --- a/src/APIs/users/services/users-delete.service.ts +++ b/src/APIs/users/services/users-delete.service.ts @@ -78,8 +78,8 @@ export class UsersDeleteService { for (const following of followingsToDelete) { await queryRunner.manager.decrement( User, - { kakaoId: following.toUserId }, - 'follower_count', + { id: following.toUserId }, + 'followerCount', 1, ); } @@ -92,7 +92,7 @@ export class UsersDeleteService { await queryRunner.manager.decrement( User, { userId: following.fromUserId }, - 'following_count', + 'followingCount', 1, ); } diff --git a/src/common/enums/report-target.enum.ts b/src/common/enums/report-target.enum.ts index fd1f998..86b2d86 100644 --- a/src/common/enums/report-target.enum.ts +++ b/src/common/enums/report-target.enum.ts @@ -1,4 +1,4 @@ export enum ReportTarget { - POSTS = 'ARTICLES', + ARTICLES = 'ARTICLES', COMMENTS = 'COMMENTS', } diff --git a/src/common/filter/http-exception.filter.ts b/src/common/filter/http-exception.filter.ts index 567ca90..5e18e7c 100644 --- a/src/common/filter/http-exception.filter.ts +++ b/src/common/filter/http-exception.filter.ts @@ -4,21 +4,44 @@ import { Catch, ArgumentsHost, } from '@nestjs/common'; +import { ValidationError } from 'class-validator'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { - // HTTP 요청과 응답 객체를 가져옵니다. const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); - const message = exception.message; + const exceptionResponse = exception.getResponse(); + + let message: string | object; + + if ( + typeof exceptionResponse === 'object' && + (exceptionResponse as any).message + ) { + const responseMessage = (exceptionResponse as any).message; + if ( + Array.isArray(responseMessage) && + responseMessage[0] instanceof ValidationError + ) { + message = responseMessage + .map((error: ValidationError) => { + return `${error.property} has wrong value ${error.value}, ${Object.values(error.constraints).join(', ')}`; + }) + .join('; '); + } else { + message = responseMessage; + } + } else { + message = exception.message; + } console.log('========='); console.log('예외 코드: ' + status); - console.log('예외 내용: ' + message); + console.log('예외 내용: ', message); console.log('========='); response.status(status).json({ diff --git a/src/common/validators/custom-number.decorator.ts b/src/common/validators/custom-number.decorator.ts new file mode 100644 index 0000000..0e7201c --- /dev/null +++ b/src/common/validators/custom-number.decorator.ts @@ -0,0 +1,25 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + isNumber, +} from 'class-validator'; + +export function IsNumberWithMessage(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isNumberWithMessage', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value, validationArguments) { + return isNumber(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a number, but received ${typeof args.value}`; + }, + }, + }); + }; +} diff --git a/src/common/validators/custom-string.decorator.ts b/src/common/validators/custom-string.decorator.ts new file mode 100644 index 0000000..d946a2d --- /dev/null +++ b/src/common/validators/custom-string.decorator.ts @@ -0,0 +1,25 @@ +import { + isString, + registerDecorator, + ValidationArguments, + ValidationOptions, +} from 'class-validator'; + +export function IsStringWithMessage(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isStringWithMessage', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value, validationArguments) { + return isString(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a string, but received ${typeof args.value}`; + }, + }, + }); + }; +} diff --git a/src/utils/classUtils.ts b/src/utils/classUtils.ts index 1cb552a..fcddaaf 100644 --- a/src/utils/classUtils.ts +++ b/src/utils/classUtils.ts @@ -1,17 +1,18 @@ import 'reflect-metadata'; import { User } from 'src/APIs/users/entities/user.entity'; +import { CommonEntity } from 'src/common/entities/common.entity'; import { getMetadataArgsStorage } from 'typeorm'; export function getUserFields(): string[] { const metadata = getMetadataArgsStorage(); - - // 클래스의 모든 멤버 변수를 담을 배열 - const members: string[] = []; - - const entityMetadata = metadata.filterColumns(User); - entityMetadata.forEach((meta) => { - members.push(meta.propertyName); - }); + // Get columns from the User entity + const userColumns = metadata.filterColumns(User); + // Get columns from the CommonEntity (superclass) + const commonEntityColumns = metadata.filterColumns(CommonEntity); + // Combine both sets of columns + const allColumns = [...userColumns, ...commonEntityColumns]; + // Extract property names + const members: string[] = allColumns.map((col) => col.propertyName); return members; } From 1d1f24d09adaaa00fced2857ee5e03fe9836e02f Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 9 Jul 2024 22:42:15 +0900 Subject: [PATCH 227/236] fix(pagination): cursor query error --- .../articleBackgrounds/articleBackgrounds.controller.ts | 6 +++--- src/APIs/articles/controllers/articles-read.controller.ts | 3 ++- .../articles/repositories/articles-paginate.repository.ts | 7 ++++--- src/common/enums/article-order-option.ts | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index ea01cfd..2ad4569 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -39,7 +39,7 @@ export class ArticleBackgroundsController { description: '이미지 서버에 파일 업로드 완료', type: ImageUploadResponseDto, }) - @Post('users/admin/article/background') + @Post('users/admin/articles/background') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async createArticleBackground( @@ -56,14 +56,14 @@ export class ArticleBackgroundsController { description: '모든 내지 fetch 완료', type: [ArticleBackgroundDto], }) - @Get('article/backgrounds') + @Get('articles/backgrounds') async getArticleBackgrounds(): Promise { return await this.articleBackgroundsService.findArticleBackgrounds(); } @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) - @Delete('users/admin/article/background/:articleBackgroundId') + @Delete('users/admin/articles/background/:articleBackgroundId') async delete(@Param('articleBackgroundId') articleBackgroundId: string) { return await this.articleBackgroundsService.deleteArticleBackground({ articleBackgroundId, diff --git a/src/APIs/articles/controllers/articles-read.controller.ts b/src/APIs/articles/controllers/articles-read.controller.ts index 2c16ba3..dffbcd8 100644 --- a/src/APIs/articles/controllers/articles-read.controller.ts +++ b/src/APIs/articles/controllers/articles-read.controller.ts @@ -24,6 +24,7 @@ import { ArticlesGetRequestDto } from '../dtos/request/articles-get-request.dto' import { ArticlesPaginateService } from '../services/articles-paginate.service'; import { SortOption } from 'src/common/enums/sort-option'; import { CustomCursorPageDto } from 'src/utils/cursor-pages/dtos/cursor-page.dto'; +import { ArticlesGetUserRequestDto } from '../dtos/request/articles-get-user-request.dto'; @ApiTags('게시글 API') @Controller('articles') @@ -151,7 +152,7 @@ export class ArticlesReadController { async fetchUserArticles( @Param('userId') targetUserId: number, @Req() req: Request, - @Query() cursorOption: ArticlesGetRequestDto, + @Query() cursorOption: ArticlesGetUserRequestDto, ): Promise> { const userId = req.user.userId; if (!cursorOption.cursor && cursorOption.sort === SortOption.ASC) { diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts index a33ab9b..0a51ec2 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts @@ -25,7 +25,7 @@ export class ArticlesPaginateRepository extends Repository
{ sort === SortOption.ASC ? `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) > :customCursor` : `CONCAT(LPAD(p.${_order}, 7, '0'), LPAD(p.id, 7, '0')) < :customCursor`; - + console.log(`p.${_order}`, sort as any); queryBuilder .take(take + 1) .innerJoin('p.user', 'user') @@ -43,8 +43,9 @@ export class ArticlesPaginateRepository extends Repository
{ customCursor: cursor, }) .andWhere('p.date_deleted IS NULL') - .orderBy(`p.${_order}`, sort as any) - .addOrderBy('p.id', sort as any); + // .orderBy('p.commentCount', sort as 'ASC' | 'DESC') + .orderBy(`p.${_order}`, sort as 'ASC' | 'DESC') + .addOrderBy('p.id', 'DESC'); return queryBuilder; } diff --git a/src/common/enums/article-order-option.ts b/src/common/enums/article-order-option.ts index 146968b..4bdc5af 100644 --- a/src/common/enums/article-order-option.ts +++ b/src/common/enums/article-order-option.ts @@ -1,7 +1,7 @@ export enum ArticleOrderOption { - LIKE = 'like_count', - VIEW = 'view_count', - COMMENT = 'comment_count', + LIKE = 'likeCount', + VIEW = 'viewCount', + COMMENT = 'commentCount', DATE = 'id', } From 8481f9994b2895612515176380dde4d83ccc7a02 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Jul 2024 15:42:03 +0900 Subject: [PATCH 228/236] fix(userFollowingDto): change to extends userPriDto --- .github/workflows/deploy-to-staging.yml | 73 +++++++++++++++++++ .../articleBackgrounds.controller.ts | 27 ++++++- .../articleBackgrounds.module.ts | 7 +- .../articleBackgrounds.service.ts | 10 ++- .../articleCategories.controller.ts | 6 +- .../articleCategories-response.dto.ts | 4 +- .../entities/articleCategory.entity.ts | 2 +- .../request/article-create-request.dto.ts | 13 +++- .../request/articles-get-user-request.dto.ts | 6 +- src/APIs/articles/entities/article.entity.ts | 6 +- .../articles-paginate.repository.ts | 29 ++++---- .../repositories/articles-read.repository.ts | 18 ++--- .../services/articles-validate.service.ts | 6 +- src/APIs/comments/comments.repository.ts | 22 ++---- src/APIs/follows/follows.repository.ts | 19 +++-- src/APIs/likes/likes.repository.ts | 12 +-- .../notifications/notifications.repository.ts | 12 +-- src/APIs/reports/reports.controller.ts | 2 +- ...rCategories-fetch-stickers-response.dto.ts | 8 ++ .../stickerCategories.controller.ts | 5 +- .../stickerCategories.service.ts | 7 +- .../response/user-following-response.dto.ts | 4 +- .../response/user-primary-response.dto.ts | 27 +++---- src/APIs/users/users.repository.ts | 8 +- 24 files changed, 226 insertions(+), 107 deletions(-) create mode 100644 .github/workflows/deploy-to-staging.yml create mode 100644 src/APIs/stickerCategories/dtos/response/stickerCategories-fetch-stickers-response.dto.ts diff --git a/.github/workflows/deploy-to-staging.yml b/.github/workflows/deploy-to-staging.yml new file mode 100644 index 0000000..dc45b88 --- /dev/null +++ b/.github/workflows/deploy-to-staging.yml @@ -0,0 +1,73 @@ +name: Deploy to Staging + +on: + push: + branches: + - staging + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '16' # 필요한 Node.js 버전으로 설정 + + - name: Install dependencies + run: npm install + + - name: Build project + run: npm run build + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to ECR + run: aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com + + - name: Build Docker image + run: docker buildx build --platform linux/amd64 -t blccu-ecr . --load + + - name: Tag Docker image + run: docker tag blccu-ecr:latest 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + + - name: Push Docker image to ECR + run: docker push 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + + - name: Deploy to server + run: | + ssh -o StrictHostKeyChecking=no -i ./keys/blccu-dev-rsa.pem ubuntu@api.blccu.com << 'EOF' + set -e + NEW_PORT=3001 + CURRENT_PORT=$(grep 'server localhost:' /etc/nginx/nginx.conf | awk '{print $2}' | cut -d ':' -f 2 | sed 's/;//') + if [ "$CURRENT_PORT" = "3001" ]; then + NEW_PORT=3000 + fi + docker pull 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + docker run --env-file .env.prod -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name blccu-ecr-$NEW_PORT -e TZ=Asia/Seoul 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + for i in {1..20}; do + HEALTH_CHECK=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:$NEW_PORT/health) + if [ "$HEALTH_CHECK" -eq 200 ]; then + break + fi + sleep 5 + done + if [ "$HEALTH_CHECK" -ne 200 ]; then + docker stop blccu-ecr-$NEW_PORT && docker rm blccu-ecr-$NEW_PORT + exit 1 + fi + sudo sed -i "s/server localhost:$CURRENT_PORT;/server localhost:$NEW_PORT;/g" /etc/nginx/nginx.conf + sudo systemctl restart nginx + docker stop blccu-ecr-$CURRENT_PORT && docker rm blccu-ecr-$CURRENT_PORT + docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi + yes | sudo docker system prune -a + EOF diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts index 2ad4569..508bd28 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.controller.ts @@ -5,13 +5,16 @@ import { HttpCode, Param, Post, + Req, UploadedFile, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { ArticleBackgroundsService } from './articleBackgrounds.service'; import { ApiBody, ApiConsumes, + ApiCookieAuth, ApiCreatedResponse, ApiOkResponse, ApiOperation, @@ -21,6 +24,8 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ImageUploadRequestDto } from 'src/modules/images/dtos/image-upload-request.dto'; import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; import { ArticleBackgroundDto } from './dtos/common/articleBackground.dto'; +import { AuthGuardV2 } from 'src/common/guards/auth.guard'; +import { Request } from 'express'; @Controller('') export class ArticleBackgroundsController { @@ -39,14 +44,20 @@ export class ArticleBackgroundsController { description: '이미지 서버에 파일 업로드 완료', type: ImageUploadResponseDto, }) - @Post('users/admin/articles/background') + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @Post('users/admin/article/background') @UseInterceptors(FileInterceptor('file')) @HttpCode(201) async createArticleBackground( + @Req() req: Request, @UploadedFile() file: Express.Multer.File, ): Promise { - const url = - await this.articleBackgroundsService.createArticleBackground(file); + const userId = req.user.userId; + const url = await this.articleBackgroundsService.createArticleBackground( + userId, + file, + ); return url; } @@ -61,11 +72,19 @@ export class ArticleBackgroundsController { return await this.articleBackgroundsService.findArticleBackgrounds(); } + @ApiCookieAuth() @ApiTags('어드민 API') @ApiOperation({ summary: '내지 삭제하기' }) + @UseGuards(AuthGuardV2) @Delete('users/admin/articles/background/:articleBackgroundId') - async delete(@Param('articleBackgroundId') articleBackgroundId: string) { + async delete( + @Req() req: Request, + @Param('articleBackgroundId') articleBackgroundId: string, + ) { + const userId = req.user.userId; + return await this.articleBackgroundsService.deleteArticleBackground({ + userId, articleBackgroundId, }); } diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts index f8873b3..2cc97f6 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.module.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.module.ts @@ -5,9 +5,14 @@ import { ArticleBackgroundsController } from './articleBackgrounds.controller'; import { ArticleBackgroundsService } from './articleBackgrounds.service'; import { ArticleBackground } from './entities/articleBackground.entity'; import { ImagesModule } from 'src/modules/images/images.module'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([ArticleBackground]), ImagesModule], + imports: [ + UsersModule, + TypeOrmModule.forFeature([ArticleBackground]), + ImagesModule, + ], providers: [JwtStrategy, ArticleBackgroundsService], controllers: [ArticleBackgroundsController], }) diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts index 58171a1..17866d3 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -5,18 +5,22 @@ import { Repository } from 'typeorm'; import { ImagesService } from 'src/modules/images/images.service'; import { ArticleBackgroundDto } from './dtos/common/articleBackground.dto'; import { ImageUploadResponseDto } from 'src/modules/images/dtos/image-upload-response.dto'; +import { UsersValidateService } from '../users/services/users-validate-service'; @Injectable() export class ArticleBackgroundsService { constructor( + private readonly svc_images: ImagesService, + private readonly svc_usersValidate: UsersValidateService, @InjectRepository(ArticleBackground) private readonly repo_articleBackgrounds: Repository, - private readonly svc_images: ImagesService, ) {} async createArticleBackground( + userId: number, file: Express.Multer.File, ): Promise { + await this.svc_usersValidate.adminCheck({ userId }); const { imageUrl } = await this.svc_images.imageUpload({ file, resize: 2000, @@ -30,7 +34,9 @@ export class ArticleBackgroundsService { return await this.repo_articleBackgrounds.find(); } - async deleteArticleBackground({ articleBackgroundId }) { + async deleteArticleBackground({ articleBackgroundId, userId }) { + await this.svc_usersValidate.adminCheck({ userId }); + const articleBackground = await this.repo_articleBackgrounds.findOne({ where: { id: articleBackgroundId }, }); diff --git a/src/APIs/articleCategories/articleCategories.controller.ts b/src/APIs/articleCategories/articleCategories.controller.ts index 5b5dc2d..9c368b2 100644 --- a/src/APIs/articleCategories/articleCategories.controller.ts +++ b/src/APIs/articleCategories/articleCategories.controller.ts @@ -61,7 +61,7 @@ export class ArticleCategoriesController { @Get('categories/:articleCategoryId') async fetchMyCategory( @Req() req: Request, - @Param('articleCategoryId') articleCategoryId: string, + @Param('articleCategoryId') articleCategoryId: number, ): Promise { return await this.svc_articleCategories.findArticleCategoryById({ articleCategoryId, @@ -99,7 +99,7 @@ export class ArticleCategoriesController { @Patch('me/categories/:articleCategoryId') async patchArticleCategory( @Req() req: Request, - @Param('articleCategoryId') articleCategoryId: string, + @Param('articleCategoryId') articleCategoryId: number, @Body() body: ArticleCategoryPatchRequestDto, ): Promise { const userId = req.user.userId; @@ -120,7 +120,7 @@ export class ArticleCategoriesController { @UseGuards(AuthGuardV2) async deleteArticleCategory( @Req() req: Request, - @Param('articleCategoryId') articleCategoryId: string, + @Param('articleCategoryId') articleCategoryId: number, ) { const userId = req.user.userId; return await this.svc_articleCategories.deleteArticleCategory({ diff --git a/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts index 51a5d19..679d86e 100644 --- a/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts +++ b/src/APIs/articleCategories/dtos/response/articleCategories-response.dto.ts @@ -4,8 +4,8 @@ export class ArticleCategoriesResponseDto { @ApiProperty({ type: Number }) articleCount: number; - @ApiProperty({ type: String }) - categoryId: string; + @ApiProperty({ type: Number }) + categoryId: number; @ApiProperty({ type: String }) categoryName: string; diff --git a/src/APIs/articleCategories/entities/articleCategory.entity.ts b/src/APIs/articleCategories/entities/articleCategory.entity.ts index 0d0d876..7846166 100644 --- a/src/APIs/articleCategories/entities/articleCategory.entity.ts +++ b/src/APIs/articleCategories/entities/articleCategory.entity.ts @@ -15,7 +15,7 @@ import { @Entity() export class ArticleCategory extends CommonEntity { - @ApiProperty({ type: String, description: 'PK: A_I_' }) + @ApiProperty({ type: Number, description: 'PK: A_I_' }) @PrimaryGeneratedColumn() @IsNumber() id: number; diff --git a/src/APIs/articles/dtos/request/article-create-request.dto.ts b/src/APIs/articles/dtos/request/article-create-request.dto.ts index d946415..2d5daf4 100644 --- a/src/APIs/articles/dtos/request/article-create-request.dto.ts +++ b/src/APIs/articles/dtos/request/article-create-request.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, IsOptional, ValidateNested } from 'class-validator'; +import { IsArray, IsOptional, IsUrl, ValidateNested } from 'class-validator'; import { ArticleDto } from '../common/article.dto'; import { StickerBlocksCreateDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlocks-create.dto'; @@ -15,6 +15,7 @@ export class ArticleCreateRequestDto extends OmitType(ArticleDto, [ 'dateCreated', 'dateDeleted', 'dateUpdated', + 'mainImageUrl', ]) { @ApiProperty({ type: [StickerBlocksCreateDto] }) @IsArray({ message: 'stickerBlocks는 배열이여야 합니다.' }) @@ -22,4 +23,14 @@ export class ArticleCreateRequestDto extends OmitType(ArticleDto, [ @Type(() => StickerBlocksCreateDto) @IsOptional() stickerBlocks: StickerBlocksCreateDto[]; + + @ApiProperty({ + description: '게시글 대표 이미지 url', + type: String, + required: false, + nullable: true, + }) + @IsUrl() + @IsOptional() + mainImageUrl?: string | null; } diff --git a/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts b/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts index d2aabf3..97d3ee8 100644 --- a/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts +++ b/src/APIs/articles/dtos/request/articles-get-user-request.dto.ts @@ -6,10 +6,10 @@ import { ArticlesGetRequestDto } from './articles-get-request.dto'; export class ArticlesGetUserRequestDto extends ArticlesGetRequestDto { @ApiProperty({ description: '필터링할 카테고리 아이디', - type: String, + type: Number, required: false, }) @IsOptional() - @Type(() => String) - categoryId?: string | null; + @Type(() => Number) + categoryId?: number | null; } diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 01686e8..6165994 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -33,7 +33,11 @@ export class Article extends IndexedCommonEntity { @IsNumber() id: number; - @ApiProperty({ description: '연결된 카테고리 fk', type: Number }) + @ApiProperty({ + description: '연결된 카테고리 fk', + type: Number, + nullable: true, + }) @Column({ name: 'article_category_id', nullable: true }) @RelationId((article: Article) => article.articleCategory) @IsNumber() diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts index 0a51ec2..44713c8 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts @@ -34,15 +34,14 @@ export class ArticlesPaginateRepository extends Repository
{ .addSelect([ 'user.handle', 'user.id', - 'user.description', - 'user.profile_image', + 'user.profileImage', 'user.username', ]) - .where('p.is_published = true') + .where('p.isPublished = true') .andWhere(queryByOrderSort, { customCursor: cursor, }) - .andWhere('p.date_deleted IS NULL') + .andWhere('p.dateDeleted IS NULL') // .orderBy('p.commentCount', sort as 'ASC' | 'DESC') .orderBy(`p.${_order}`, sort as 'ASC' | 'DESC') .addOrderBy('p.id', 'DESC'); @@ -62,7 +61,7 @@ export class ArticlesPaginateRepository extends Repository
{ }); if (dateFilter) { - queryBuilder.andWhere('p.date_created > :dateFilter', { + queryBuilder.andWhere('p.dateCreated > :dateFilter', { dateFilter, }); } @@ -82,22 +81,22 @@ export class ArticlesPaginateRepository extends Repository
{ const mutualFollows = await this.db_dataSource .createQueryBuilder(Follow, 'f1') - .select('f1.from_user_id', 'user1') - .addSelect('f1.to_user_id', 'user2') + .select('f1.fromUserId', 'user1') + .addSelect('f1.toUserId', 'user2') .innerJoin( Follow, 'f2', - 'f1.from_user_id = f2.to_user_id AND f1.to_user_id = f2.from_user_id', + 'f1.fromUserId = f2.toUserId AND f1.toUserId = f2.fromUserId', ); queryBuilder - .innerJoin(Follow, 'f', 'p.user_id = f.to_user_id') + .innerJoin(Follow, 'f', 'p.userId = f.toUserId') .leftJoin( `(${mutualFollows.getQuery()})`, 'mf', - 'p.user_id = mf.user1 AND f.from_user_id = mf.user2', + 'p.userId = mf.user1 AND f.fromUserId = mf.user2', ) - .where('f.from_user_id = :userId', { userId }) + .where('f.fromUserId = :userId', { userId }) .andWhere( new Brackets((qb) => { qb.where('mf.user1 IS NOT NULL AND p.scope IN (:...scopes)', { @@ -109,7 +108,7 @@ export class ArticlesPaginateRepository extends Repository
{ ); if (dateFilter) { - queryBuilder.andWhere('p.date_created > :dateFilter', { + queryBuilder.andWhere('p.dateCreated > :dateFilter', { dateFilter, }); } @@ -129,18 +128,18 @@ export class ArticlesPaginateRepository extends Repository
{ const queryBuilder = this.getCursorQuery({ order, cursor, take, sort }); if (cursorOption.categoryId) { - queryBuilder.andWhere('article_category.id = :categoryId', { + queryBuilder.andWhere('articleCategory.id = :categoryId', { categoryId: cursorOption.categoryId, }); } queryBuilder - .andWhere('p.user_id = :userId', { + .andWhere('p.userId = :userId', { userId, }) .andWhere('p.scope IN (:scope)', { scope }); if (dateFilter) { - queryBuilder.andWhere('p.date_created > :date_filter', { + queryBuilder.andWhere('p.dateCreated > :date_filter', { date_filter: dateFilter, }); } diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index fc5d67f..568936f 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -21,12 +21,12 @@ export class ArticlesReadRepository extends Repository
{ 'user.handle', 'user.id', 'user.description', - 'user.profile_image', + 'user.profileImage', 'user.username', ]) .where('p.id = :articleId', { articleId }) .andWhere('p.scope IN (:scope)', { scope }) - .andWhere('p.date_deleted IS NULL') + .andWhere('p.dateDeleted IS NULL') .getOne(); } @@ -38,12 +38,11 @@ export class ArticlesReadRepository extends Repository
{ .addSelect([ 'user.handle', 'user.id', - 'user.description', - 'user.profile_image', + 'user.profileImage', 'user.username', ]) .where('p.id = :articleId', { articleId }) - .andWhere('p.date_deleted IS NULL') + .andWhere('p.dateDeleted IS NULL') .getOne(); } @@ -55,13 +54,12 @@ export class ArticlesReadRepository extends Repository
{ .addSelect([ 'user.handle', 'user.id', - 'user.description', - 'user.profile_image', + 'user.profileImage', 'user.username', ]) - .where('p.user_id = :userId', { userId }) - .andWhere(`p.is_published = false`) - .andWhere('p.date_deleted IS NULL') + .where('p.userId = :userId', { userId }) + .andWhere(`p.isPublished = false`) + .andWhere('p.dateDeleted IS NULL') .orderBy('p.id', 'DESC') .getMany(); } diff --git a/src/APIs/articles/services/articles-validate.service.ts b/src/APIs/articles/services/articles-validate.service.ts index ca5224a..f0d8bf8 100644 --- a/src/APIs/articles/services/articles-validate.service.ts +++ b/src/APIs/articles/services/articles-validate.service.ts @@ -31,20 +31,20 @@ export class ArticlesValidateService { .createQueryBuilder('pc') .where('pc.id = :id', { id: articles.articleCategoryId }) .getOne(); - if (!pc && !passNonEssentail) + if (pc == null && !passNonEssentail) throw new BadRequestException('존재하지 않는 article_category입니다.'); const pg = await this.dataSource .getRepository(ArticleBackground) .createQueryBuilder('pg') .where('pg.id = :id', { id: articles.articleBackgroundId }) .getOne(); - if (!pg && articles.articleBackgroundId && !passNonEssentail) + if (pg == null && !passNonEssentail) throw new BadRequestException('존재하지 않는 article_background입니다.'); const us = await this.dataSource .getRepository(User) .createQueryBuilder('us') .where('us.id = :id', { id: articles.userId }) .getOne(); - if (!us) throw new BadRequestException('존재하지 않는 user입니다.'); + if (us == null) throw new BadRequestException('존재하지 않는 user입니다.'); } } diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 17afd9c..8740e14 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -31,8 +31,7 @@ export class CommentsRepository extends Repository { .addSelect([ 'user.handle', 'user.id', - 'user.description', - 'user.profile_image', + 'user.profileImage', 'user.username', ]) .leftJoinAndSelect('c.article', 'article') @@ -47,26 +46,19 @@ export class CommentsRepository extends Repository { let comments = await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') - .addSelect([ - 'u.id', - 'u.username', - 'u.description', - 'u.profile_image', - 'u.handle', - ]) + .addSelect(['u.id', 'u.username', 'u.profileImage', 'u.handle']) .addSelect([ 'childrenUser.id', 'childrenUser.username', - 'childrenUser.description', - 'childrenUser.profile_image', + 'childrenUser.profileImage', 'childrenUser.handle', ]) .leftJoinAndSelect('c.children', 'children') .leftJoin('children.user', 'childrenUser') - .where('c.article_id = :articleId', { articleId }) - .andWhere('c.parent_id IS NULL') - .orderBy('c.date_created', 'ASC') - .addOrderBy('children.date_created', 'ASC') + .where('c.articleId = :articleId', { articleId }) + .andWhere('c.parentId IS NULL') + .orderBy('c.dateCreated', 'ASC') + .addOrderBy('children.dateCreated', 'ASC') .getMany(); comments = comments.filter((comment) => { diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index 658c5d0..ab58fb4 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -5,6 +5,7 @@ import { IFollowsRepositoryFindList } from './interfaces/follows.repository.inte import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; import { plainToClass } from 'class-transformer'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from '../users/dtos/response/user-primary-response.dto'; @Injectable() export class FollowsRepository extends Repository { @@ -18,8 +19,8 @@ export class FollowsRepository extends Repository { }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.fromUser', 'user') - .where('user.date_deleted IS NULL') - .andWhere('follow.to_user_id = :userId') + .where('user.dateDeleted IS NULL') + .andWhere('follow.toUserId = :userId') .leftJoinAndSelect( (subQuery) => { return subQuery @@ -31,7 +32,9 @@ export class FollowsRepository extends Repository { 'follow2.to_user_id = user.id', ) .select([ - ...getUserFields().map((column) => `user.${column} AS ${column}`), + ...USER_PRIMARY_RESPONSE_DTO_KEYS.map( + (column) => `user.${column} AS ${column}`, + ), 'CASE WHEN follow2.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId, loggedUser }) @@ -40,7 +43,6 @@ export class FollowsRepository extends Repository { return followings.map((follower) => plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(follower), - isAdmin: follower.is_admin === 1, isFollowing: follower.is_following === 1, }), ); @@ -52,8 +54,8 @@ export class FollowsRepository extends Repository { }: IFollowsRepositoryFindList): Promise { const followings = await this.createQueryBuilder('follow') .innerJoin('follow.toUser', 'user') - .where('user.date_deleted IS NULL') - .andWhere('follow.from_user_id = :userId') + .where('user.dateDeleted IS NULL') + .andWhere('follow.fromUserId = :userId') .leftJoinAndSelect( (subQuery) => { return subQuery @@ -65,7 +67,9 @@ export class FollowsRepository extends Repository { 'follow2.to_user_id = user.id', ) .select([ - ...getUserFields().map((column) => `user.${column} AS ${column}`), + ...USER_PRIMARY_RESPONSE_DTO_KEYS.map( + (column) => `user.${column} AS ${column}`, + ), 'CASE WHEN follow2.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ userId, loggedUser }) @@ -74,7 +78,6 @@ export class FollowsRepository extends Repository { return followings.map((follower) => plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(follower), - isAdmin: follower.is_admin === 1, isFollowing: follower.is_following === 1, }), ); diff --git a/src/APIs/likes/likes.repository.ts b/src/APIs/likes/likes.repository.ts index a071925..0661b04 100644 --- a/src/APIs/likes/likes.repository.ts +++ b/src/APIs/likes/likes.repository.ts @@ -4,8 +4,9 @@ import { Injectable } from '@nestjs/common'; import { ILikesRepositoryIds } from './interfaces/likes.repository.interface'; import { Like } from './entities/like.entity'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; -import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; +import { convertToCamelCase } from 'src/utils/classUtils'; import { plainToClass } from 'class-transformer'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from '../users/dtos/response/user-primary-response.dto'; @Injectable() export class LikesRepository extends Repository { @@ -29,11 +30,13 @@ export class LikesRepository extends Repository { 'follow', 'follow.to_user_id = user.id', ) - .where('user.date_deleted IS NULL') - .andWhere('article.date_deleted IS NULL') + .where('user.dateDeleted IS NULL') + .andWhere('article.dateDeleted IS NULL') .andWhere('like.articleId = :articleId') .select([ - ...getUserFields().map((column) => `user.${column} AS ${column}`), + ...USER_PRIMARY_RESPONSE_DTO_KEYS.map( + (column) => `user.${column} AS ${column}`, + ), 'CASE WHEN follow.to_user_id IS NOT NULL THEN true ELSE false END AS is_following', ]) .setParameters({ articleId, userId }) @@ -42,7 +45,6 @@ export class LikesRepository extends Repository { return users.map((user) => plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(user), - isAdmin: user.is_admin === 1, isFollowing: user.is_following === 1, }), ); diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index d888823..978373d 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -28,9 +28,9 @@ export class NotificationsRepository extends Repository { }: INotificationsServiceRead): Promise { return await this.createQueryBuilder('n') .leftJoin('n.user', 'user') - .addSelect(['user.profile_image', 'user.username', 'user.handle']) + .addSelect(['user.profileImage', 'user.username', 'user.handle']) .where('n.id = :id', { id: notificationId }) - .andWhere('n.target_user_id = :targetUserId', { + .andWhere('n.targetUserId = :targetUserId', { targetUserId, }) .getOne(); @@ -43,15 +43,15 @@ export class NotificationsRepository extends Repository { }): Promise { const query = this.createQueryBuilder('n') .leftJoin('n.user', 'user') - .addSelect(['user.profile_image', 'user.username', 'user.handle']) - .where('n.target_user_id = :userId', { + .addSelect(['user.profileImage', 'user.username', 'user.handle']) + .where('n.targetUserId = :userId', { userId, }); if (!isChecked) { - query.andWhere('n.is_checked = true'); + query.andWhere('n.isChecked = true'); } if (dateCreated) { - query.andWhere('n.date_created > :date_created', { dateCreated }); + query.andWhere('n.dateCreated > :date_created', { dateCreated }); } return await query.getMany(); diff --git a/src/APIs/reports/reports.controller.ts b/src/APIs/reports/reports.controller.ts index 5f94190..cfa3dc0 100644 --- a/src/APIs/reports/reports.controller.ts +++ b/src/APIs/reports/reports.controller.ts @@ -35,7 +35,7 @@ export class ReportsController { @UseGuards(AuthGuardV2) @Post('articles/:articleId/report') @HttpCode(201) - async reportPost( + async reportArticle( @Req() req: Request, @Body() body: ReportCreateRequestDto, @Param('articleId') targetId: number, diff --git a/src/APIs/stickerCategories/dtos/response/stickerCategories-fetch-stickers-response.dto.ts b/src/APIs/stickerCategories/dtos/response/stickerCategories-fetch-stickers-response.dto.ts new file mode 100644 index 0000000..ecb9d0d --- /dev/null +++ b/src/APIs/stickerCategories/dtos/response/stickerCategories-fetch-stickers-response.dto.ts @@ -0,0 +1,8 @@ +import { StickerDto } from 'src/APIs/stickers/dtos/common/sticker.dto'; +import { StickerCategoryMapperDto } from '../common/stickerCategoryMapper.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class StickersCategoryFetchStickersResponseDto extends StickerCategoryMapperDto { + @ApiProperty({ type: StickerDto }) + sticker: StickerDto; +} diff --git a/src/APIs/stickerCategories/stickerCategories.controller.ts b/src/APIs/stickerCategories/stickerCategories.controller.ts index 35d091b..4968545 100644 --- a/src/APIs/stickerCategories/stickerCategories.controller.ts +++ b/src/APIs/stickerCategories/stickerCategories.controller.ts @@ -20,6 +20,7 @@ import { StickerCategoryCreateRequestDto } from './dtos/request/stickerCategory- import { StickerCategoriesMapDto } from './dtos/request/stickerCategories-map-request.dto'; import { StickerCategoryMapperDto } from './dtos/common/stickerCategoryMapper.dto'; import { StickerCategoryDto } from './dtos/common/stickerCategory.dto'; +import { StickersCategoryFetchStickersResponseDto } from './dtos/response/stickerCategories-fetch-stickers-response.dto'; @ApiTags('스티커 API') @Controller() @@ -42,11 +43,11 @@ export class StickerCategoriesController { summary: '카테고리 id에 해당하는 스티커를 fetchAll', description: '카테고리를 id로 찾고, 이에 매핑된 스티커들을 가져온다', }) - @ApiOkResponse({ type: [StickerCategoryMapperDto] }) + @ApiOkResponse({ type: [StickersCategoryFetchStickersResponseDto] }) @Get('stickers/categories/:stickerCategoryId') async fetchStickersByCategoryName( @Param('stickerCategoryId') stickerCategoryId: number, - ): Promise { + ): Promise { return await this.stickerCategoriesService.fetchStickersByCategoryId({ stickerCategoryId, }); diff --git a/src/APIs/stickerCategories/stickerCategories.service.ts b/src/APIs/stickerCategories/stickerCategories.service.ts index 1b83e57..8e2bfb2 100644 --- a/src/APIs/stickerCategories/stickerCategories.service.ts +++ b/src/APIs/stickerCategories/stickerCategories.service.ts @@ -18,6 +18,7 @@ import { import { UsersValidateService } from '../users/services/users-validate-service'; import { StickerCategoryDto } from './dtos/common/stickerCategory.dto'; import { StickerCategoryMapperDto } from './dtos/common/stickerCategoryMapper.dto'; +import { StickersCategoryFetchStickersResponseDto } from './dtos/response/stickerCategories-fetch-stickers-response.dto'; @Injectable() export class StickerCategoriesService { @@ -100,10 +101,12 @@ export class StickerCategoriesService { async fetchStickersByCategoryId({ stickerCategoryId, - }: IStickerCategoriesServiceId): Promise { + }: IStickerCategoriesServiceId): Promise< + StickersCategoryFetchStickersResponseDto[] + > { await this.existCheckById({ stickerCategoryId }); return await this.repo_stickerCategoryMappers.find({ - // relations: { sticker: true, stickerCategory: true }, + relations: { sticker: true }, where: { stickerCategory: { id: stickerCategoryId }, sticker: { isDefault: true }, diff --git a/src/APIs/users/dtos/response/user-following-response.dto.ts b/src/APIs/users/dtos/response/user-following-response.dto.ts index dee5a34..a889ff6 100644 --- a/src/APIs/users/dtos/response/user-following-response.dto.ts +++ b/src/APIs/users/dtos/response/user-following-response.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserDto } from '../common/user.dto'; +import { UserPrimaryResponseDto } from './user-primary-response.dto'; -export class UserFollowingResponseDto extends UserDto { +export class UserFollowingResponseDto extends UserPrimaryResponseDto { @ApiProperty({ type: Boolean, description: '팔로잉 유무' }) isFollowing: boolean; } diff --git a/src/APIs/users/dtos/response/user-primary-response.dto.ts b/src/APIs/users/dtos/response/user-primary-response.dto.ts index 4db3071..e1ec4f6 100644 --- a/src/APIs/users/dtos/response/user-primary-response.dto.ts +++ b/src/APIs/users/dtos/response/user-primary-response.dto.ts @@ -1,23 +1,18 @@ import { PickType } from '@nestjs/swagger'; import { User } from '../../entities/user.entity'; - -export class UserPrimaryResponseDto extends PickType(User, [ +export const USER_PRIMARY_RESPONSE_DTO_KEYS: (keyof User)[] = [ 'id', 'username', 'profileImage', 'handle', -]) {} +]; +export class UserPrimaryResponseDto extends PickType( + User, + USER_PRIMARY_RESPONSE_DTO_KEYS, +) {} -export const USER_PRIMARY_SELECT_OPTION: { [key: string]: boolean } = { - id: true, - username: true, - profileImage: true, - handle: true, -}; -// getEntityFields(User).reduce( -// (options, field) => { -// options[field] = true; -// return options; -// }, -// {} as { [key: string]: boolean }, -// ); +export const USER_PRIMARY_SELECT_OPTION: { + [key in (typeof USER_PRIMARY_RESPONSE_DTO_KEYS)[number]]: boolean; +} = Object.fromEntries( + USER_PRIMARY_RESPONSE_DTO_KEYS.map((key) => [key, true]), +) as { [key in (typeof USER_PRIMARY_RESPONSE_DTO_KEYS)[number]]: boolean }; diff --git a/src/APIs/users/users.repository.ts b/src/APIs/users/users.repository.ts index c4f77f0..f488107 100644 --- a/src/APIs/users/users.repository.ts +++ b/src/APIs/users/users.repository.ts @@ -3,8 +3,9 @@ import { User } from './entities/user.entity'; import { DataSource, Repository } from 'typeorm'; import { Follow } from '../follows/entities/follow.entity'; import { plainToClass } from 'class-transformer'; -import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; +import { convertToCamelCase } from 'src/utils/classUtils'; import { UserFollowingResponseDto } from './dtos/response/user-following-response.dto'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from './dtos/response/user-primary-response.dto'; @Injectable() export class UsersRepository extends Repository { @@ -24,9 +25,9 @@ export class UsersRepository extends Repository { 'follow', 'follow.to_user_id = user.id', ) - .where('user.date_deleted IS NULL') + .where('user.dateDeleted IS NULL') .select([ - ...getUserFields().map((column) => { + ...USER_PRIMARY_RESPONSE_DTO_KEYS.map((column) => { console.log(column); return `user.${column} AS ${column}`; }), @@ -54,7 +55,6 @@ export class UsersRepository extends Repository { return users.map((user) => plainToClass(UserFollowingResponseDto, { ...convertToCamelCase(user), - isAdmin: user.is_admin === 1, isFollowing: user.is_following === 1, }), ); From 248487470521908ee83b11ff612fd139033d182b Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Jul 2024 15:48:35 +0900 Subject: [PATCH 229/236] fix(workflow): change CI code --- .github/workflows/deploy-to-staging.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-to-staging.yml b/.github/workflows/deploy-to-staging.yml index dc45b88..0ce793d 100644 --- a/.github/workflows/deploy-to-staging.yml +++ b/.github/workflows/deploy-to-staging.yml @@ -27,8 +27,8 @@ jobs: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-access-key-id: ${{ secrets.AWS_STAGING_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_STAGING_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 - name: Login to ECR @@ -43,9 +43,13 @@ jobs: - name: Push Docker image to ECR run: docker push 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + - name: Create PEM file from secret + run: echo "${{ secrets.BLCCU_DEV_RSA_PEM }}" > blccu-dev-rsa.pem + - name: Deploy to server run: | - ssh -o StrictHostKeyChecking=no -i ./keys/blccu-dev-rsa.pem ubuntu@api.blccu.com << 'EOF' + chmod 400 blccu-dev-rsa.pem + ssh -o StrictHostKeyChecking=no -i blccu-dev-rsa.pem ubuntu@api.blccu.com << 'EOF' set -e NEW_PORT=3001 CURRENT_PORT=$(grep 'server localhost:' /etc/nginx/nginx.conf | awk '{print $2}' | cut -d ':' -f 2 | sed 's/;//') @@ -71,3 +75,6 @@ jobs: docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi yes | sudo docker system prune -a EOF + + - name: Remove PEM file + run: rm blccu-dev-rsa.pem From 4072f9454da9df08123a857440a82ca006faa254 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 10 Jul 2024 16:07:54 +0900 Subject: [PATCH 230/236] fix(workflow): add secret variables --- .github/workflows/deploy-to-staging.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy-to-staging.yml b/.github/workflows/deploy-to-staging.yml index 0ce793d..6f7270b 100644 --- a/.github/workflows/deploy-to-staging.yml +++ b/.github/workflows/deploy-to-staging.yml @@ -29,35 +29,35 @@ jobs: with: aws-access-key-id: ${{ secrets.AWS_STAGING_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_STAGING_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 + aws-region: ${{ secrets.AWS_REGION }} - name: Login to ECR - run: aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com + run: aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.AWS_STAGING_ECR_URL }} - name: Build Docker image run: docker buildx build --platform linux/amd64 -t blccu-ecr . --load - name: Tag Docker image - run: docker tag blccu-ecr:latest 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + run: docker tag blccu-ecr:latest ${{ secrets.AWS_STAGING_ECR_URL }}:staging-latest - name: Push Docker image to ECR - run: docker push 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + run: docker push ${{ secrets.AWS_STAGING_ECR_URL }}:staging-latest - name: Create PEM file from secret - run: echo "${{ secrets.BLCCU_DEV_RSA_PEM }}" > blccu-dev-rsa.pem + run: echo "${{ secrets.BLCCU_STAGING_RSA_PEM }}" > blccu-staging-rsa.pem - name: Deploy to server run: | - chmod 400 blccu-dev-rsa.pem - ssh -o StrictHostKeyChecking=no -i blccu-dev-rsa.pem ubuntu@api.blccu.com << 'EOF' + chmod 400 blccu-staging-rsa.pem + ssh -o StrictHostKeyChecking=no -i blccu-staging-rsa.pem ubuntu@${{ secrets.BLCCU_STAGING_HOST }} << 'EOF' set -e NEW_PORT=3001 CURRENT_PORT=$(grep 'server localhost:' /etc/nginx/nginx.conf | awk '{print $2}' | cut -d ':' -f 2 | sed 's/;//') if [ "$CURRENT_PORT" = "3001" ]; then NEW_PORT=3000 fi - docker pull 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest - docker run --env-file .env.prod -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name blccu-ecr-$NEW_PORT -e TZ=Asia/Seoul 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com/blccu-ecr:latest + docker pull ${{ secrets.AWS_STAGING_ECR_URL }}:staging-latest + docker run --env-file .env.staging -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name blccu-ecr-$NEW_PORT -e TZ=Asia/Seoul ${{ secrets.AWS_STAGING_ECR_URL }}:staging-latest for i in {1..20}; do HEALTH_CHECK=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:$NEW_PORT/health) if [ "$HEALTH_CHECK" -eq 200 ]; then @@ -72,9 +72,9 @@ jobs: sudo sed -i "s/server localhost:$CURRENT_PORT;/server localhost:$NEW_PORT;/g" /etc/nginx/nginx.conf sudo systemctl restart nginx docker stop blccu-ecr-$CURRENT_PORT && docker rm blccu-ecr-$CURRENT_PORT - docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':latest' | awk '{print $1}' | xargs -r docker rmi + docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v ':staging-latest' | awk '{print $1}' | xargs -r docker rmi yes | sudo docker system prune -a EOF - name: Remove PEM file - run: rm blccu-dev-rsa.pem + run: rm blccu-staging-rsa.pem From a1828997b0d3b203dc6a6e96d92c6fc0b8e9ade5 Mon Sep 17 00:00:00 2001 From: do-huni Date: Thu, 11 Jul 2024 00:56:59 +0900 Subject: [PATCH 231/236] fix(userPrimaryResDto): add description column --- deploy/deploy.sh | 2 +- src/APIs/articles/entities/article.entity.ts | 2 +- .../articles-paginate.repository.ts | 14 ++++--- .../repositories/articles-read.repository.ts | 39 ++++++++++--------- .../services/articles-validate.service.ts | 18 +++++---- src/APIs/comments/comments.repository.ts | 33 +++++++++------- src/APIs/follows/follows.repository.ts | 2 +- .../notifications-get-response.dto.ts | 3 +- .../notifications/notifications.repository.ts | 16 +++++++- .../response/user-primary-response.dto.ts | 1 + src/modules/aws/aws.service.ts | 4 +- src/utils/classUtils.ts | 11 ++++++ 12 files changed, 92 insertions(+), 53 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 4d3ab63..24d8104 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -56,7 +56,7 @@ OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT echo -e "\n## new docker pull & run ##\n" ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" -ssh -i $PEM_PATH $SERVER "docker run --env-file .env.prod -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" +ssh -i $PEM_PATH $SERVER "docker run --env-file .env.staging -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # memory랑 cpu 사용량 조절 # 헬스체크 수행 diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index 6165994..d13459a 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -44,7 +44,7 @@ export class Article extends IndexedCommonEntity { @IsOptional() articleCategoryId: number; - @ApiProperty({ description: '연결된 내지 fk', type: Number }) + @ApiProperty({ description: '연결된 내지 fk', type: Number, nullable: true }) @Column({ name: 'article_background_id', nullable: true }) @RelationId((article: Article) => article.articleBackground) @IsNumber() diff --git a/src/APIs/articles/repositories/articles-paginate.repository.ts b/src/APIs/articles/repositories/articles-paginate.repository.ts index 44713c8..0bec2e1 100644 --- a/src/APIs/articles/repositories/articles-paginate.repository.ts +++ b/src/APIs/articles/repositories/articles-paginate.repository.ts @@ -11,6 +11,8 @@ import { } from '../interfaces/articles.repository.interface'; import { Follow } from 'src/APIs/follows/entities/follow.entity'; import { Injectable } from '@nestjs/common'; +import { transformKeysToArgsFormat } from 'src/utils/classUtils'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from 'src/APIs/users/dtos/response/user-primary-response.dto'; @Injectable() export class ArticlesPaginateRepository extends Repository
{ @@ -31,12 +33,12 @@ export class ArticlesPaginateRepository extends Repository
{ .innerJoin('p.user', 'user') // .leftJoinAndSelect('p.articleBackground', 'article_background') // .leftJoinAndSelect('p.articleCategory', 'article_category') - .addSelect([ - 'user.handle', - 'user.id', - 'user.profileImage', - 'user.username', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('p.isPublished = true') .andWhere(queryByOrderSort, { customCursor: cursor, diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index 568936f..69fccba 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -3,6 +3,8 @@ import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; import { ArticleDto } from '../dtos/common/article.dto'; +import { transformKeysToArgsFormat } from 'src/utils/classUtils'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from 'src/APIs/users/dtos/response/user-primary-response.dto'; @Injectable() export class ArticlesReadRepository extends Repository
{ @@ -17,13 +19,12 @@ export class ArticlesReadRepository extends Repository
{ .innerJoin('p.user', 'user') .leftJoinAndSelect('p.articleBackground', 'article_background') .leftJoinAndSelect('p.articleCategory', 'article_category') - .addSelect([ - 'user.handle', - 'user.id', - 'user.description', - 'user.profileImage', - 'user.username', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('p.id = :articleId', { articleId }) .andWhere('p.scope IN (:scope)', { scope }) .andWhere('p.dateDeleted IS NULL') @@ -35,12 +36,12 @@ export class ArticlesReadRepository extends Repository
{ .innerJoin('p.user', 'user') .leftJoinAndSelect('p.articleBackground', 'article_background') .leftJoinAndSelect('p.articleCategory', 'article_category') - .addSelect([ - 'user.handle', - 'user.id', - 'user.profileImage', - 'user.username', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('p.id = :articleId', { articleId }) .andWhere('p.dateDeleted IS NULL') .getOne(); @@ -51,12 +52,12 @@ export class ArticlesReadRepository extends Repository
{ .innerJoin('p.user', 'user') .leftJoinAndSelect('p.articleBackground', 'article_background') .leftJoinAndSelect('p.articleCategory', 'article_category') - .addSelect([ - 'user.handle', - 'user.id', - 'user.profileImage', - 'user.username', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('p.userId = :userId', { userId }) .andWhere(`p.isPublished = false`) .andWhere('p.dateDeleted IS NULL') diff --git a/src/APIs/articles/services/articles-validate.service.ts b/src/APIs/articles/services/articles-validate.service.ts index f0d8bf8..0d88407 100644 --- a/src/APIs/articles/services/articles-validate.service.ts +++ b/src/APIs/articles/services/articles-validate.service.ts @@ -33,13 +33,17 @@ export class ArticlesValidateService { .getOne(); if (pc == null && !passNonEssentail) throw new BadRequestException('존재하지 않는 article_category입니다.'); - const pg = await this.dataSource - .getRepository(ArticleBackground) - .createQueryBuilder('pg') - .where('pg.id = :id', { id: articles.articleBackgroundId }) - .getOne(); - if (pg == null && !passNonEssentail) - throw new BadRequestException('존재하지 않는 article_background입니다.'); + if (articles.articleBackgroundId != null) { + const pg = await this.dataSource + .getRepository(ArticleBackground) + .createQueryBuilder('pg') + .where('pg.id = :id', { id: articles.articleBackgroundId }) + .getOne(); + if (pg == null && !passNonEssentail) + throw new BadRequestException( + '존재하지 않는 article_background입니다.', + ); + } const us = await this.dataSource .getRepository(User) .createQueryBuilder('us') diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index 8740e14..c210c7a 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -7,6 +7,8 @@ import { ICommentsRepositoryfetchComments, } from './interfaces/comments.repository.interface'; import { CommentsGetResponseDto } from './dtos/response/comments-get-response.dto'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from '../users/dtos/response/user-primary-response.dto'; +import { transformKeysToArgsFormat } from 'src/utils/classUtils'; @Injectable() export class CommentsRepository extends Repository { @@ -28,12 +30,12 @@ export class CommentsRepository extends Repository { }: ICommentsRepositoryId): Promise { return await this.createQueryBuilder('c') .leftJoin('c.user', 'user') - .addSelect([ - 'user.handle', - 'user.id', - 'user.profileImage', - 'user.username', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .leftJoinAndSelect('c.article', 'article') .leftJoinAndSelect('c.parent', 'parent') .where('c.id = :commentId', { commentId }) @@ -46,13 +48,18 @@ export class CommentsRepository extends Repository { let comments = await this.createQueryBuilder('c') .withDeleted() .innerJoin('c.user', 'u') - .addSelect(['u.id', 'u.username', 'u.profileImage', 'u.handle']) - .addSelect([ - 'childrenUser.id', - 'childrenUser.username', - 'childrenUser.profileImage', - 'childrenUser.handle', - ]) + .addSelect( + transformKeysToArgsFormat({ + args: 'u', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) + .addSelect( + transformKeysToArgsFormat({ + args: 'childrenUser', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .leftJoinAndSelect('c.children', 'children') .leftJoin('children.user', 'childrenUser') .where('c.articleId = :articleId', { articleId }) diff --git a/src/APIs/follows/follows.repository.ts b/src/APIs/follows/follows.repository.ts index ab58fb4..6a5a7eb 100644 --- a/src/APIs/follows/follows.repository.ts +++ b/src/APIs/follows/follows.repository.ts @@ -3,7 +3,7 @@ import { Follow } from './entities/follow.entity'; import { Injectable } from '@nestjs/common'; import { IFollowsRepositoryFindList } from './interfaces/follows.repository.interface'; import { UserFollowingResponseDto } from '../users/dtos/response/user-following-response.dto'; -import { convertToCamelCase, getUserFields } from 'src/utils/classUtils'; +import { convertToCamelCase } from 'src/utils/classUtils'; import { plainToClass } from 'class-transformer'; import { USER_PRIMARY_RESPONSE_DTO_KEYS } from '../users/dtos/response/user-primary-response.dto'; diff --git a/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts b/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts index 8c5c55c..1c8e337 100644 --- a/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts +++ b/src/APIs/notifications/dtos/response/notifications-get-response.dto.ts @@ -1,10 +1,11 @@ import { ApiProperty, PickType } from '@nestjs/swagger'; import { NotificationDto } from '../common/notification.dto'; import { UserDto } from 'src/APIs/users/dtos/common/user.dto'; +import { UserPrimaryResponseDto } from 'src/APIs/users/dtos/response/user-primary-response.dto'; export class NotificationsGetResponseDto extends NotificationDto { @ApiProperty({ type: PickType(UserDto, ['username', 'profileImage', 'handle']), }) - user: Pick; + user: UserPrimaryResponseDto; } diff --git a/src/APIs/notifications/notifications.repository.ts b/src/APIs/notifications/notifications.repository.ts index 978373d..4602eb9 100644 --- a/src/APIs/notifications/notifications.repository.ts +++ b/src/APIs/notifications/notifications.repository.ts @@ -7,6 +7,8 @@ import { INotificationsSeviceEmitNotification, } from './interfaces/notifications.service.interface'; import { NotificationsGetResponseDto } from './dtos/response/notifications-get-response.dto'; +import { transformKeysToArgsFormat } from 'src/utils/classUtils'; +import { USER_PRIMARY_RESPONSE_DTO_KEYS } from '../users/dtos/response/user-primary-response.dto'; @Injectable() export class NotificationsRepository extends Repository { @@ -28,7 +30,12 @@ export class NotificationsRepository extends Repository { }: INotificationsServiceRead): Promise { return await this.createQueryBuilder('n') .leftJoin('n.user', 'user') - .addSelect(['user.profileImage', 'user.username', 'user.handle']) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('n.id = :id', { id: notificationId }) .andWhere('n.targetUserId = :targetUserId', { targetUserId, @@ -43,7 +50,12 @@ export class NotificationsRepository extends Repository { }): Promise { const query = this.createQueryBuilder('n') .leftJoin('n.user', 'user') - .addSelect(['user.profileImage', 'user.username', 'user.handle']) + .addSelect( + transformKeysToArgsFormat({ + args: 'user', + keys: USER_PRIMARY_RESPONSE_DTO_KEYS, + }), + ) .where('n.targetUserId = :userId', { userId, }); diff --git a/src/APIs/users/dtos/response/user-primary-response.dto.ts b/src/APIs/users/dtos/response/user-primary-response.dto.ts index e1ec4f6..9381e44 100644 --- a/src/APIs/users/dtos/response/user-primary-response.dto.ts +++ b/src/APIs/users/dtos/response/user-primary-response.dto.ts @@ -5,6 +5,7 @@ export const USER_PRIMARY_RESPONSE_DTO_KEYS: (keyof User)[] = [ 'username', 'profileImage', 'handle', + 'description', ]; export class UserPrimaryResponseDto extends PickType( User, diff --git a/src/modules/aws/aws.service.ts b/src/modules/aws/aws.service.ts index 135a533..d31277f 100644 --- a/src/modules/aws/aws.service.ts +++ b/src/modules/aws/aws.service.ts @@ -18,8 +18,8 @@ export class AwsService { this.s3Client = new S3Client({ region: this.configService.get('AWS_REGION'), // AWS Region credentials: { - accessKeyId: this.configService.get('AWS_S3_ACCESS_KEY'), // Access Key - secretAccessKey: this.configService.get('AWS_S3_SECRET_ACCESS_KEY'), // Secret Key + accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'), + secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'), }, }); } diff --git a/src/utils/classUtils.ts b/src/utils/classUtils.ts index fcddaaf..2e60d72 100644 --- a/src/utils/classUtils.ts +++ b/src/utils/classUtils.ts @@ -21,6 +21,17 @@ export function toCamelCase(snakeCase: string): string { return snakeCase.replace(/_([a-z])/g, (group) => group[1].toUpperCase()); } +interface ITransformKeysToArgsFormat { + keys: string[]; + args: string; +} +export function transformKeysToArgsFormat({ + keys, + args, +}: ITransformKeysToArgsFormat): string[] { + return keys.map((key) => `${args}.${key}`); +} + export function convertToCamelCase(obj: any): any { if (Array.isArray(obj)) { return obj.map((v) => convertToCamelCase(v)); From 3b2248fe33a27e7926c50d1c128ae1ffd486d923 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Jul 2024 15:33:29 +0900 Subject: [PATCH 232/236] feat(prod): connect to production server --- .gitignore | 1 + deploy/deploy-prod.sh | 105 ++++++++++++++++++ deploy/{deploy.sh => deploy-staging.sh} | 7 +- deploy/nginx.conf | 12 +- deploy/nginx.prod.conf | 44 ++++++++ package.json | 6 +- .../article-detail-for-update-response.dto.ts | 8 +- ...stickerBlocks-with-sticker-response.dto.ts | 8 ++ .../stickerBlocks/stickerBlocks.service.ts | 16 ++- src/APIs/stickers/stickers.service.ts | 14 ++- 10 files changed, 199 insertions(+), 22 deletions(-) create mode 100755 deploy/deploy-prod.sh rename deploy/{deploy.sh => deploy-staging.sh} (92%) create mode 100644 deploy/nginx.prod.conf create mode 100644 src/APIs/stickerBlocks/dtos/response/stickerBlocks-with-sticker-response.dto.ts diff --git a/.gitignore b/.gitignore index 8149eaa..adfeef3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ lerna-debug.log* .env .env.* .env.prod +.env.staging .env.development.local .env.test.local .env.production.local diff --git a/deploy/deploy-prod.sh b/deploy/deploy-prod.sh new file mode 100755 index 0000000..9343f99 --- /dev/null +++ b/deploy/deploy-prod.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# 스크립트의 실제 위치를 기준으로 경로 설정 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR=$(cd "$SCRIPT_DIR/.."; pwd) +PEM_PATH="$SCRIPT_DIR/../../../keys/blccu-prod.pem" + +# PEM 파일 경로가 올바른지 확인 +if [[ ! -f "$PEM_PATH" ]]; then + echo "PEM 파일이 존재하지 않습니다: $PEM_PATH" + exit 1 +fi + +# PEM 파일 권한 확인 및 수정 +chmod 400 "$PEM_PATH" + +HOSTS=("13.209.215.21" "3.35.68.4") +ACCOUNT=ubuntu +SERVICE_NAME=blccu +DOCKER_TAG=latest +ECR_URL="792939917746.dkr.ecr.ap-northeast-2.amazonaws.com" +AWS_PROFILE=production # 배포 프로파일 사용 +ENV_FILE="$ROOT_DIR/.env.prod" + +NGINX_CONFIG=/etc/nginx/nginx.conf +BLUE_PORT="3000" +GREEN_PORT="3001" + +# docker push (aws ecr) +echo -e "\n## Docker build & push ##\n" + +npm run build +aws ecr get-login-password --region ap-northeast-2 --profile $AWS_PROFILE | docker login --username AWS --password-stdin $ECR_URL +docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load +docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG +docker push $ECR_URL/$SERVICE_NAME:$DOCKER_TAG + +for HOST in "${HOSTS[@]}"; do + SERVER=$ACCOUNT@$HOST + + # .env.prod 파일 전송 + echo -e "\n## .env.prod 파일 전송 to $HOST ##\n" + ssh -i $PEM_PATH $SERVER "mkdir -p /home/ubuntu/upload" + ssh -i $PEM_PATH $SERVER "chmod 700 /home/ubuntu/upload" + scp -i "$PEM_PATH" "$ENV_FILE" $SERVER:/home/$ACCOUNT/upload/.env.prod + + # 현재 설정에서 활성 포트 찾기 + CURRENT_PORT=$(ssh -i "$PEM_PATH" -o StrictHostKeyChecking=no $SERVER "grep 'server localhost:' $NGINX_CONFIG | awk '{print \$2}' | cut -d ':' -f 2 | sed 's/;//'") + echo -e "\nOld = $CURRENT_PORT on $HOST\n" + + # 포트 변경 + if [ "$CURRENT_PORT" = "$BLUE_PORT" ]; then + NEW_PORT=$GREEN_PORT + elif [ "$CURRENT_PORT" = "$GREEN_PORT" ]; then + NEW_PORT=$BLUE_PORT + else + echo -e "\n 서버의 blue green 포트 확인 실패 on $HOST \n" + exit 1 + fi + + echo -e "\nNew = $NEW_PORT on $HOST\n" + + NEW_SERVICE_NAME=$SERVICE_NAME-$NEW_PORT + OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT + + # docker pull & run + echo -e "\n## new docker pull & run on $HOST ##\n" + ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URL" + ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" + ssh -i $PEM_PATH $SERVER "docker run --env-file /home/$ACCOUNT/upload/.env.prod -d -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" + + # 헬스체크 수행 + echo -e "\n## 헬스체크 수행 on $HOST ##\n" + for i in {1..20}; do + HEALTH_CHECK=$(ssh -i $PEM_PATH $SERVER "curl -s -o /dev/null -w '%{http_code}' http://localhost:$NEW_PORT/health") + echo "http://localhost:$NEW_PORT/health" + echo "HTTP Status Code: $HEALTH_CHECK" + if [ "$HEALTH_CHECK" -eq 200 ]; then + echo -e "\n 헬스체크 성공 on $HOST \n" + break + fi + echo -e "\n 헬스체크 시도 $i/20 실패. 5초 후 재시도 on $HOST... \n" + sleep 5 + done + + if [ "$HEALTH_CHECK" -ne 200 ]; then + echo -e "\n 헬스체크 실패. 배포 중단 on $HOST \n" + ssh -i $PEM_PATH $SERVER "docker stop $NEW_SERVICE_NAME && docker rm $NEW_SERVICE_NAME" + exit 1 + fi + + # NGINX 설정 파일 수정 + echo -e "\n## Nginx 설정 수정 & restart on $HOST ##\n" + ssh -i $PEM_PATH $SERVER "sudo sed -i 's/server localhost:$CURRENT_PORT;/server localhost:$NEW_PORT;/g' $NGINX_CONFIG" + ssh -i $PEM_PATH $SERVER "sudo systemctl restart nginx" + + # old docker 제거 + echo -e "\n## old docker 제거 on $HOST ##\n" + ssh -i $PEM_PATH $SERVER "sudo docker stop $OLD_SERVICE_NAME" + ssh -i $PEM_PATH $SERVER "sudo docker rm $OLD_SERVICE_NAME" + echo -e "$ECR_URL/$SERVICE_NAME:$DOCKER_TAG" + ssh -i $PEM_PATH $SERVER "docker images --format \"{{.ID}} {{.Repository}}:{{.Tag}}\" | grep -v ':latest' | awk '{print \$1}' | xargs -r docker rmi" + ssh -i $PEM_PATH $SERVER "y | sudo docker system prune -a" + + echo -e "\n## 배포 완료 on $HOST. $NEW_SERVICE_NAME ##\n" +done diff --git a/deploy/deploy.sh b/deploy/deploy-staging.sh similarity index 92% rename from deploy/deploy.sh rename to deploy/deploy-staging.sh index 24d8104..a57f6bd 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy-staging.sh @@ -12,12 +12,13 @@ fi # PEM 파일 권한 확인 및 수정 chmod 400 "$PEM_PATH" -HOST="api.blccu.com" +HOST="staging.api.blccu.com" ACCOUNT=ubuntu SERVICE_NAME=blccu-ecr DOCKER_TAG=latest ECR_URL="637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" SERVER=$ACCOUNT@$HOST +AWS_PROFILE=staging # 배포 프로파일 사용 NGINX_CONFIG=/etc/nginx/nginx.conf BLUE_PORT="3000" @@ -28,7 +29,7 @@ echo -e "\n## Docker build & push ##\n" npm run build -aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com +aws ecr get-login-password --region ap-northeast-2 --profile $AWS_PROFILE | docker login --username AWS --password-stdin $ECR_URL docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG docker push $ECR_URL/$SERVICE_NAME:$DOCKER_TAG @@ -54,7 +55,7 @@ OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT # docker pull & run echo -e "\n## new docker pull & run ##\n" -ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" +ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URL" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" ssh -i $PEM_PATH $SERVER "docker run --env-file .env.staging -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # memory랑 cpu 사용량 조절 diff --git a/deploy/nginx.conf b/deploy/nginx.conf index fede971..d6317ec 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -18,7 +18,7 @@ http { } server { - server_name api.blccu.com; + server_name staging.api.blccu.com; location / { proxy_pass http://blccu-backend; root html; @@ -51,21 +51,23 @@ http { listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/api.blccu.com/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/api.blccu.com/privkey.pem; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/staging.api.blccu.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/staging.api.blccu.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { - if ($host = api.blccu.com) { + if ($host = staging.api.blccu.com) { return 301 https://$host$request_uri; } # managed by Certbot listen 80 default_server; listen [::]:80 default_server; - server_name api.blccu.com; + server_name staging.api.blccu.com; return 404; # managed by Certbot + } +} diff --git a/deploy/nginx.prod.conf b/deploy/nginx.prod.conf new file mode 100644 index 0000000..da530a3 --- /dev/null +++ b/deploy/nginx.prod.conf @@ -0,0 +1,44 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + client_max_body_size 10M; + upstream blccu-backend { + server localhost:3000; + } + + server { + server_name api.blccu.com; + location / { + proxy_pass http://blccu-backend; + root html; + index index.html index.htm; + } + + location /notifications/subscribe { + proxy_pass http://blccu-backend; + proxy_read_timeout 3600s; # 1시간으로 설정 + proxy_send_timeout 3600s; # 1시간으로 설정 + proxy_connect_timeout 3600s; # 1시간으로 설정 + } + + + location /metrics { + proxy_pass http://localhost:9100/metrics; # Node Exporter의 주소 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + listen 80 default_server; + listen [::]:80 default_server; + } +} diff --git a/package.json b/package.json index 50e16ab..49377a6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "private": true, "license": "UNLICENSED", "scripts": { + "ssh:staging": "ssh -i ../../keys/blccu-dev-rsa.pem ubuntu@staging.api.blccu.com;", + "ssh:prod1": "ssh -i ../../keys/blccu-prod.pem ubuntu@13.209.215.21", + "ssh:prod2": "ssh -i ../../keys/blccu-prod.pem ubuntu@3.35.68.4", "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", @@ -13,7 +16,8 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "deploy": "./deploy/deploy.sh", + "deploy:staging": "./deploy/deploy-staging.sh", + "deploy:prod": "./deploy/deploy-prod.sh", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts index 0ab1f80..bd1d516 100644 --- a/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts +++ b/src/APIs/articles/dtos/response/article-detail-for-update-response.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { ArticleDetailResponseDto } from './article-detail-response.dto'; -import { StickerBlockDto } from 'src/APIs/stickerBlocks/dtos/common/stickerBlock.dto'; +import { StickerBlocksWithStickerResponseDto } from 'src/APIs/stickerBlocks/dtos/response/stickerBlocks-with-sticker-response.dto'; export class ArticleDetailForUpdateResponseDto { @ApiProperty({ type: ArticleDetailResponseDto }) - article; + article: ArticleDetailResponseDto; - @ApiProperty({ type: [StickerBlockDto] }) - stickerBlocks; + @ApiProperty({ type: [StickerBlocksWithStickerResponseDto] }) + stickerBlocks: StickerBlocksWithStickerResponseDto[]; } diff --git a/src/APIs/stickerBlocks/dtos/response/stickerBlocks-with-sticker-response.dto.ts b/src/APIs/stickerBlocks/dtos/response/stickerBlocks-with-sticker-response.dto.ts new file mode 100644 index 0000000..aa7045c --- /dev/null +++ b/src/APIs/stickerBlocks/dtos/response/stickerBlocks-with-sticker-response.dto.ts @@ -0,0 +1,8 @@ +import { StickerDto } from 'src/APIs/stickers/dtos/common/sticker.dto'; +import { StickerBlockDto } from '../common/stickerBlock.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class StickerBlocksWithStickerResponseDto extends StickerBlockDto { + @ApiProperty({ type: StickerDto }) + sticker: StickerDto; +} diff --git a/src/APIs/stickerBlocks/stickerBlocks.service.ts b/src/APIs/stickerBlocks/stickerBlocks.service.ts index 634451b..b70fbaf 100644 --- a/src/APIs/stickerBlocks/stickerBlocks.service.ts +++ b/src/APIs/stickerBlocks/stickerBlocks.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { StickerBlock } from './entities/stickerblock.entity'; import { Repository } from 'typeorm'; @@ -10,6 +10,7 @@ import { IStikcerBlocksServiceFetchBlocks, } from './interfaces/stickerBlocks.service.interface'; import { StickerBlockDto } from './dtos/common/stickerBlock.dto'; +import { StickerBlocksWithStickerResponseDto } from './dtos/response/stickerBlocks-with-sticker-response.dto'; @Injectable() export class StickerBlocksService { @@ -25,9 +26,9 @@ export class StickerBlocksService { ...rest }: IStickerBlocksServiceCreateStickerBlock): Promise { try { - await this.svc_stickers.existCheck({ - stickerId: stickerId, - }); + // await this.svc_stickers.existCheck({ + // stickerId: stickerId, + // }); const data = await this.repo_stickerBlocks.save({ ...rest, @@ -36,7 +37,7 @@ export class StickerBlocksService { }); return data; } catch (e) { - throw new NotFoundException('게시글을 찾을 수 없습니다.'); + throw e; } } @@ -58,8 +59,11 @@ export class StickerBlocksService { async findStickerBlocks({ articleId, - }: IStikcerBlocksServiceFetchBlocks): Promise { + }: IStikcerBlocksServiceFetchBlocks): Promise< + StickerBlocksWithStickerResponseDto[] + > { return await this.repo_stickerBlocks.find({ + relations: ['sticker'], where: { articleId }, }); } diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 7d733ef..5034cc5 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -28,9 +28,17 @@ export class StickersService { return await this.repo_stickers.findOne({ where: { id: stickerId } }); } - async existCheck({ stickerId }: IStickersServiceId): Promise { - const data = await this.findStickerById({ stickerId }); - if (!data) throw new NotFoundException('스티커를 찾을 수 없습니다.'); + async existCheck({ stickerId }: IStickersServiceId): Promise { + try { + const data = await this.findStickerById({ stickerId }); + if (!data) { + throw new NotFoundException('스티커를 찾을 수 없습니다.'); + } + + return data; + } catch (e) { + throw e; + } } async createPrivateSticker({ From 6cd362c90a7b4c5d6b0e5e4a19e7accbea57e678 Mon Sep 17 00:00:00 2001 From: do-huni Date: Fri, 12 Jul 2024 16:09:21 +0900 Subject: [PATCH 233/236] feat(comments): add find own comments api --- .github/workflows/deploy-to-master.yml | 0 src/APIs/comments/comments.controller.ts | 28 +++++++++++++++---- src/APIs/comments/comments.repository.ts | 2 +- src/APIs/comments/comments.service.ts | 13 ++++++++- .../interfaces/comments.service.interface.ts | 4 +++ 5 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/deploy-to-master.yml diff --git a/.github/workflows/deploy-to-master.yml b/.github/workflows/deploy-to-master.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/APIs/comments/comments.controller.ts b/src/APIs/comments/comments.controller.ts index 4b58f7f..fb16f94 100644 --- a/src/APIs/comments/comments.controller.ts +++ b/src/APIs/comments/comments.controller.ts @@ -26,18 +26,18 @@ import { CommentsGetResponseDto } from './dtos/response/comments-get-response.dt import { CommentDto } from './dtos/common/comment.dto'; import { CommentPatchRequestDto } from './dtos/request/comment-patch-request.dto'; -@ApiTags('게시글 API') -@Controller('articles/:articleId/comments') +@Controller() export class CommentsController { constructor(private readonly svc_comments: CommentsService) {} + @ApiTags('게시글 API') @ApiOperation({ summary: '댓글을 작성한다.', description: '댓글을 작성한다.', }) @ApiOkResponse({ type: CommentChildrenDto }) @ApiCookieAuth() - @Post() + @Post('articles/:articleId/comments') @UseGuards(AuthGuardV2) @HttpCode(200) async createComment( @@ -53,22 +53,37 @@ export class CommentsController { }); } + @ApiTags('게시글 API') @ApiOperation({ summary: '특정 게시글에 대한 댓글 조회', }) @ApiOkResponse({ type: [CommentsGetResponseDto] }) - @Get() + @Get('articles/:articleId/comments') async fetchComments( @Param('articleId') articleId: number, ): Promise { return await this.svc_comments.fetchComments({ articleId }); } + @ApiTags('유저 API') + @ApiOperation({ + summary: '자신의 최근 댓글 10개 조회', + }) + @UseGuards(AuthGuardV2) + @ApiCookieAuth() + @ApiOkResponse({ type: [CommentDto] }) + @Get('users/me/comments') + async fetchUserComments(@Req() req: Request): Promise { + const userId = req.user.userId; + return await this.svc_comments.fetchUserComments({ userId }); + } + + @ApiTags('게시글 API') @ApiOperation({ summary: '특정 게시글에 대한 댓글 수정' }) @ApiCookieAuth() @ApiOkResponse({ type: CommentDto }) @UseGuards(AuthGuardV2) - @Patch(':commentId') + @Patch('articles/:articleId/comments/:commentId') async patchComment( @Req() req: Request, @Param('articleId') articleId: number, @@ -84,13 +99,14 @@ export class CommentsController { }); } + @ApiTags('게시글 API') @ApiOperation({ summary: '댓글을 삭제한다.', description: '댓글을 논리삭제한다. date_deleted 칼럼에 값이 생긴다.', }) @ApiCookieAuth() @ApiNoContentResponse({ description: '삭제 성공' }) - @Delete(':commentId') + @Delete('articles/:articleId/comments/:commentId') @UseGuards(AuthGuardV2) @HttpCode(204) async deleteComment( diff --git a/src/APIs/comments/comments.repository.ts b/src/APIs/comments/comments.repository.ts index c210c7a..c2f3650 100644 --- a/src/APIs/comments/comments.repository.ts +++ b/src/APIs/comments/comments.repository.ts @@ -42,7 +42,7 @@ export class CommentsRepository extends Repository { .getOne(); } - async fetchComments({ + async fetchAll({ articleId, }: ICommentsRepositoryfetchComments): Promise { let comments = await this.createQueryBuilder('c') diff --git a/src/APIs/comments/comments.service.ts b/src/APIs/comments/comments.service.ts index 9dce56e..fc2e007 100644 --- a/src/APIs/comments/comments.service.ts +++ b/src/APIs/comments/comments.service.ts @@ -11,6 +11,7 @@ import { ICommentsServiceCreateComment, ICommentsServiceDeleteComment, ICommentsServiceFindComments, + ICommentsServiceFindUserComments, ICommentsServiceId, ICommentsServicePatchComment, } from './interfaces/comments.service.interface'; @@ -125,7 +126,17 @@ export class CommentsService { async fetchComments({ articleId, }: ICommentsServiceFindComments): Promise { - return await this.repo_comments.fetchComments({ articleId }); + return await this.repo_comments.fetchAll({ articleId }); + } + + async fetchUserComments({ + userId, + }: ICommentsServiceFindUserComments): Promise { + return await this.repo_comments.find({ + where: { userId }, + order: { dateCreated: 'DESC' }, + take: 10, + }); } async delete({ diff --git a/src/APIs/comments/interfaces/comments.service.interface.ts b/src/APIs/comments/interfaces/comments.service.interface.ts index e401231..77fd85e 100644 --- a/src/APIs/comments/interfaces/comments.service.interface.ts +++ b/src/APIs/comments/interfaces/comments.service.interface.ts @@ -20,6 +20,10 @@ export interface ICommentsServiceFindComments { articleId: number; } +export interface ICommentsServiceFindUserComments { + userId: number; +} + export interface ICommentsServiceDeleteComment { commentId: number; userId: number; From 7c4bf9cb632bc41a729de8b83d16a096202a9db1 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Jul 2024 16:33:19 +0900 Subject: [PATCH 234/236] fix(aws): delete acl access --- src/modules/aws/aws.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/aws/aws.service.ts b/src/modules/aws/aws.service.ts index d31277f..c22a2e1 100644 --- a/src/modules/aws/aws.service.ts +++ b/src/modules/aws/aws.service.ts @@ -31,7 +31,6 @@ export class AwsService { Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 Key: fileName, // 업로드될 파일의 이름 Body: resizedImageBuffer, // 업로드할 파일 - ACL: 'public-read', // 파일 접근 권한 ContentType: `image/${ext}`, // 파일 타입, }); await this.s3Client.send(command); @@ -67,7 +66,6 @@ export class AwsService { Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 Key: fileName, // 업로드될 파일의 이름 Body: resizedImageBuffer, // 업로드할 파일 - ACL: 'public-read', // 파일 접근 권한 ContentType: `image/${ext}`, // 파일 타입 }); From 78ae2eb6281f508485adbe88bb385e7d37db81e8 Mon Sep 17 00:00:00 2001 From: do-huni Date: Tue, 16 Jul 2024 16:46:51 +0900 Subject: [PATCH 235/236] fix(articlesRead): synchronized swagger --- src/APIs/articles/controllers/articles-read.controller.ts | 6 ++++-- src/APIs/articles/repositories/articles-read.repository.ts | 2 +- src/APIs/articles/services/articles-read.service.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/APIs/articles/controllers/articles-read.controller.ts b/src/APIs/articles/controllers/articles-read.controller.ts index dffbcd8..364bd1e 100644 --- a/src/APIs/articles/controllers/articles-read.controller.ts +++ b/src/APIs/articles/controllers/articles-read.controller.ts @@ -39,10 +39,12 @@ export class ArticlesReadController { description: '로그인된 유저의 임시작성 게시글을 조회한다.', }) @ApiCookieAuth() - @ApiOkResponse({ type: [ArticleDto] }) + @ApiOkResponse({ type: [ArticleDetailResponseDto] }) @UseGuards(AuthGuardV2) @Get('temp') - async fetchTempArticles(@Req() req: Request): Promise { + async fetchTempArticles( + @Req() req: Request, + ): Promise { const userId = req.user.userId; return await this.svc_articlesRead.readTempArticles({ userId }); } diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index 69fccba..33171cf 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -47,7 +47,7 @@ export class ArticlesReadRepository extends Repository
{ .getOne(); } - async readTemp({ userId }): Promise { + async readTemp({ userId }): Promise { return this.createQueryBuilder('p') .innerJoin('p.user', 'user') .leftJoinAndSelect('p.articleBackground', 'article_background') diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts index c605cbb..3bfc1f1 100644 --- a/src/APIs/articles/services/articles-read.service.ts +++ b/src/APIs/articles/services/articles-read.service.ts @@ -46,7 +46,7 @@ export class ArticlesReadService { return { article, stickerBlocks }; } - async readTempArticles({ userId }): Promise { + async readTempArticles({ userId }): Promise { return await this.repo_articlesRead.readTemp({ userId }); } From 795ff31659ccde430b30b4f4cc12778b433245f5 Mon Sep 17 00:00:00 2001 From: do-huni Date: Wed, 17 Jul 2024 18:03:42 +0900 Subject: [PATCH 236/236] feat(article): add current_image_id column --- deploy/deploy-staging.sh | 11 ++++++++++- src/APIs/articles/entities/article.entity.ts | 9 +++++++++ .../articles/repositories/articles-read.repository.ts | 1 - src/APIs/articles/services/articles-read.service.ts | 1 - src/APIs/notifications/notifications.controller.ts | 4 ++-- src/APIs/users/services/users-delete.service.ts | 2 +- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/deploy/deploy-staging.sh b/deploy/deploy-staging.sh index a57f6bd..a9ba481 100755 --- a/deploy/deploy-staging.sh +++ b/deploy/deploy-staging.sh @@ -2,6 +2,9 @@ # 스크립트의 실제 위치를 기준으로 경로 설정 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PEM_PATH="$SCRIPT_DIR/../../../keys/blccu-dev-rsa.pem" +ROOT_DIR=$(cd "$SCRIPT_DIR/.."; pwd) + +ENV_FILE="$ROOT_DIR/.env.staging" # PEM 파일 경로가 올바른지 확인 if [[ ! -f "$PEM_PATH" ]]; then @@ -34,6 +37,12 @@ docker buildx build --platform linux/amd64 -t $SERVICE_NAME . --load docker tag $SERVICE_NAME:$DOCKER_TAG $ECR_URL/$SERVICE_NAME:$DOCKER_TAG docker push $ECR_URL/$SERVICE_NAME:$DOCKER_TAG + # .env.staging 파일 전송 +echo -e "\n## .env.staging 파일 전송 to $HOST ##\n" +ssh -i $PEM_PATH $SERVER "mkdir -p /home/ubuntu/upload" +ssh -i $PEM_PATH $SERVER "chmod 700 /home/ubuntu/upload" +scp -i "$PEM_PATH" "$ENV_FILE" $SERVER:/home/$ACCOUNT/upload/.env.staging + # 현재 설정에서 활성 포트 찾기 CURRENT_PORT=$(ssh -i "$PEM_PATH" -o StrictHostKeyChecking=no $SERVER "grep 'server localhost:' $NGINX_CONFIG | awk '{print \$2}' | cut -d ':' -f 2 | sed 's/;//'") echo -e "\nOld = $CURRENT_PORT\n" @@ -57,7 +66,7 @@ OLD_SERVICE_NAME=$SERVICE_NAME-$CURRENT_PORT echo -e "\n## new docker pull & run ##\n" ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URL" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" -ssh -i $PEM_PATH $SERVER "docker run --env-file .env.staging -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" +ssh -i $PEM_PATH $SERVER "docker run --env-file /home/$ACCOUNT/upload/.env.staging -d --memory="512m" --cpus="0.5" -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # memory랑 cpu 사용량 조절 # 헬스체크 수행 diff --git a/src/APIs/articles/entities/article.entity.ts b/src/APIs/articles/entities/article.entity.ts index d13459a..3dbd088 100644 --- a/src/APIs/articles/entities/article.entity.ts +++ b/src/APIs/articles/entities/article.entity.ts @@ -57,6 +57,15 @@ export class Article extends IndexedCommonEntity { @IsNumber() userId: number; + @ApiProperty({ + description: '현재 이미지 라벨링 정보', + type: Number, + nullable: true, + }) + @Column({ name: 'currrent_image_id', nullable: true }) + @IsNumber() + currrentImageId: number; + @ApiProperty({ description: '제목(최대 100자)', type: String, default: '' }) @Column({ length: 100, default: '' }) @IsString() diff --git a/src/APIs/articles/repositories/articles-read.repository.ts b/src/APIs/articles/repositories/articles-read.repository.ts index 33171cf..7eb06d7 100644 --- a/src/APIs/articles/repositories/articles-read.repository.ts +++ b/src/APIs/articles/repositories/articles-read.repository.ts @@ -2,7 +2,6 @@ import { DataSource, Repository } from 'typeorm'; import { Article } from '../entities/article.entity'; import { Injectable } from '@nestjs/common'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; -import { ArticleDto } from '../dtos/common/article.dto'; import { transformKeysToArgsFormat } from 'src/utils/classUtils'; import { USER_PRIMARY_RESPONSE_DTO_KEYS } from 'src/APIs/users/dtos/response/user-primary-response.dto'; diff --git a/src/APIs/articles/services/articles-read.service.ts b/src/APIs/articles/services/articles-read.service.ts index 3bfc1f1..ffa25f9 100644 --- a/src/APIs/articles/services/articles-read.service.ts +++ b/src/APIs/articles/services/articles-read.service.ts @@ -10,7 +10,6 @@ import { } from '../interfaces/articles.service.interface'; import { ArticleDetailForUpdateResponseDto } from '../dtos/response/article-detail-for-update-response.dto'; import { ArticleDetailResponseDto } from '../dtos/response/article-detail-response.dto'; -import { ArticleDto } from '../dtos/common/article.dto'; @Injectable() export class ArticlesReadService { diff --git a/src/APIs/notifications/notifications.controller.ts b/src/APIs/notifications/notifications.controller.ts index 066d128..bcf8250 100644 --- a/src/APIs/notifications/notifications.controller.ts +++ b/src/APIs/notifications/notifications.controller.ts @@ -41,8 +41,8 @@ export class NotificationsController { @Sse('subscribe') connectUser(@Req() req: Request, @Res() res: Response) { const targetUserId = req.user.userId; - res.setTimeout(60 * 10000); // 600초로 설정, 필요에 따라 변경 가능 nginx도 함께 변경할 것. - + res.setTimeout(0); // 600초로 설정, 필요에 따라 변경 가능 nginx도 함께 변경할 것. + // res.setTimeout(15 * 1000); const sseStream = this.notificationsService.connectUser({ targetUserId, }); diff --git a/src/APIs/users/services/users-delete.service.ts b/src/APIs/users/services/users-delete.service.ts index 4a76165..660af37 100644 --- a/src/APIs/users/services/users-delete.service.ts +++ b/src/APIs/users/services/users-delete.service.ts @@ -91,7 +91,7 @@ export class UsersDeleteService { for (const following of followersToDelete) { await queryRunner.manager.decrement( User, - { userId: following.fromUserId }, + { id: following.fromUserId }, 'followingCount', 1, );