From e9df17034df7395fb5913cb7f66ce7ebd6f8bfe5 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 14:18:53 +0900 Subject: [PATCH 001/414] [Chore] install socket.io --- package-lock.json | 254 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 2 files changed, 256 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1985150..9b51763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", @@ -32,6 +33,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -2754,6 +2756,25 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.15.tgz", + "integrity": "sha512-KZAxNEADPwoORixh3NJgGYWMVGORVPKeTqjD7hbF8TPDLKWWxru9yasBQwEz2/wXH/WgpkQbbaYwx4nUjCIVpw==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2839,6 +2860,30 @@ } } }, + "node_modules/@nestjs/websockets": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.15.tgz", + "integrity": "sha512-OmCUJwvtagzXfMVko595O98UI3M9zg+URL+/HV7vd3QPMCZ3uGCKSq15YYJ99LHJn9NyK4e4Szm2KnHtUg2QzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3786,6 +3831,12 @@ "node": ">=16.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3890,6 +3941,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -3897,6 +3954,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5101,6 +5167,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -6207,6 +6282,62 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -9590,6 +9721,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -10964,6 +11105,98 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -12281,6 +12514,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index 14ead8d..792fe49 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", @@ -45,6 +46,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { From b8ee0e0333065df128f500f0b9bcdfbb13a20787 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 14:18:59 +0900 Subject: [PATCH 002/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ src/chat/chat.gateway.spec.ts | 18 ++++++++++++++++++ src/chat/chat.gateway.ts | 9 +++++++++ 3 files changed, 29 insertions(+) create mode 100644 src/chat/chat.gateway.spec.ts create mode 100644 src/chat/chat.gateway.ts diff --git a/src/app.module.ts b/src/app.module.ts index 5c900ea..7c75a10 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; +import { ChatGateway } from './chat/chat.gateway'; @Module({ imports: [ ConfigModule.forRoot({ @@ -10,5 +11,6 @@ import { UserModule } from '@modules/user/user.module'; AuthModule, // Auth 모듈 추가 UserModule, ], + providers: [ChatGateway], }) export class AppModule {} diff --git a/src/chat/chat.gateway.spec.ts b/src/chat/chat.gateway.spec.ts new file mode 100644 index 0000000..34daca9 --- /dev/null +++ b/src/chat/chat.gateway.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatGateway } from './chat.gateway'; + +describe('ChatGateway', () => { + let gateway: ChatGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatGateway], + }).compile(); + + gateway = module.get(ChatGateway); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts new file mode 100644 index 0000000..f505d25 --- /dev/null +++ b/src/chat/chat.gateway.ts @@ -0,0 +1,9 @@ +import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets'; + +@WebSocketGateway() +export class ChatGateway { + @SubscribeMessage('message') + handleMessage(client: any, payload: any): string { + return 'Hello world!'; + } +} From 745e037f45c008fd1878a1268ef42cf80f071504 Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:16:34 +0900 Subject: [PATCH 003/414] =?UTF-8?q?[Feature]=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EC=97=AD=ED=95=A0=20=EC=84=A0=ED=83=9D=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat] JWT Strategy 리턴값 변경 * [Feat] CallBack URI 변경 * [Feat] Role 선택 API 구현 * [Feat] 소셜 로그인 로직 변경 * [Feat] CORS 설정 --- src/main.ts | 9 +- src/modules/auth/auth.controller.ts | 48 +++-- src/modules/auth/auth.service.ts | 201 +++++++++++++++--- .../auth/strategies/github.strategy.ts | 2 +- src/modules/auth/strategies/jwt.strategy.ts | 8 +- 5 files changed, 217 insertions(+), 51 deletions(-) diff --git a/src/main.ts b/src/main.ts index c4cc14d..717935d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,15 +2,18 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; +import * as express from 'express'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors(); - app.useGlobalFilters(new HttpExceptionFilter()); + app.enableCors({ + origin: 'http://localhost:5173' | 'https://'; + credentials:true, + }); + //app.useGlobalFilters(new HttpExceptionFilter()); const port = process.env.PORT; await app.listen(process.env.PORT); - console.log(`Application is run on: http://localhost:${port}`); } bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 44bf877..daba169 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,8 +1,7 @@ -import { Controller, Get, UseGuards, Req, Body, Post } from '@nestjs/common'; +import { Controller, Get, UseGuards, Req, Body, Post, Put, BadRequestException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; -import { AuthUserDto } from './dto/auth-user.dto'; - +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @@ -13,17 +12,11 @@ export class AuthController { // Google 로그인 요청 } - // @Get('google/callback') - // @UseGuards(AuthGuard('google')) - // async googleCallback(@Req() req: { user: AuthUserDto }) { - // const { user, accessToken } = await this.authService.socialLogin(req.user); - // return { user, accessToken }; - // } @Post('google/callback') async googleCallback(@Body('code') code: string) { // Authorization Code 교환 및 사용자 정보 가져오기 - const { user, accessToken } = await this.authService.handleGoogleCallback(code); - return { user, accessToken }; + const { user, accessToken, isExistingUser } = await this.authService.handleGoogleCallback(code); + return { user, accessToken, isExistingUser }; } @Get('github') @@ -32,10 +25,33 @@ export class AuthController { // GitHub 로그인 요청 } - @Get('github/callback') - @UseGuards(AuthGuard('github')) - async githubCallback(@Req() req: { user: AuthUserDto }) { - const { user, accessToken } = await this.authService.socialLogin(req.user); - return { user, accessToken }; + @Post('github/callback') + async githubCallback(@Body('code') code: string) { + const { user, accessToken, isExistingUser } = await this.authService.handleGithubCallback(code); + return { user, accessToken, isExistingUser }; } + + // Role 선택 API + @Put('roleselect') + @UseGuards(JwtAuthGuard) + async selectRole( + @Body('role_id') roleId: number, + @Req() req: any, // JWT에서 사용자 정보 추출 + ) { + const validRoles = [1, 2, 3]; // 1: Programmer, 2: Artist, 3: Designer + + // 유효한 role_id인지 확인 + if (!validRoles.includes(roleId)) { + throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + } + + const userId = req.user?.id; // JWT에서 추출된 userId 확인 + + if (!userId) { + throw new BadRequestException('사용자 ID가 누락되었습니다.'); + } + + return await this.authService.updateUserRole(userId, roleId); + } + } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 14244a6..b340dda 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '@src/prisma/prisma.service'; import { AuthUserDto } from './dto/auth-user.dto'; @@ -12,27 +12,123 @@ export class AuthService { ) {} async handleGoogleCallback(code: string) { - // Google 토큰 요청 - const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: '1030869508870-7svsfcscu3la43lpprj6kui580cp0uhf.apps.googleusercontent.com', - client_secret: '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', - redirect_uri: 'http://localhost:5173/auth/google/callback', - grant_type: 'authorization_code', - }); - const { access_token } = tokenResponse.data; - // Google 사용자 정보 요청 - const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + try { + console.log('Received Authorization Code:', code); + + // Google 토큰 요청 + const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, + grant_type: 'authorization_code', + }); + + const { access_token } = tokenResponse.data; + + // Google 사용자 정보 요청 + const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userData = userInfoResponse.data; + + const isExistingUser = await this.checkUserExist(userData.email); + const user = await this.findOrCreateUser({ + email: userData.email, + name: userData.name, + nickname: userData.given_name, + profile_url: userData.picture, + auth_provider: 'google', + }); + + const jwt = this.generateJwt(user); + const responseUser = this.filterUserFields(user); + + return { user: responseUser, accessToken: jwt, isExistingUser }; + } catch (error) { + console.error('Google OAuth Error:', error.response?.data || error.message); + + if (error.response?.data?.error === 'invalid_grant') { + throw new Error('Authorization Code가 이미 사용되었거나 만료되었습니다.'); + } + + throw new Error('Google OAuth 인증 실패'); + } + } + + async handleGithubCallback(code: string) { + try { + console.log('Received GitHub Authorization Code:', code); + + // GitHub 토큰 요청 + const tokenResponse = await axios.post( + 'https://github.com/login/oauth/access_token', + { + code, + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + redirect_uri: process.env.GITHUB_CALLBACK_URL, + }, + { + headers: { Accept: 'application/json' }, + }, + ); + + const { access_token } = tokenResponse.data; + console.log(access_token); + // GitHub 사용자 정보 요청 + const userInfoResponse = await axios.get('https://api.github.com/user', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userData = userInfoResponse.data; + + // GitHub 사용자 이메일 요청 (필요 시) + let email = userData.email; + if (!email) { + const emailResponse = await axios.get('https://api.github.com/user/emails', { + headers: { Authorization: `Bearer ${access_token}` }, + }); - const userData = userInfoResponse.data; + const primaryEmail = emailResponse.data.find((e: any) => e.primary && e.verified); + email = primaryEmail?.email; + } - // JWT 생성 - const jwt = this.generateJwt(userData); - return { user: userData, accessToken: jwt }; + if (!email) { + throw new Error('GitHub 사용자 이메일을 확인할 수 없습니다.'); + } + + const isExistingUser = await this.checkUserExist(email); + const user = await this.findOrCreateUser({ + email, + name: userData.name || userData.login, + nickname: userData.login, + profile_url: userData.avatar_url, + auth_provider: 'github', + }); + + const jwt = this.generateJwt(user); + const responseUser = this.filterUserFields(user); + + return { user: responseUser, accessToken: jwt, isExistingUser }; + } catch (error) { + console.error('GitHub OAuth Error:', error.response?.data || error.message); + + if (error.response?.data?.error === 'bad_verification_code') { + throw new Error('Authorization Code가 잘못되었거나 만료되었습니다.'); + } + + throw new Error('GitHub OAuth 인증 실패'); + } } + private async checkUserExist(email: string): Promise{ + const user = await this.prisma.user.findUnique({ + where: { email }, + }); + return !!user; + } // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { const user = await this.prisma.user.findUnique({ @@ -58,20 +154,71 @@ export class AuthService { return user; } - // JWT Access Token 생성 - async generateAccessToken(user: any) { - const payload = { userId: user.id, email: user.email }; - return this.jwtService.sign(payload, { expiresIn: '1h' }); + private filterUserFields(user: any) { + return { + id: user.id, + email: user.email, + name: user.name, + nickname: user.nickname, + profile_url: user.profile_url, + auth_provider: user.auth_provider, + role_id: user.role_id, + }; } private generateJwt(user: any) { const payload = { email: user.email, id: user.id }; return this.jwtService.sign(payload, { expiresIn: '1h' }); } - // 소셜 로그인 프로세스 - async socialLogin(profile: AuthUserDto) { - const user = await this.findOrCreateUser(profile); - const accessToken = await this.generateAccessToken(user); - return { user, accessToken }; + + // 사용자 Role 업데이트 + async updateUserRole(userId: number, roleId: number) { + if (!userId) { + throw new BadRequestException('유효하지 않은 사용자 ID입니다.'); + } + + // 사용자 확인 + const user = await this.prisma.user.findUnique({ + where: { id: userId }, // userId가 반드시 존재해야 함 + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // Role 메시지 설정 + let roleMessage = ''; + switch (roleId) { + case 1: + roleMessage = '프로그래머로 변경되었습니다.'; + break; + case 2: + roleMessage = '아티스트로 변경되었습니다.'; + break; + case 3: + roleMessage = '디자이너로 변경되었습니다.'; + break; + default: + throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + } + // 사용자 Role 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { role_id: roleId }, + }); + + return { + message: { + code : 200, + text: `${roleMessage}` + }, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + nickname: updatedUser.nickname, + role_id: updatedUser.role_id, + }, + }; } } diff --git a/src/modules/auth/strategies/github.strategy.ts b/src/modules/auth/strategies/github.strategy.ts index 0608257..31cadc3 100644 --- a/src/modules/auth/strategies/github.strategy.ts +++ b/src/modules/auth/strategies/github.strategy.ts @@ -9,7 +9,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { super({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: 'http://localhost:8080/auth/github/callback', + callbackURL: 'http://localhost:5173/auth/github/callback', scope: ['user:email'], }); } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index dfedf53..988c9ef 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -3,16 +3,16 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { +export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, // 만료된 토큰은 거부 - secretOrKey: process.env.JWT_SECRET, // 환경 변수에서 비밀키 가져오기 + secretOrKey: process.env.JWT_SECRET, }); } async validate(payload: any) { - return { userId: payload.userId, email: payload.email }; + // req.user에 설정될 사용자 정보 반환 + return { id: payload.id, email: payload.email }; } } From 796038abd6442bd14626fd47ef72eb97755a2f6e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 18:08:43 +0900 Subject: [PATCH 004/414] =?UTF-8?q?[Feat]=20Prisma=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 457 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 51 +- 3 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20250107090813_creat_chat_tables/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/prisma/migrations/20250107090813_creat_chat_tables/migration.sql b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql new file mode 100644 index 0000000..420f7b8 --- /dev/null +++ b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql @@ -0,0 +1,457 @@ +-- CreateTable +CREATE TABLE `User` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `nickname` VARCHAR(191) NOT NULL, + `auth_provider` VARCHAR(191) NOT NULL, + `profile_url` VARCHAR(191) NULL, + `role_id` INTEGER NOT NULL, + `introduce` VARCHAR(191) NULL, + `status_id` INTEGER NOT NULL, + `apply_count` INTEGER NULL DEFAULT 0, + `post_count` INTEGER NULL DEFAULT 0, + `push_alert` BOOLEAN NOT NULL DEFAULT false, + `following_alert` BOOLEAN NOT NULL DEFAULT false, + `project_alert` BOOLEAN NOT NULL DEFAULT false, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `User_email_key`(`email`), + INDEX `User_role_id_fkey`(`role_id`), + INDEX `User_status_id_fkey`(`status_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Role` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Role_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Project` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `description` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Project_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `project_id` INTEGER NOT NULL, + `type_id` INTEGER NOT NULL, + `url` VARCHAR(191) NOT NULL, + + INDEX `ProjectLink_project_id_fkey`(`project_id`), + INDEX `ProjectLink_type_id_fkey`(`type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `LinkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `LinkType_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProgrammerData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `github_username` VARCHAR(191) NOT NULL, + `github_url` VARCHAR(191) NOT NULL, + `commit_count` INTEGER NOT NULL, + `contribution_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ProgrammerData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ArtistData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `soundcloud_url` VARCHAR(191) NOT NULL, + `portfolio_url` VARCHAR(191) NOT NULL, + `music_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ArtistData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Resume` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `introduce` VARCHAR(191) NOT NULL, + + INDEX `Resume_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Skill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Skill_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserSkill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `skill_id` INTEGER NOT NULL, + + INDEX `UserSkill_skill_id_fkey`(`skill_id`), + UNIQUE INDEX `UserSkill_user_id_skill_id_key`(`user_id`, `skill_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `platform` VARCHAR(191) NOT NULL, + `link` VARCHAR(191) NOT NULL, + + INDEX `UserLink_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Follows` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `following_user_id` INTEGER NOT NULL, + `followed_user_id` INTEGER NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `Follows_followed_user_id_fkey`(`followed_user_id`), + UNIQUE INDEX `Follows_following_user_id_followed_user_id_key`(`following_user_id`, `followed_user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + `view` INTEGER NOT NULL, + `comment_count` INTEGER NOT NULL, + `like_count` INTEGER NOT NULL, + + INDEX `FeedPost_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `FeedPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `FeedPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedComment` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + `content` VARCHAR(191) NOT NULL, + `image_url` VARCHAR(191) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + INDEX `FeedComment_post_id_fkey`(`post_id`), + INDEX `FeedComment_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedLike` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `FeedLike_post_id_fkey`(`post_id`), + INDEX `FeedLike_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `role` INTEGER NOT NULL, + `unit` VARCHAR(191) NOT NULL, + `start_date` DATETIME(3) NOT NULL, + `end_date` DATETIME(3) NOT NULL, + `work_type_id` INTEGER NOT NULL, + `recruiting` BOOLEAN NOT NULL, + `applicant_count` INTEGER NOT NULL, + `view` INTEGER NOT NULL, + `saved_count` INTEGER NOT NULL, + + INDEX `ProjectPost_user_id_fkey`(`user_id`), + INDEX `ProjectPost_work_type_id_fkey`(`work_type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `WorkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectSave` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `ProjectSave_post_id_fkey`(`post_id`), + INDEX `ProjectSave_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectDetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `detail_role_id` INTEGER NOT NULL, + + INDEX `ProjectDetailRole_detail_role_id_fkey`(`detail_role_id`), + INDEX `ProjectDetailRole_post_id_fkey`(`post_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `DetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `role_id` INTEGER NOT NULL, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `ProjectPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `ProjectPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserApplyProject` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `UserApplyProject_post_id_fkey`(`post_id`), + INDEX `UserApplyProject_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `photo_url` VARCHAR(191) NULL, + `message` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message_status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `message_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `is_read` BOOLEAN NOT NULL DEFAULT false, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_role_id_fkey` FOREIGN KEY (`role_id`) REFERENCES `Role`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_status_id_fkey` FOREIGN KEY (`status_id`) REFERENCES `Status`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Project` ADD CONSTRAINT `Project_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `Project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_type_id_fkey` FOREIGN KEY (`type_id`) REFERENCES `LinkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProgrammerData` ADD CONSTRAINT `ProgrammerData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ArtistData` ADD CONSTRAINT `ArtistData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Resume` ADD CONSTRAINT `Resume_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_skill_id_fkey` FOREIGN KEY (`skill_id`) REFERENCES `Skill`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserLink` ADD CONSTRAINT `UserLink_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_followed_user_id_fkey` FOREIGN KEY (`followed_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_following_user_id_fkey` FOREIGN KEY (`following_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPost` ADD CONSTRAINT `FeedPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `FeedTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_work_type_id_fkey` FOREIGN KEY (`work_type_id`) REFERENCES `WorkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_detail_role_id_fkey` FOREIGN KEY (`detail_role_id`) REFERENCES `DetailRole`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `ProjectTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_message_id_fkey` FOREIGN KEY (`message_id`) REFERENCES `Message`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..8a21669 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc2ebdf..1efa848 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,7 +40,9 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - + Room_users Room_users[] + Message Message[] + Message_status Message_status[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -144,12 +146,12 @@ model UserLink { } model Follows { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) following_user_id Int followed_user_id Int - created_at DateTime @default(now()) - followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) - following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) + created_at DateTime @default(now()) + followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) + following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) @@unique([following_user_id, followed_user_id]) @@index([followed_user_id], map: "Follows_followed_user_id_fkey") @@ -305,3 +307,42 @@ model UserApplyProject { @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } + +model Room { + id Int @id @default(autoincrement()) + name String + active Boolean @default(true) + created_at DateTime @default(now()) + Room_users Room_users[] + Message Message[] +} + +model Room_users { + id Int @id @default(autoincrement()) + room_id Int + user_id Int + joined_at DateTime @default(now()) + room Room @relation(fields: [room_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) +} + +model Message { + id Int @id @default(autoincrement()) + room_id Int + user_id Int + photo_url String? + message String + created_at DateTime @default(now()) + user User @relation(fields: [user_id], references: [id]) + room Room @relation(fields: [room_id], references: [id]) + message_status Message_status[] +} + +model Message_status { + id Int @id @default(autoincrement()) + message_id Int + user_id Int + is_read Boolean @default(false) + user User @relation(fields: [user_id], references: [id]) + message Message @relation(fields: [message_id], references: [id]) +} From 95177dc0d3fee7c584d572523fd1b6886bd53a46 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 19:37:01 +0900 Subject: [PATCH 005/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20Chat=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ src/chat/chat.module.ts | 7 +++++++ src/chat/chat.service.spec.ts | 18 ++++++++++++++++++ src/chat/chat.service.ts | 4 ++++ 4 files changed, 31 insertions(+) create mode 100644 src/chat/chat.module.ts create mode 100644 src/chat/chat.service.spec.ts create mode 100644 src/chat/chat.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 7c75a10..29b5416 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config'; import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; +import { ChatModule } from './chat/chat.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -10,6 +11,7 @@ import { ChatGateway } from './chat/chat.gateway'; }), AuthModule, // Auth 모듈 추가 UserModule, + ChatModule, ], providers: [ChatGateway], }) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts new file mode 100644 index 0000000..e403e6d --- /dev/null +++ b/src/chat/chat.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { ChatService } from './chat.service'; + +@Module({ + providers: [ChatService] +}) +export class ChatModule {} diff --git a/src/chat/chat.service.spec.ts b/src/chat/chat.service.spec.ts new file mode 100644 index 0000000..110cd7d --- /dev/null +++ b/src/chat/chat.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from './chat.service'; + +describe('ChatService', () => { + let service: ChatService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatService], + }).compile(); + + service = module.get(ChatService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts new file mode 100644 index 0000000..408edcc --- /dev/null +++ b/src/chat/chat.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ChatService {} From a4e0f4f451b947279e073d284d0e4a736e4eefb1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 19:37:36 +0900 Subject: [PATCH 006/414] =?UTF-8?q?[Feat]=20Prisma=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9E=84=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index e403e6d..e50d30c 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; import { ChatService } from './chat.service'; +import { PrismaModule } from '@src/prisma/prisma.module'; @Module({ - providers: [ChatService] + imports: [PrismaModule], + providers: [ChatService], }) export class ChatModule {} From 7ec15eab2ee3f879a454e1aed5bd792de55848bf Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 19:41:41 +0900 Subject: [PATCH 007/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 408edcc..a162318 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -1,4 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() -export class ChatService {} +export class ChatService { + constructor(private readonly prisma: PrismaService) {} + + // 채팅방 생성 + async createRoom(id: number, name: string) { + const data = { id, name }; + await this.prisma.room.create({ data }); + } +} From 745044684e2a0f7f5ae4f2962731acec0515952f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 21:17:48 +0900 Subject: [PATCH 008/414] =?UTF-8?q?[Refactor]=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index a162318..7f913bc 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -6,8 +6,9 @@ export class ChatService { constructor(private readonly prisma: PrismaService) {} // 채팅방 생성 - async createRoom(id: number, name: string) { - const data = { id, name }; - await this.prisma.room.create({ data }); + async creatRoom() { + const result = await this.prisma.room.create({}); + // 생성한 채팅방 id 리턴 + return result.id; } } From dcda836c4633bc4b30fa1487338c9143583a83e5 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 7 Jan 2025 21:22:28 +0900 Subject: [PATCH 009/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7f913bc..ed68102 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -11,4 +11,20 @@ export class ChatService { // 생성한 채팅방 id 리턴 return result.id; } + + // 채팅방 멤버 저장 + async joinRoom(roomId, userId1, userId2) { + await this.prisma.room_users.createMany({ + data: [ + { + room_id: roomId, + user_id: userId1, + }, + { + room_id: roomId, + user_id: userId2, + }, + ], + }); + } } From 0e67fefa179dd867b88999ea85a14652caab38b6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 15:33:18 +0900 Subject: [PATCH 010/414] =?UTF-8?q?[Feat]=20Prisma=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B0=8F=20=EC=86=8D=EC=84=B1=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 59 +++++++++++++++++++ prisma/schema.prisma | 34 +++++------ 2 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 prisma/migrations/20250109063122_fix_chat_table/migration.sql diff --git a/prisma/migrations/20250109063122_fix_chat_table/migration.sql b/prisma/migrations/20250109063122_fix_chat_table/migration.sql new file mode 100644 index 0000000..072405a --- /dev/null +++ b/prisma/migrations/20250109063122_fix_chat_table/migration.sql @@ -0,0 +1,59 @@ +/* + Warnings: + + - You are about to drop the column `room_id` on the `Message` table. All the data in the column will be lost. + - You are about to drop the `Room` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Room_users` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `channel_id` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `Message` DROP FOREIGN KEY `Message_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_user_id_fkey`; + +-- DropIndex +DROP INDEX `Message_room_id_fkey` ON `Message`; + +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `room_id`, + ADD COLUMN `channel_id` VARCHAR(191) NOT NULL; + +-- DropTable +DROP TABLE `Room`; + +-- DropTable +DROP TABLE `Room_users`; + +-- CreateTable +CREATE TABLE `Channel` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL DEFAULT 'default_channel_name', + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Channel_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `channel_id` VARCHAR(191) NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1efa848..1cb9e88 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,7 +40,7 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - Room_users Room_users[] + Channel_users Channel_users[] Message Message[] Message_status Message_status[] @@ -308,33 +308,33 @@ model UserApplyProject { @@index([user_id], map: "UserApplyProject_user_id_fkey") } -model Room { - id Int @id @default(autoincrement()) - name String - active Boolean @default(true) - created_at DateTime @default(now()) - Room_users Room_users[] - Message Message[] +model Channel { + id String @id + name String @default("default_channel_name") + active Boolean @default(true) + created_at DateTime @default(now()) + Channel_users Channel_users[] + Message Message[] } -model Room_users { - id Int @id @default(autoincrement()) - room_id Int - user_id Int - joined_at DateTime @default(now()) - room Room @relation(fields: [room_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) +model Channel_users { + id Int @id @default(autoincrement()) + channel_id String + user_id Int + joined_at DateTime @default(now()) + channel Channel @relation(fields: [channel_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) } model Message { id Int @id @default(autoincrement()) - room_id Int + channel_id String user_id Int photo_url String? message String created_at DateTime @default(now()) user User @relation(fields: [user_id], references: [id]) - room Room @relation(fields: [room_id], references: [id]) + channel Channel @relation(fields: [channel_id], references: [id]) message_status Message_status[] } From eba4c5c2e9bb02dbd3e93bdd1b8d78a2a233d2d1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 15:33:51 +0900 Subject: [PATCH 011/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index ed68102..15b6744 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -5,26 +5,31 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class ChatService { constructor(private readonly prisma: PrismaService) {} + // 이미 존재하는 채팅방인지 확인 + async channelExist(channelId) { + const result = await this.prisma.channel.count({ + where: { id: channelId }, + }); + return result; + } + // 채팅방 생성 async creatRoom() { - const result = await this.prisma.room.create({}); + const result = await this.prisma.channel.create({}); // 생성한 채팅방 id 리턴 return result.id; } // 채팅방 멤버 저장 - async joinRoom(roomId, userId1, userId2) { - await this.prisma.room_users.createMany({ - data: [ - { - room_id: roomId, - user_id: userId1, - }, - { - room_id: roomId, - user_id: userId2, - }, - ], + async joinChannel(channelId, userId1) { + await this.prisma.room_users.create({ + data: { + room_id: channelId, + user_id: userId1, + }, }); } + + // 메세지 저장 + // 메세지 상태 업데이트 } From 340a0a31c08762d0a00b5efbccedcc659d0246ba Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 15:52:17 +0900 Subject: [PATCH 012/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index f505d25..3147d00 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -1,9 +1,27 @@ -import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets'; +import { + SubscribeMessage, + WebSocketGateway, + MessageBody, +} from '@nestjs/websockets'; +import { ChatService } from './chat.service'; @WebSocketGateway() export class ChatGateway { - @SubscribeMessage('message') - handleMessage(client: any, payload: any): string { - return 'Hello world!'; + constructor(private readonly chatService: ChatService) {} + + // 채팅방 참여 + @SubscribeMessage('joinChannel') + async handleJoinChannel( + @MessageBody() data: { channelId: string; userId: string } + ) { + const { channelId, userId } = data; + // 존재하는 채팅방인지 확인 + const exist = await this.chatService.channelExist(channelId); + if (exist) return; + + // userId JWT 토큰 값이라 디코딩 해야함 + const user = userId; + // 채팅방 멤버 저장 + await this.chatService.joinChannel(channelId, userId); } } From 61bb5c2c3ee353781cdc5db31f43f9ff1d6d2d3e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 15:54:58 +0900 Subject: [PATCH 013/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 3 +++ src/chat/chat.service.ts | 16 ++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 3147d00..8f0c1a4 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -19,6 +19,9 @@ export class ChatGateway { const exist = await this.chatService.channelExist(channelId); if (exist) return; + // 채팅방 생성 + await this.chatService.createChannel(channelId); + // userId JWT 토큰 값이라 디코딩 해야함 const user = userId; // 채팅방 멤버 저장 diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 15b6744..5af2af5 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -14,20 +14,8 @@ export class ChatService { } // 채팅방 생성 - async creatRoom() { - const result = await this.prisma.channel.create({}); - // 생성한 채팅방 id 리턴 - return result.id; - } - - // 채팅방 멤버 저장 - async joinChannel(channelId, userId1) { - await this.prisma.room_users.create({ - data: { - room_id: channelId, - user_id: userId1, - }, - }); + async createChannel(id) { + await this.prisma.channel.create({ data: id }); } // 메세지 저장 From 9b79ff6a5867bc1c46a8a3591c163683e6fd4d8a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 16:02:14 +0900 Subject: [PATCH 014/414] =?UTF-8?q?[Feat]=20Jwt=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9E=84=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index e50d30c..1e54921 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { ChatService } from './chat.service'; import { PrismaModule } from '@src/prisma/prisma.module'; +import { JwtModule } from '@nestjs/jwt'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, JwtModule], providers: [ChatService], }) export class ChatModule {} From 0825d4a3cf753e31f8a02b0f87f9f3bae9f39c8a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 16:04:56 +0900 Subject: [PATCH 015/414] =?UTF-8?q?[Feat]=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=94=94=EC=BD=94=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5af2af5..293461f 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -18,6 +18,8 @@ export class ChatService { await this.prisma.channel.create({ data: id }); } + // 채팅방 멤버 저장 + async joinChannel(channelId, userId) {} // 메세지 저장 // 메세지 상태 업데이트 } From f6ad738ca79518e00cce4a025d5cde3700cdf00b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 16:06:46 +0900 Subject: [PATCH 016/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 11 ++++++++--- src/chat/chat.service.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 8f0c1a4..aab0d94 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -4,10 +4,14 @@ import { MessageBody, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; +import { JwtService } from '@nestjs/jwt'; @WebSocketGateway() export class ChatGateway { - constructor(private readonly chatService: ChatService) {} + constructor( + private readonly chatService: ChatService, + private readonly jwtService: JwtService + ) {} // 채팅방 참여 @SubscribeMessage('joinChannel') @@ -23,8 +27,9 @@ export class ChatGateway { await this.chatService.createChannel(channelId); // userId JWT 토큰 값이라 디코딩 해야함 - const user = userId; + const user = this.jwtService.decode(userId); + // 채팅방 멤버 저장 - await this.chatService.joinChannel(channelId, userId); + await this.chatService.joinChannel(channelId, user.id); } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 293461f..67528a0 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -19,7 +19,14 @@ export class ChatService { } // 채팅방 멤버 저장 - async joinChannel(channelId, userId) {} + async joinChannel(channelId, userId) { + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + } // 메세지 저장 // 메세지 상태 업데이트 } From 33301c95c29c7ce5df9de46c9c7b2ca879e64f65 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 16:24:55 +0900 Subject: [PATCH 017/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- src/chat/chat.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index aab0d94..aac7013 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -30,6 +30,6 @@ export class ChatGateway { const user = this.jwtService.decode(userId); // 채팅방 멤버 저장 - await this.chatService.joinChannel(channelId, user.id); + await this.chatService.joinChannel(channelId, user.user_id); } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 67528a0..59e5bbf 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -15,7 +15,7 @@ export class ChatService { // 채팅방 생성 async createChannel(id) { - await this.prisma.channel.create({ data: id }); + await this.prisma.channel.create({ data: { id } }); } // 채팅방 멤버 저장 From 4d2adcf4dda66853378254c93983fa460de19cd9 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 17:08:49 +0900 Subject: [PATCH 018/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 14 +++++++++----- src/chat/chat.service.ts | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index aac7013..ed6e776 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -19,16 +19,20 @@ export class ChatGateway { @MessageBody() data: { channelId: string; userId: string } ) { const { channelId, userId } = data; + + // userId JWT 토큰 값이라 디코딩 해야함 + const user = this.jwtService.decode(userId); + // 존재하는 채팅방인지 확인 - const exist = await this.chatService.channelExist(channelId); - if (exist) return; + const existData = await this.chatService.channelExist( + channelId, + user.user_id + ); + if (existData) return; // 채팅방 생성 await this.chatService.createChannel(channelId); - // userId JWT 토큰 값이라 디코딩 해야함 - const user = this.jwtService.decode(userId); - // 채팅방 멤버 저장 await this.chatService.joinChannel(channelId, user.user_id); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 59e5bbf..e49e513 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -6,11 +6,43 @@ export class ChatService { constructor(private readonly prisma: PrismaService) {} // 이미 존재하는 채팅방인지 확인 - async channelExist(channelId) { - const result = await this.prisma.channel.count({ + // 없다면 생성, 있다면 매핑 테이블 확인 + // 매핑 테이블 유저 데이터 있다면 대화 내용 불러오기, 없다면 매핑 테이블에 유저 데이터 생성 + async channelExist(channelId, userId) { + const exist = await this.prisma.channel.count({ where: { id: channelId }, }); - return result; + + // 존재하지 않으면 함수 종료 후 컨트롤러에서 채팅방 생성 로직 재개 + if (!exist) return false; + + // 매핑 테이블 channel_users에 유저 데이터 있는지 확인 + const userExist = await this.prisma.channel_users.findFirst({ + where: { + channel_id: channelId, + user_id: userId, + }, + }); + + // 있다면 대화내용 불러오기 + if (userExist) { + const message = await this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + }); + return message; + } else { + // 없다면 매핑 테이블에 유저 데이터 추가 + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + const notice = `${userId}님이 채팅방에 참가했습니다`; + return { notice }; + } } // 채팅방 생성 From ae8eb8fe34d0dde2fa8bfcf53537d80414b854ab Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:52:38 +0900 Subject: [PATCH 019/414] =?UTF-8?q?[Feature]=209=20-Refresh=20token=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Add] ioredis 패키지 추가 * [Feat] Redis 관련 설정 파일 구현 * [Feat] payload 변경 * [Feat] Redis를 이용한 리프레쉬 토큰 구현 * [Feat] http status code 타입화 * [Feat] 응답 관련 Interface DTO 구현 * [Feat] Redis 연결 config 구현 * [Feat] Redis 사용을 위한 module, service 로직 구현 * [Feat] 에러 메세지 관련 타입화 구현 * [Feat] http-exception filter 고도화 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] auth 관련 코드들 응답처리, 에러처리 리펙토링 * [Feat] 에러메세지 추가 구현 * [Feat] 에러메세지 추가 구현 * [Feat] HttpExceptionFilter Global 설정 * [Feat] 응답 데이터에 대한 DTO 구현 * [Refactor] 필요없는 코드 제거 * [Feat] 리프레쉬 토큰 쿠키에 전달하도록 로직 변경 구현 * [Feat] payload 응답 값 변경 * [Feat] Response dto 변경 구현 * [Refactor] auth 관련 컨트롤러, 서비스 코드 리팩토링 * [Fix] accessToken 시간 변경 --- package-lock.json | 79 +++++++ package.json | 1 + src/app.controller.ts | 11 +- src/common/constants/error-messages.ts | 50 +++- src/common/constants/http-status-code.ts | 9 + src/common/dto/response.dto.ts | 33 +++ src/common/filters/http-exception.filter.ts | 19 +- src/config/redis.config.ts | 8 + src/main.ts | 10 +- src/modules/auth/auth.controller.ts | 154 +++++++++++-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/auth.service.ts | 241 ++++++++++++-------- src/modules/auth/strategies/jwt.strategy.ts | 5 +- src/modules/redis/redis.module.ts | 23 ++ src/modules/redis/redis.service.ts | 19 ++ src/modules/user/user.controller.ts | 10 +- src/modules/user/user.service.ts | 23 +- 17 files changed, 514 insertions(+), 184 deletions(-) create mode 100644 src/common/constants/http-status-code.ts create mode 100644 src/common/dto/response.dto.ts create mode 100644 src/config/redis.config.ts create mode 100644 src/modules/redis/redis.module.ts create mode 100644 src/modules/redis/redis.service.ts diff --git a/package-lock.json b/package-lock.json index 1985150..9315f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-github": "^1.1.0", @@ -1856,6 +1857,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6017,6 +6024,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7727,6 +7743,30 @@ "node": ">=12.0.0" } }, + "node_modules/ioredis": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz", + "integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==", + "license": "MIT", + "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/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9070,12 +9110,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "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==", + "license": "MIT" + }, "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==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10376,6 +10428,27 @@ "@redis/time-series": "1.1.0" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -11032,6 +11105,12 @@ "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==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 14ead8d..b75f88d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-github": "^1.1.0", diff --git a/src/app.controller.ts b/src/app.controller.ts index fa2ccf6..b0f585e 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,6 +1,5 @@ -import { Controller, Get, HttpStatus, HttpException } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { AppService } from '@src/app.service'; -import { ERROR_MESSAGES } from '@common/constants/error-messages'; @Controller() export class AppController { @@ -10,12 +9,4 @@ export class AppController { getHello(): string { return this.appService.getHello(); } - - @Get('test-error') - testError() { - throw new HttpException( - ERROR_MESSAGES.INTERNAL_SERVER_ERROR, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } } diff --git a/src/common/constants/error-messages.ts b/src/common/constants/error-messages.ts index 3dbce0f..a834cfa 100644 --- a/src/common/constants/error-messages.ts +++ b/src/common/constants/error-messages.ts @@ -1,10 +1,42 @@ -export const ERROR_MESSAGES = { - INVALID_ROLE_ID: '유효하지 않은 역할 ID입니다. 허용된 값은 1, 2, 3입니다.', - USER_NOT_FOUND: '해당 ID를 가진 사용자를 찾을 수 없습니다.', - UNAUTHORIZED: '인증이 필요합니다. 다시 로그인해주세요.', - FORBIDDEN: '이 리소스에 접근할 권한이 없습니다.', - JWT_EXPIRED: '세션이 만료되었습니다. 다시 로그인해주세요.', - INTERNAL_SERVER_ERROR: - '예기치 않은 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', - BAD_REQUEST: '요청 데이터가 올바르지 않습니다. 입력값을 확인해주세요.', +import { HttpStatusCodes } from '@common/constants/http-status-code'; + +export const ErrorMessages = { + AUTH: { + INVALID_REFRESH_TOKEN: { + code: HttpStatusCodes.UNAUTHORIZED, + text: '유효하지 않은 리프레시 토큰입니다.', + }, + USER_NOT_FOUND: { + code: HttpStatusCodes.NOT_FOUND, + text: '사용자를 찾을 수 없습니다.', + }, + TOKEN_EXPIRED: { + code: HttpStatusCodes.UNAUTHORIZED, + text: '토큰이 만료되었습니다.', + }, + }, + VALIDATION: { + MISSING_REQUIRED_FIELDS: { + code: HttpStatusCodes.BAD_REQUEST, + text: '필수 입력 필드가 누락되었습니다.', + }, + INVALID_EMAIL: { + code: HttpStatusCodes.BAD_REQUEST, + text: '유효하지 않은 이메일 형식입니다.', + }, + INVALID_ROLE_ID: { + code: HttpStatusCodes.BAD_REQUEST, + text: '유효하지 않은 역할 ID입니다.', + }, + }, + SERVER: { + DATABASE_ERROR: { + code: HttpStatusCodes.INTERNAL_SERVER_ERROR, + text: '데이터베이스 작업 중 오류가 발생했습니다.', + }, + INTERNAL_ERROR: { + code: HttpStatusCodes.INTERNAL_SERVER_ERROR, + text: '서버에서 오류가 발생했습니다.', + }, + }, }; diff --git a/src/common/constants/http-status-code.ts b/src/common/constants/http-status-code.ts new file mode 100644 index 0000000..bc71485 --- /dev/null +++ b/src/common/constants/http-status-code.ts @@ -0,0 +1,9 @@ +export const HttpStatusCodes = { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, +}; diff --git a/src/common/dto/response.dto.ts b/src/common/dto/response.dto.ts new file mode 100644 index 0000000..ed37552 --- /dev/null +++ b/src/common/dto/response.dto.ts @@ -0,0 +1,33 @@ +export class ApiResponse { + message: { + code: number; // HTTP 상태 코드 + text: string; // 메시지 + }; + + [key: string]: any; // 동적으로 다른 필드를 추가 가능 + + constructor(statusCode: number, message: string, additionalFields?: T) { + this.message = { + code: statusCode, + text: message, + }; + + // 추가 데이터를 최상위 레벨에 병합 + if (additionalFields) { + Object.assign(this, additionalFields); // `data` 키 없이 병합 + } + } +} +export class ErrorResponse { + message: { + code: number; // HTTP 상태 코드 + text: string; // 에러 메시지 + }; + + constructor(statusCode: number, message: string) { + this.message = { + code: statusCode, + text: message, + }; + } +} diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 94de81b..2c26892 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -5,30 +5,33 @@ import { HttpException, HttpStatus, } from '@nestjs/common'; -import { Response } from 'express'; +import { Request, Response } from 'express'; +import { ErrorResponse } from '@common/dto/response.dto'; -@Catch() // 모든 예외를 잡도록 설정 +@Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: unknown, host: ArgumentsHost) { + catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); - // HTTP 예외인지 확인 const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; - // 에러 메시지 추출 const message = exception instanceof HttpException ? exception.getResponse() - : '서버 내부의 에러입니다'; + : '서버에서 오류가 발생했습니다.'; + + const errorResponse = new ErrorResponse( + status, + typeof message === 'string' ? message : (message as any).message + ); response.status(status).json({ - statusCode: status, - message: typeof message === 'string' ? message : (message as any).message, + ...errorResponse, timestamp: new Date().toISOString(), path: request.url, }); diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts new file mode 100644 index 0000000..35615b9 --- /dev/null +++ b/src/config/redis.config.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class RedisConfig { + public static readonly host = process.env.REDIS_HOST || 'localhost'; + public static readonly port = parseInt(process.env.REDIS_PORT, 10) || 6379; + public static readonly password = process.env.REDIS_PASSWORD || undefined; +} diff --git a/src/main.ts b/src/main.ts index 717935d..c74f07b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,18 +2,16 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; -import * as express from 'express'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ - origin: 'http://localhost:5173' | 'https://'; - credentials:true, + origin: true, + credentials: true, + exposedHeaders: ['Authorization'], }); - //app.useGlobalFilters(new HttpExceptionFilter()); - - const port = process.env.PORT; + app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); } bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index daba169..db5a133 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,10 +1,28 @@ -import { Controller, Get, UseGuards, Req, Body, Post, Put, BadRequestException } from '@nestjs/common'; +import { + Controller, + Get, + UseGuards, + Req, + Body, + Post, + Put, + HttpException, + Res, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { AuthService } from './auth.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { AuthService } from '@src/modules/auth/auth.service'; +import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; +import { JwtService } from '@nestjs/jwt'; +import { ApiResponse } from '@common/dto/response.dto'; +import { ErrorMessages } from '@common/constants/error-messages'; +import { HttpStatusCodes } from '@common/constants/http-status-code'; +import { Response } from 'express'; @Controller('auth') export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly jwtService: JwtService + ) {} @Get('google') @UseGuards(AuthGuard('google')) @@ -13,10 +31,33 @@ export class AuthController { } @Post('google/callback') - async googleCallback(@Body('code') code: string) { + async googleCallback(@Body('code') code: string, @Res() res: Response) { // Authorization Code 교환 및 사용자 정보 가져오기 - const { user, accessToken, isExistingUser } = await this.authService.handleGoogleCallback(code); - return { user, accessToken, isExistingUser }; + const { user, accessToken, refreshToken, isExistingUser } = + await this.authService.handleGoogleCallback(code); + + console.log(refreshToken); + // 리프레시 토큰을 HTTP-Only 쿠키로 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + sameSite: 'none', // CSRF 방지 + maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + }); + + return res.status(HttpStatusCodes.OK).json( + new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + user, + accessToken, + refreshToken, + isExistingUser, + }) + ); + // return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + // user, + // accessToken, + // isExistingUser, + // }); } @Get('github') @@ -26,9 +67,24 @@ export class AuthController { } @Post('github/callback') - async githubCallback(@Body('code') code: string) { - const { user, accessToken, isExistingUser } = await this.authService.handleGithubCallback(code); - return { user, accessToken, isExistingUser }; + async githubCallback(@Body('code') code: string, @Res() res: Response) { + // Authorization Code 교환 및 사용자 정보 가져오기 + const { user, accessToken, refreshToken, isExistingUser } = + await this.authService.handleGithubCallback(code); + + // 리프레시 토큰을 HTTP-Only 쿠키로 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', // CSRF 방지 + maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + }); + + return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + user, + accessToken, + isExistingUser, + }); } // Role 선택 API @@ -36,22 +92,80 @@ export class AuthController { @UseGuards(JwtAuthGuard) async selectRole( @Body('role_id') roleId: number, - @Req() req: any, // JWT에서 사용자 정보 추출 + @Req() req: any, + @Res() res: Response ) { - const validRoles = [1, 2, 3]; // 1: Programmer, 2: Artist, 3: Designer + const userId = req.user?.user_id; + console.log(userId); + const { user, message } = await this.authService.updateUserRole( + userId, + roleId + ); - // 유효한 role_id인지 확인 - if (!validRoles.includes(roleId)) { - throw new BadRequestException('유효하지 않은 역할 ID입니다.'); - } + const serviceResult = await this.authService.updateUserRole(userId, roleId); + + console.log('Service Result in Controller:', serviceResult); - const userId = req.user?.id; // JWT에서 추출된 userId 확인 + // 응답 객체 생성 + const responseBody = { + message: { + code: HttpStatusCodes.OK, + text: serviceResult.message, + }, + user: serviceResult.user, + }; - if (!userId) { - throw new BadRequestException('사용자 ID가 누락되었습니다.'); + console.log('Response Body:', responseBody); + + // 응답 반환 + return res.status(HttpStatusCodes.OK).json(responseBody); + } + + @Post('refresh') + async refreshAccessToken(@Req() req: any, @Res() res: Response) { + const refreshToken = req.cookies['refreshToken']; + + if (!refreshToken) { + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + //const userId = req.user?.user_id; + const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + const isValid = await this.authService.validateRefreshToken( + userId, + refreshToken + ); + if (!isValid) { + const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; + throw new HttpException(error.text, error.code); } - return await this.authService.updateUserRole(userId, roleId); + const newAccessToken = await this.authService.generateAccessToken(userId); + const newRefreshToken = await this.authService.generateRefreshToken(userId); + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + return res.status(HttpStatusCodes.OK).json( + new ApiResponse( + HttpStatusCodes.OK, + '액세스 토큰이 성공적으로 갱신되었습니다.', + { + accessToken: newAccessToken, + } + ) + ); } + @Post('logout') + async logout(@Req() req: any, @Res() res: Response) { + res.clearCookie('refreshToken'); // HTTP-Only 쿠키 삭제 + return res + .status(HttpStatusCodes.OK) + .json(new ApiResponse(HttpStatusCodes.OK, '로그아웃 성공')); + } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 46153c1..74b3159 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -8,10 +8,11 @@ import { GitHubStrategy } from './strategies/github.strategy'; import { GoogleStrategy } from './strategies/google.strategy'; import { PrismaService } from '@src/prisma/prisma.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; - +import { RedisModule } from '../redis/redis.module'; @Module({ imports: [ PassportModule, + RedisModule, JwtModule.register({ secret: process.env.JWT_SECRET, // JWT 비밀키 설정 signOptions: { expiresIn: '1h' }, // 기본 만료 시간 diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index b340dda..89a4e79 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,35 +1,41 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, HttpException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { RedisService } from '@modules/redis/redis.service'; import { PrismaService } from '@src/prisma/prisma.service'; import { AuthUserDto } from './dto/auth-user.dto'; import axios from 'axios'; +import { ErrorMessages } from '@common/constants/error-messages'; +import { HttpStatusCodes } from '@common/constants/http-status-code'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, - private readonly prisma: PrismaService + private readonly prisma: PrismaService, + private readonly redisService: RedisService ) {} async handleGoogleCallback(code: string) { try { - console.log('Received Authorization Code:', code); - // Google 토큰 요청 - const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: process.env.GOOGLE_CLIENT_ID, - client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, - grant_type: 'authorization_code', - }); - + const tokenResponse = await axios.post( + 'https://oauth2.googleapis.com/token', + { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, + grant_type: 'authorization_code', + } + ); const { access_token } = tokenResponse.data; - // Google 사용자 정보 요청 - const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + const userInfoResponse = await axios.get( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + headers: { Authorization: `Bearer ${access_token}` }, + } + ); const userData = userInfoResponse.data; @@ -42,25 +48,30 @@ export class AuthService { auth_provider: 'google', }); - const jwt = this.generateJwt(user); - const responseUser = this.filterUserFields(user); + const accessToken = this.generateAccessToken(user.id); + const refreshToken = this.generateRefreshToken(user.id); // 리프레시 토큰 생성 - return { user: responseUser, accessToken: jwt, isExistingUser }; - } catch (error) { - console.error('Google OAuth Error:', error.response?.data || error.message); + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); - if (error.response?.data?.error === 'invalid_grant') { - throw new Error('Authorization Code가 이미 사용되었거나 만료되었습니다.'); - } + const responseUser = this.filterUserFields(user); - throw new Error('Google OAuth 인증 실패'); + return { + user: responseUser, + accessToken, + refreshToken, + isExistingUser, + }; + } catch (error) { + throw new HttpException( + ErrorMessages.SERVER.INTERNAL_ERROR.text, + ErrorMessages.SERVER.INTERNAL_ERROR.code + ); } } async handleGithubCallback(code: string) { try { - console.log('Received GitHub Authorization Code:', code); - // GitHub 토큰 요청 const tokenResponse = await axios.post( 'https://github.com/login/oauth/access_token', @@ -72,11 +83,10 @@ export class AuthService { }, { headers: { Accept: 'application/json' }, - }, + } ); - + const { access_token } = tokenResponse.data; - console.log(access_token); // GitHub 사용자 정보 요청 const userInfoResponse = await axios.get('https://api.github.com/user', { headers: { Authorization: `Bearer ${access_token}` }, @@ -87,11 +97,16 @@ export class AuthService { // GitHub 사용자 이메일 요청 (필요 시) let email = userData.email; if (!email) { - const emailResponse = await axios.get('https://api.github.com/user/emails', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + const emailResponse = await axios.get( + 'https://api.github.com/user/emails', + { + headers: { Authorization: `Bearer ${access_token}` }, + } + ); - const primaryEmail = emailResponse.data.find((e: any) => e.primary && e.verified); + const primaryEmail = emailResponse.data.find( + (e: any) => e.primary && e.verified + ); email = primaryEmail?.email; } @@ -108,22 +123,27 @@ export class AuthService { auth_provider: 'github', }); - const jwt = this.generateJwt(user); - const responseUser = this.filterUserFields(user); + const jwt = this.generateAccessToken(user.id); + const refreshToken = await this.generateRefreshToken(user.id); // 리프레시 토큰 생성 - return { user: responseUser, accessToken: jwt, isExistingUser }; + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); + const responseUser = this.filterUserFields(user); + return { + user: responseUser, + accessToken: jwt, + refreshToken: refreshToken, // 리프레시 토큰 반환 + isExistingUser, + }; } catch (error) { - console.error('GitHub OAuth Error:', error.response?.data || error.message); - - if (error.response?.data?.error === 'bad_verification_code') { - throw new Error('Authorization Code가 잘못되었거나 만료되었습니다.'); - } - - throw new Error('GitHub OAuth 인증 실패'); + throw new HttpException( + ErrorMessages.SERVER.INTERNAL_ERROR.text, + ErrorMessages.SERVER.INTERNAL_ERROR.code + ); } } - private async checkUserExist(email: string): Promise{ + private async checkUserExist(email: string): Promise { const user = await this.prisma.user.findUnique({ where: { email }, }); @@ -131,32 +151,27 @@ export class AuthService { } // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { - const user = await this.prisma.user.findUnique({ + return this.prisma.user.upsert({ where: { email: profile.email }, + update: {}, // 이미 존재하면 아무것도 업데이트하지 않음 + create: { + email: profile.email, + name: profile.name, + nickname: profile.nickname, + profile_url: profile.profile_url, + auth_provider: profile.auth_provider, + push_alert: false, + following_alert: false, + project_alert: false, + role: { connect: { id: 1 } }, + status: { connect: { id: 1 } }, + }, }); - - if (!user) { - return this.prisma.user.create({ - data: { - email: profile.email, - name: profile.name, - nickname: profile.nickname, - profile_url: profile.profile_url, - auth_provider: profile.auth_provider, - push_alert: false, - following_alert: false, - project_alert: false, - role: { connect: { id: 1 } }, - status: { connect: { id: 1 } }, - }, - }); - } - return user; } private filterUserFields(user: any) { return { - id: user.id, + user_id: user.id, email: user.email, name: user.name, nickname: user.nickname, @@ -166,59 +181,89 @@ export class AuthService { }; } - private generateJwt(user: any) { - const payload = { email: user.email, id: user.id }; - return this.jwtService.sign(payload, { expiresIn: '1h' }); + generateAccessToken(userId: number): string { + return this.jwtService.sign( + { userId }, + { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + ); } - // 사용자 Role 업데이트 - async updateUserRole(userId: number, roleId: number) { - if (!userId) { - throw new BadRequestException('유효하지 않은 사용자 ID입니다.'); + generateRefreshToken(userId: number): string { + return this.jwtService.sign( + { userId }, + { expiresIn: '7d', secret: process.env.REFRESH_TOKEN_SECRET } + ); + } + + getUserIdFromRefreshToken(refreshToken: string): number | null { + try { + const payload = this.jwtService.verify(refreshToken, { + secret: process.env.REFRESH_TOKEN_SECRET, + }); + return payload.userId; + } catch (error) { + return null; } + } + // 리프레시 토큰 저장 + async storeRefreshToken(userId: number, refreshToken: string): Promise { + const key = `refresh_token:${userId}`; + const ttl = 7 * 24 * 60 * 60; // 7일 + await this.redisService.set(key, refreshToken, ttl); + } - // 사용자 확인 - const user = await this.prisma.user.findUnique({ - where: { id: userId }, // userId가 반드시 존재해야 함 - }); + // 리프레시 토큰 검증 + async validateRefreshToken(userId: number, token: string): Promise { + const key = `refresh_token:${userId}`; + const storedToken = await this.redisService.get(key); + return storedToken === token; + } + // 리프레시 토큰 삭제 + async deleteRefreshToken(userId: number): Promise { + const key = `refresh_token:${userId}`; + await this.redisService.del(key); + } + // 사용자 Role 업데이트 + async updateUserRole(userId: number, roleId: number) { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) { - throw new NotFoundException('사용자를 찾을 수 없습니다.'); + const error = ErrorMessages.AUTH.USER_NOT_FOUND; + throw new HttpException(error.text, error.code); } - // Role 메시지 설정 - let roleMessage = ''; - switch (roleId) { - case 1: - roleMessage = '프로그래머로 변경되었습니다.'; - break; - case 2: - roleMessage = '아티스트로 변경되었습니다.'; - break; - case 3: - roleMessage = '디자이너로 변경되었습니다.'; - break; - default: - throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + // 역할에 따른 메시지 생성 + const roleMessages = { + 1: '프로그래머로 변경되었습니다.', + 2: '아티스트로 변경되었습니다.', + 3: '디자이너로 변경되었습니다.', + }; + + if (!roleMessages[roleId]) { + throw new HttpException( + '유효하지 않은 역할 ID입니다.', + HttpStatusCodes.BAD_REQUEST + ); } - // 사용자 Role 업데이트 + + // 사용자 역할 업데이트 const updatedUser = await this.prisma.user.update({ where: { id: userId }, data: { role_id: roleId }, }); - return { - message: { - code : 200, - text: `${roleMessage}` - }, + const result = { user: { - id: updatedUser.id, + user_id: updatedUser.id, email: updatedUser.email, name: updatedUser.name, nickname: updatedUser.nickname, role_id: updatedUser.role_id, }, + message: roleMessages[roleId], }; + + console.log('Service Result:', result); // 디버깅 + return result; } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 988c9ef..ac9ef55 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -7,12 +7,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: process.env.JWT_SECRET, + secretOrKey: process.env.ACCESS_TOKEN_SECRET, }); } async validate(payload: any) { + console.log('JWT_payload: ', payload); // req.user에 설정될 사용자 정보 반환 - return { id: payload.id, email: payload.email }; + return { user_id: payload.userId, email: payload.email }; } } diff --git a/src/modules/redis/redis.module.ts b/src/modules/redis/redis.module.ts new file mode 100644 index 0000000..a61e7b9 --- /dev/null +++ b/src/modules/redis/redis.module.ts @@ -0,0 +1,23 @@ +import { Module, Global } from '@nestjs/common'; +import Redis from 'ioredis'; +import { RedisService } from './redis.service'; +import { RedisConfig } from '@config/redis.config'; + +@Global() +@Module({ + providers: [ + { + provide: 'REDIS_CLIENT', + useFactory: () => { + return new Redis({ + host: RedisConfig.host, + port: RedisConfig.port, + password: RedisConfig.password, + }); + }, + }, + RedisService, + ], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/src/modules/redis/redis.service.ts b/src/modules/redis/redis.service.ts new file mode 100644 index 0000000..308a778 --- /dev/null +++ b/src/modules/redis/redis.service.ts @@ -0,0 +1,19 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Redis } from 'ioredis'; + +@Injectable() +export class RedisService { + constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {} + + async set(key: string, value: string, ttl: number): Promise { + await this.redis.set(key, value, 'EX', ttl); // TTL 설정 + } + + async get(key: string): Promise { + return this.redis.get(key); + } + + async del(key: string): Promise { + await this.redis.del(key); + } +} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 896c6a4..4bbfcb3 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,15 +1,7 @@ -import { Controller, Patch, Body, UseGuards, Req } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { UserService } from './user.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} - - @UseGuards(JwtAuthGuard) - @Patch('select-role') - async selectRole(@Req() req, @Body('roleId') roleId: number) { - const userId = req.user.id; // JWT로부터 가져온 사용자 ID - return this.userService.updateUserRole(userId, roleId); - } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 37036d0..757992d 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,27 +1,8 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { ERROR_MESSAGES } from '@common/constants/error-messages'; +import { Injectable } from '@nestjs/common'; + import { PrismaService } from '@prisma/prisma.service'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} - async updateUserRole(userId: number, roleId: number) { - if (![1, 2, 3].includes(roleId)) { - throw new BadRequestException(ERROR_MESSAGES.INVALID_ROLE_ID); - } - - const user = await this.prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND); - } - console.log(userId); - return this.prisma.user.update({ - where: { id: userId }, - data: { role_id: roleId }, - }); - } } From ff5752f316caeda2b2c599a7ce1f765f4f7fbf74 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 17:57:08 +0900 Subject: [PATCH 020/414] =?UTF-8?q?[Feat]=20Prisma=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20Message=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=BB=AC=EB=9F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20250109085641_fix_message_table/migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250109085641_fix_message_table/migration.sql diff --git a/prisma/migrations/20250109085641_fix_message_table/migration.sql b/prisma/migrations/20250109085641_fix_message_table/migration.sql new file mode 100644 index 0000000..6fdbbfb --- /dev/null +++ b/prisma/migrations/20250109085641_fix_message_table/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `message` on the `Message` table. All the data in the column will be lost. + - You are about to drop the column `photo_url` on the `Message` table. All the data in the column will be lost. + - Added the required column `content` to the `Message` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `message`, + DROP COLUMN `photo_url`, + ADD COLUMN `content` VARCHAR(191) NOT NULL, + ADD COLUMN `type` VARCHAR(191) NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1cb9e88..e63e746 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -330,8 +330,8 @@ model Message { id Int @id @default(autoincrement()) channel_id String user_id Int - photo_url String? - message String + type String + content String created_at DateTime @default(now()) user User @relation(fields: [user_id], references: [id]) channel Channel @relation(fields: [channel_id], references: [id]) From 215df01a19f9eb522a6de5a420ccdebfe4908ac2 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 18:08:28 +0900 Subject: [PATCH 021/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 23 +++++++++++++++++++++++ src/chat/chat.service.ts | 10 ++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index ed6e776..0ce4691 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -36,4 +36,27 @@ export class ChatGateway { // 채팅방 멤버 저장 await this.chatService.joinChannel(channelId, user.user_id); } + + // 메세지 송수신 + @SubscribeMessage('sendMessage') + async handleSendMessage( + @MessageBody() + data: { + type: string; + content: string; + user: string; + channelId: string; + } + ) { + // user 디코딩 + const user = this.jwtService.decode(data.user); + + // 메세지 데이터 저장 + await this.chatService.createMessage( + data.type, + data.channelId, + user.user_id, + data.content + ); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e49e513..6f67f6c 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -60,5 +60,15 @@ export class ChatService { }); } // 메세지 저장 + async createMessage(type, channelId, userId, content) { + await this.prisma.message.create({ + data: { + type, + content, + channel_id: channelId, + user_id: userId, + }, + }); + } // 메세지 상태 업데이트 } From 5a58bcd22230f3ae9dd3275d370e00594e099d93 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 18:42:52 +0900 Subject: [PATCH 022/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B8=20=EC=9C=A0=EC=A0=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 17 ++++++++++------- src/chat/chat.service.ts | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 0ce4691..20982ee 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -24,17 +24,14 @@ export class ChatGateway { const user = this.jwtService.decode(userId); // 존재하는 채팅방인지 확인 - const existData = await this.chatService.channelExist( - channelId, - user.user_id - ); + const existData = await this.chatService.channelExist(channelId, user.id); if (existData) return; // 채팅방 생성 await this.chatService.createChannel(channelId); // 채팅방 멤버 저장 - await this.chatService.joinChannel(channelId, user.user_id); + await this.chatService.joinChannel(channelId, user.id); } // 메세지 송수신 @@ -44,7 +41,7 @@ export class ChatGateway { data: { type: string; content: string; - user: string; + user: any; channelId: string; } ) { @@ -55,8 +52,14 @@ export class ChatGateway { await this.chatService.createMessage( data.type, data.channelId, - user.user_id, + user.id, data.content ); + + // 유저 정보 추가 + const userData = await this.chatService.getSenderProfile(user.id); + data.user = userData; + const createdAt = new Date(); + const sendData = { ...data, createdAt }; } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 6f67f6c..41476a9 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -70,5 +70,23 @@ export class ChatService { }, }); } + + // 유저 정보 확인 + async getSenderProfile(userId) { + const data = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + id: true, + email: true, + name: true, + nickname: true, + role_id: true, + profile_url: true, + }, + }); + return data; + } // 메세지 상태 업데이트 } From 36892dc3b46355cbe73d651601f3bd0048ba05de Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 18:49:43 +0900 Subject: [PATCH 023/414] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=BC=93=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 20982ee..f23d5f6 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -2,17 +2,19 @@ import { SubscribeMessage, WebSocketGateway, MessageBody, + WebSocketServer, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; import { JwtService } from '@nestjs/jwt'; +import { Server } from 'socket.io'; -@WebSocketGateway() +@WebSocketGateway({ cors: { origin: '*' } }) export class ChatGateway { constructor( private readonly chatService: ChatService, private readonly jwtService: JwtService ) {} - + @WebSocketServer() server: Server; // 채팅방 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( @@ -61,5 +63,7 @@ export class ChatGateway { data.user = userData; const createdAt = new Date(); const sendData = { ...data, createdAt }; + + this.server.to(data.channelId).emit('message', sendData); } } From c2b066788b51e073373fa370a6b5b171ae32cbfa Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 19:01:09 +0900 Subject: [PATCH 024/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EC=8B=9C=20=EC=86=8C=EC=BC=93=EB=A3=B8?= =?UTF-8?q?=EC=97=90=20join?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index f23d5f6..1b90e3b 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -3,10 +3,11 @@ import { WebSocketGateway, MessageBody, WebSocketServer, + ConnectedSocket, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; import { JwtService } from '@nestjs/jwt'; -import { Server } from 'socket.io'; +import { Server, Socket } from 'socket.io'; @WebSocketGateway({ cors: { origin: '*' } }) export class ChatGateway { @@ -18,7 +19,8 @@ export class ChatGateway { // 채팅방 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( - @MessageBody() data: { channelId: string; userId: string } + @MessageBody() data: { channelId: string; userId: string }, + @ConnectedSocket() client: Socket ) { const { channelId, userId } = data; @@ -27,11 +29,18 @@ export class ChatGateway { // 존재하는 채팅방인지 확인 const existData = await this.chatService.channelExist(channelId, user.id); - if (existData) return; + if (existData) { + // 채팅방 참여 + client.join(channelId); + return existData; + } // 채팅방 생성 await this.chatService.createChannel(channelId); + // 채팅방 참여 + client.join(channelId); + // 채팅방 멤버 저장 await this.chatService.joinChannel(channelId, user.id); } From 2260bceea0b2a977f9b267dbff9af3bbcc61e89b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 19:12:01 +0900 Subject: [PATCH 025/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20namespace=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 1b90e3b..28962cb 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -9,7 +9,7 @@ import { ChatService } from './chat.service'; import { JwtService } from '@nestjs/jwt'; import { Server, Socket } from 'socket.io'; -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) export class ChatGateway { constructor( private readonly chatService: ChatService, From b8fb9b85bc8878845ca3ae628b203d9e716c0793 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 9 Jan 2025 23:19:26 +0900 Subject: [PATCH 026/414] =?UTF-8?q?[Fix]=20=EC=8B=A4=ED=96=89=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ src/chat/chat.module.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 29b5416..52daeaa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ ConfigModule.forRoot({ @@ -12,6 +13,7 @@ import { ChatModule } from './chat/chat.module'; AuthModule, // Auth 모듈 추가 UserModule, ChatModule, + JwtModule, ], providers: [ChatGateway], }) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 1e54921..5a50b92 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -6,5 +6,6 @@ import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [PrismaModule, JwtModule], providers: [ChatService], + exports: [ChatService], }) export class ChatModule {} From 0342bad88c1db9de5f4f83aea4f22e08f7785d89 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 16:35:34 +0900 Subject: [PATCH 027/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 27 ++++---------- src/chat/chat.service.ts | 79 +++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 65 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 28962cb..984a883 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -19,32 +19,19 @@ export class ChatGateway { // 채팅방 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( - @MessageBody() data: { channelId: string; userId: string }, + @MessageBody() data: { userId1: number; userId2: number }, @ConnectedSocket() client: Socket ) { - const { channelId, userId } = data; + const { userId1, userId2 } = data; - // userId JWT 토큰 값이라 디코딩 해야함 - const user = this.jwtService.decode(userId); + // 채널 id 조회 + const channelId = await this.chatService.getChannelId(userId1, userId2); - // 존재하는 채팅방인지 확인 - const existData = await this.chatService.channelExist(channelId, user.id); - if (existData) { - // 채팅방 참여 - client.join(channelId); - return existData; - } - - // 채팅방 생성 - await this.chatService.createChannel(channelId); + client.to(userId1.toString()).socketsJoin(channelId); + client.to(userId2.toString()).socketsJoin(channelId); - // 채팅방 참여 - client.join(channelId); - - // 채팅방 멤버 저장 - await this.chatService.joinChannel(channelId, user.id); + this.server.emit('channelId', channelId); } - // 메세지 송수신 @SubscribeMessage('sendMessage') async handleSendMessage( diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 41476a9..73547fd 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -5,60 +5,48 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class ChatService { constructor(private readonly prisma: PrismaService) {} - // 이미 존재하는 채팅방인지 확인 - // 없다면 생성, 있다면 매핑 테이블 확인 - // 매핑 테이블 유저 데이터 있다면 대화 내용 불러오기, 없다면 매핑 테이블에 유저 데이터 생성 - async channelExist(channelId, userId) { - const exist = await this.prisma.channel.count({ - where: { id: channelId }, - }); - - // 존재하지 않으면 함수 종료 후 컨트롤러에서 채팅방 생성 로직 재개 - if (!exist) return false; - - // 매핑 테이블 channel_users에 유저 데이터 있는지 확인 - const userExist = await this.prisma.channel_users.findFirst({ + // 채널 id를 리턴하는 로직 + async getChannelId(userId1, userId2) { + // 매핑 테이블에서 파라미터로 전달된 유저 아이디에 해당하는 데이터 찾기 + const result = await this.prisma.channel_users.groupBy({ + by: ['channel_id'], where: { - channel_id: channelId, - user_id: userId, + user_id: { + in: [userId1, userId2], + }, + }, + _count: { + user_id: true, }, }); - // 있다면 대화내용 불러오기 - if (userExist) { - const message = await this.prisma.message.findMany({ - where: { - channel_id: channelId, + // user_id == 2 -> 두 유저가 모두 참여한 채널 필터링 + const channel = result.filter(data => data._count.user_id == 2)[0]; + + // 참여한 채널이 있다면 채널 id 리턴 + if (channel) return channel.channel_id; + + // 없다면 새로운 채널 생성 후 + const newChannel = await this.prisma.channel.create({}); + + // 매핑 테이블에 데이터 저장 + await this.prisma.channel_users.createMany({ + data: [ + { + channel_id: newChannel.id, + user_id: userId1, }, - }); - return message; - } else { - // 없다면 매핑 테이블에 유저 데이터 추가 - await this.prisma.channel_users.create({ - data: { - channel_id: channelId, - user_id: userId, + { + channel_id: newChannel.id, + user_id: userId2, }, - }); - const notice = `${userId}님이 채팅방에 참가했습니다`; - return { notice }; - } - } + ], + }); - // 채팅방 생성 - async createChannel(id) { - await this.prisma.channel.create({ data: { id } }); + // 채널 id 리턴 + return newChannel.id; } - // 채팅방 멤버 저장 - async joinChannel(channelId, userId) { - await this.prisma.channel_users.create({ - data: { - channel_id: channelId, - user_id: userId, - }, - }); - } // 메세지 저장 async createMessage(type, channelId, userId, content) { await this.prisma.message.create({ @@ -84,6 +72,7 @@ export class ChatService { nickname: true, role_id: true, profile_url: true, + //authprovider 추가 }, }); return data; From 537e9796d29d009c1417d8dccfdc95b8c1683736 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 16:38:06 +0900 Subject: [PATCH 028/414] =?UTF-8?q?[Docs]=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 984a883..f44bb7a 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -27,9 +27,11 @@ export class ChatGateway { // 채널 id 조회 const channelId = await this.chatService.getChannelId(userId1, userId2); + // 채널에 유저 참여 client.to(userId1.toString()).socketsJoin(channelId); client.to(userId2.toString()).socketsJoin(channelId); + // 클라이언트에 채널id 전달 this.server.emit('channelId', channelId); } // 메세지 송수신 From f5917a967295aa78c701f7e15d6f066cd5739b2e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 16:41:45 +0900 Subject: [PATCH 029/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20emit=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=96=91=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index f44bb7a..dc9e5f9 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -32,7 +32,7 @@ export class ChatGateway { client.to(userId2.toString()).socketsJoin(channelId); // 클라이언트에 채널id 전달 - this.server.emit('channelId', channelId); + this.server.emit('channelJoined', { channelId }); } // 메세지 송수신 @SubscribeMessage('sendMessage') From 0619ba5c9088125ce65ec012261bdf5a4aee4cb4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 16:49:01 +0900 Subject: [PATCH 030/414] =?UTF-8?q?[Feat]=20Prisma=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 37 +++++++++++++++++++ prisma/schema.prisma | 6 +-- 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20250110074825_chat_logic_fixed/migration.sql diff --git a/prisma/migrations/20250110074825_chat_logic_fixed/migration.sql b/prisma/migrations/20250110074825_chat_logic_fixed/migration.sql new file mode 100644 index 0000000..33b5ff3 --- /dev/null +++ b/prisma/migrations/20250110074825_chat_logic_fixed/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - The primary key for the `Channel` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `Channel` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Int`. + - You are about to alter the column `channel_id` on the `Channel_users` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Int`. + - You are about to alter the column `channel_id` on the `Message` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Int`. + +*/ +-- DropForeignKey +ALTER TABLE `Channel_users` DROP FOREIGN KEY `Channel_users_channel_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Message` DROP FOREIGN KEY `Message_channel_id_fkey`; + +-- DropIndex +DROP INDEX `Channel_users_channel_id_fkey` ON `Channel_users`; + +-- DropIndex +DROP INDEX `Message_channel_id_fkey` ON `Message`; + +-- AlterTable +ALTER TABLE `Channel` DROP PRIMARY KEY, + MODIFY `id` INTEGER NOT NULL AUTO_INCREMENT, + ADD PRIMARY KEY (`id`); + +-- AlterTable +ALTER TABLE `Channel_users` MODIFY `channel_id` INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE `Message` MODIFY `channel_id` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e63e746..4c5e917 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -309,7 +309,7 @@ model UserApplyProject { } model Channel { - id String @id + id Int @id @default(autoincrement()) name String @default("default_channel_name") active Boolean @default(true) created_at DateTime @default(now()) @@ -319,7 +319,7 @@ model Channel { model Channel_users { id Int @id @default(autoincrement()) - channel_id String + channel_id Int user_id Int joined_at DateTime @default(now()) channel Channel @relation(fields: [channel_id], references: [id]) @@ -328,7 +328,7 @@ model Channel_users { model Message { id Int @id @default(autoincrement()) - channel_id String + channel_id Int user_id Int type String content String From 314b2af5ab36955099014763f144463e7b455ba7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 17:04:42 +0900 Subject: [PATCH 031/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=86=A1=EC=88=98=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 16 ++++++++-------- src/chat/chat.service.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index dc9e5f9..46220ec 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -41,26 +41,26 @@ export class ChatGateway { data: { type: string; content: string; - user: any; + userId: number; channelId: string; } ) { - // user 디코딩 - const user = this.jwtService.decode(data.user); + const { userId, ...resData } = data; // 메세지 데이터 저장 await this.chatService.createMessage( data.type, data.channelId, - user.id, + userId, data.content ); // 유저 정보 추가 - const userData = await this.chatService.getSenderProfile(user.id); - data.user = userData; - const createdAt = new Date(); - const sendData = { ...data, createdAt }; + const user = await this.chatService.getSenderProfile(userId); + const date = new Date(); + + // 전달 데이터 양식 + const sendData = { ...resData, user, date }; this.server.to(data.channelId).emit('message', sendData); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 73547fd..c1e50c9 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -72,7 +72,7 @@ export class ChatService { nickname: true, role_id: true, profile_url: true, - //authprovider 추가 + auth_provider: true, }, }); return data; From b651e9a3e70ae945a98101601dd14b9988dee06b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 17:06:21 +0900 Subject: [PATCH 032/414] =?UTF-8?q?[Fix]=20channelId(=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=95=84=EC=9D=B4=EB=94=94)=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20number=20->=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 46220ec..ff67d6d 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -11,10 +11,7 @@ import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) export class ChatGateway { - constructor( - private readonly chatService: ChatService, - private readonly jwtService: JwtService - ) {} + constructor(private readonly chatService: ChatService) {} @WebSocketServer() server: Server; // 채팅방 참여 @SubscribeMessage('joinChannel') @@ -28,8 +25,8 @@ export class ChatGateway { const channelId = await this.chatService.getChannelId(userId1, userId2); // 채널에 유저 참여 - client.to(userId1.toString()).socketsJoin(channelId); - client.to(userId2.toString()).socketsJoin(channelId); + client.to(userId1.toString()).socketsJoin(channelId.toString()); + client.to(userId2.toString()).socketsJoin(channelId.toString()); // 클라이언트에 채널id 전달 this.server.emit('channelJoined', { channelId }); From faa48db8805b664d64b901491bad3276c2771760 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 10 Jan 2025 17:07:08 +0900 Subject: [PATCH 033/414] =?UTF-8?q?[Refactor]=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EB=AA=A8=EB=93=88=20=EC=A0=9C=EA=B1=B0=20(Jwt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 -- src/chat/chat.gateway.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 52daeaa..29b5416 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,7 +4,6 @@ import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; -import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ ConfigModule.forRoot({ @@ -13,7 +12,6 @@ import { JwtModule } from '@nestjs/jwt'; AuthModule, // Auth 모듈 추가 UserModule, ChatModule, - JwtModule, ], providers: [ChatGateway], }) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index ff67d6d..9c448fe 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -6,7 +6,6 @@ import { ConnectedSocket, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; -import { JwtService } from '@nestjs/jwt'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) From 84030a9e3cdef94c188fe757f30c9af1f408418b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 12:13:55 +0900 Subject: [PATCH 034/414] =?UTF-8?q?[Fix]=20Prisma=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 70bd639..0682094 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,9 +44,7 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - Channel_users Channel_users[] - Message Message[] - Message_status Message_status[] + @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") From 169a73c690983e946f0044f95e557dc6bc5a6ff8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 13:41:36 +0900 Subject: [PATCH 035/414] =?UTF-8?q?[Feat]=20chat=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.spec.ts | 18 ++++++++++++++++++ src/chat/chat.controller.ts | 4 ++++ src/chat/chat.module.ts | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 src/chat/chat.controller.spec.ts create mode 100644 src/chat/chat.controller.ts diff --git a/src/chat/chat.controller.spec.ts b/src/chat/chat.controller.spec.ts new file mode 100644 index 0000000..571463d --- /dev/null +++ b/src/chat/chat.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatController } from './chat.controller'; + +describe('ChatController', () => { + let controller: ChatController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ChatController], + }).compile(); + + controller = module.get(ChatController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts new file mode 100644 index 0000000..d71b71a --- /dev/null +++ b/src/chat/chat.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('chat') +export class ChatController {} diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 5a50b92..6eed9ed 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { ChatService } from './chat.service'; import { PrismaModule } from '@src/prisma/prisma.module'; import { JwtModule } from '@nestjs/jwt'; +import { ChatController } from './chat.controller'; @Module({ imports: [PrismaModule, JwtModule], providers: [ChatService], exports: [ChatService], + controllers: [ChatController], }) export class ChatModule {} From daf992e480fabc1e10a2d9474558b67d9c351338 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 15:30:44 +0900 Subject: [PATCH 036/414] =?UTF-8?q?[Feat]=20=EC=B0=B8=EC=97=AC=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20=EC=A0=84=EC=B2=B4=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 16 ++++++++++++++-- src/chat/chat.service.ts | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index d71b71a..5ced60e 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,4 +1,16 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { ChatService } from './chat.service'; +import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @Controller('chat') -export class ChatController {} +export class ChatController { + constructor(private readonly chatService: ChatService) {} + + // 유저가 참여한 채널 전체 조회 + @Get('channels') + @UseGuards(JwtAuthGuard) + async getAllChannels(@Req() req: any) { + const userId = +req.user.user_id; + return this.chatService.getAllChannels(userId); + } +} diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index c1e50c9..38396ef 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -5,6 +5,8 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class ChatService { constructor(private readonly prisma: PrismaService) {} + // ---- Socket ---- + // 채널 id를 리턴하는 로직 async getChannelId(userId1, userId2) { // 매핑 테이블에서 파라미터로 전달된 유저 아이디에 해당하는 데이터 찾기 @@ -78,4 +80,20 @@ export class ChatService { return data; } // 메세지 상태 업데이트 + + // ---- HTTP ---- + + // 전체 조회 + async getAllChannels(id: number) { + const result = await this.prisma.channel_users.findMany({ + where: { user_id: id }, + select: { channel_id: true }, + }); + + const data = result.map(channel => ({ + id: channel.channel_id, + })); + + return data; + } } From 017118474cf79843829bb0298438a03d68863b62 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 16:43:39 +0900 Subject: [PATCH 037/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 8 +++++++- src/chat/chat.service.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index 5ced60e..fbe5711 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @@ -13,4 +13,10 @@ export class ChatController { const userId = +req.user.user_id; return this.chatService.getAllChannels(userId); } + + @Get('channels/:id') + @UseGuards(JwtAuthGuard) + async getChannel(@Param('id') id: number) { + return this.chatService.getChannel(+id); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 38396ef..b6e6059 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -96,4 +96,38 @@ export class ChatService { return data; } + + async getChannel(channelId: number) { + try { + const result = await this.prisma.channel.findUnique({ + where: { id: channelId }, + include: { + Channel_users: { select: { user_id: true } }, + Message: { take: 1, orderBy: { created_at: 'desc' } }, + }, + }); + + const nickname = await this.prisma.user.findMany({ + where: { + id: { in: result.Channel_users.map(v => +v.user_id) }, + }, + select: { nickname: true }, + }); + + const userNicnames = nickname.map(result => result.nickname); + + console.log(nickname); + const data = { + id: channelId, + title: result.name, + userNicnames, + lastMessageTime: result.Message[0].created_at, + lastMessage: result.Message[0].content, + }; + + return data; + } catch (err) { + return err; + } + } } From 7fb6b4927f90395c472fda833cfb20750cd89286 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 16:44:09 +0900 Subject: [PATCH 038/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=B0=B8=EC=97=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 9c448fe..339cf50 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -24,12 +24,15 @@ export class ChatGateway { const channelId = await this.chatService.getChannelId(userId1, userId2); // 채널에 유저 참여 - client.to(userId1.toString()).socketsJoin(channelId.toString()); - client.to(userId2.toString()).socketsJoin(channelId.toString()); + client.join(channelId.toString()); + const sockets = await this.server.fetchSockets(); // 연결된 모든 소켓 가져오기 + const targetSocket = sockets.find(socket => socket.data.userId === userId2); + targetSocket.join(channelId.toString()); // 클라이언트에 채널id 전달 this.server.emit('channelJoined', { channelId }); } + // 메세지 송수신 @SubscribeMessage('sendMessage') async handleSendMessage( From 54157db4108fde0924fb7155f4b70f909366d492 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 13 Jan 2025 17:27:48 +0900 Subject: [PATCH 039/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EC=88=98=EC=A0=95,=20handleConnection=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 339cf50..60cae41 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -4,14 +4,22 @@ import { MessageBody, WebSocketServer, ConnectedSocket, + OnGatewayConnection, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) -export class ChatGateway { +export class ChatGateway implements OnGatewayConnection { constructor(private readonly chatService: ChatService) {} @WebSocketServer() server: Server; + + async handleConnection(client: Socket) { + const userId = client.handshake.query.userId; + client.data.userId = userId; + console.log(`User ${userId} connected`); + } + // 채팅방 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( @@ -19,6 +27,7 @@ export class ChatGateway { @ConnectedSocket() client: Socket ) { const { userId1, userId2 } = data; + client.data.userId = userId1; // 채널 id 조회 const channelId = await this.chatService.getChannelId(userId1, userId2); From 728d82db713eb6202c2a6ea31a2aa5a4f31b03a4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 12:51:47 +0900 Subject: [PATCH 040/414] =?UTF-8?q?[Feat]=20Prisma=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98;=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=98=A8=EB=9D=BC=EC=9D=B8=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20250114034707_online_users_table/migration.sql | 11 +++++++++++ prisma/schema.prisma | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250114034707_online_users_table/migration.sql diff --git a/prisma/migrations/20250114034707_online_users_table/migration.sql b/prisma/migrations/20250114034707_online_users_table/migration.sql new file mode 100644 index 0000000..6aab0ba --- /dev/null +++ b/prisma/migrations/20250114034707_online_users_table/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE `User` ADD COLUMN `password` VARCHAR(191) NULL; + +-- CreateTable +CREATE TABLE `online_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `client_id` VARCHAR(191) NOT NULL, + `user_id` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0682094..75e736a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -45,7 +45,6 @@ model User { UserLinks UserLink[] UserSkills UserSkill[] - @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") } @@ -357,3 +356,9 @@ model Message_status { @@index([message_id], map: "Message_status_message_id_fkey") @@index([user_id], map: "Message_status_user_id_fkey") } + +model online_users { + id Int @id @default(autoincrement()) + client_id String + user_id Int +} From 18abbdabe9483ba62bbc582287a7555ea9a817b0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 12:57:33 +0900 Subject: [PATCH 041/414] =?UTF-8?q?[Feat]=20=EC=98=A8=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20DB=EC=97=90=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index b6e6059..e7f88de 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -130,4 +130,14 @@ export class ChatService { return err; } } + + // 온라인 유저 DB에 저장 + async addUserOnline(userId, clientId) { + await this.prisma.online_users.create({ + data: { + user_id: userId, + client_id: clientId, + }, + }); + } } From 89b72b0ed59e2b93c76bff9a83a50094e367c94f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 12:58:48 +0900 Subject: [PATCH 042/414] =?UTF-8?q?[Refactor]=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=B0=B8=EC=97=AC=ED=95=9C=20=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 339cf50..4ce60fc 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -4,14 +4,34 @@ import { MessageBody, WebSocketServer, ConnectedSocket, + OnGatewayConnection, + OnGatewayDisconnect, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) -export class ChatGateway { +export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor(private readonly chatService: ChatService) {} + @WebSocketServer() server: Server; + + // 유저 소켓 접속 + async handleConnection(client: Socket) { + const userId = client.handshake.query.userId; + client.data.userId = userId; + + await this.chatService.addUserOnline(userId, client.id); + console.log(`User ${userId} connected with socket ID ${client.id}`); + } + + // 유저 소켓 접속 해제 + handleDisconnect(client: Socket) { + console.log('유저 연결 해제', client.data.userId); + // 유저가 연결 끊을 때, 온라인 유저 목록에서 삭제 + this.onlineUsers.delete(client.data.userId); + } + // 채팅방 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( @@ -25,12 +45,26 @@ export class ChatGateway { // 채널에 유저 참여 client.join(channelId.toString()); - const sockets = await this.server.fetchSockets(); // 연결된 모든 소켓 가져오기 - const targetSocket = sockets.find(socket => socket.data.userId === userId2); - targetSocket.join(channelId.toString()); + console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); + + // B 유저 온라인 여부 확인 + const targetSocket = this.onlineUsers.get(userId2.toString()); + if (targetSocket) { + // 온라인 + targetSocket.join(channelId.toString()); // B 유저도 채널에 입장 + targetSocket.emit('channelJoined', { channelId: channelId.toString() }); + console.log( + `client ${userId2} ${channelId}번 채팅방 입장 (온라인 바로 입장)` + ); + } else { + // 오프라인 + // 채팅 요청에 올려둠 + this.chatRequest.set(userId2.toString(), channelId.toString()); + console.log(`client ${userId2} 오프라인 `); + } // 클라이언트에 채널id 전달 - this.server.emit('channelJoined', { channelId }); + client.emit('channelJoined', { channelId: channelId.toString() }); } // 메세지 송수신 @@ -60,7 +94,7 @@ export class ChatGateway { // 전달 데이터 양식 const sendData = { ...resData, user, date }; - + console.log(sendData); this.server.to(data.channelId).emit('message', sendData); } } From cbee85055fed9597d477966bf049b08fece3b0af Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 13:21:28 +0900 Subject: [PATCH 043/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=98=A4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=EC=8B=9C=20DB=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e7f88de..cb836d8 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -83,18 +83,14 @@ export class ChatService { // ---- HTTP ---- - // 전체 조회 + // 유저가 참여한 채널 전체 조회 async getAllChannels(id: number) { const result = await this.prisma.channel_users.findMany({ where: { user_id: id }, select: { channel_id: true }, }); - const data = result.map(channel => ({ - id: channel.channel_id, - })); - - return data; + return result; } async getChannel(channelId: number) { @@ -140,4 +136,13 @@ export class ChatService { }, }); } + + // 오프라인 유저 DB에서 삭제 + async deleteUserOnline(userId: number) { + await this.prisma.online_users.deleteMany({ + where: { + user_id: userId, + }, + }); + } } From 8853b8c2348b8e3202e0e90b924d47b80c04e6f2 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 13:23:07 +0900 Subject: [PATCH 044/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=95=84=EC=9D=B4=EB=94=94=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index cb836d8..cbebee8 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -145,4 +145,14 @@ export class ChatService { }, }); } + + // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 + async getSocketId(userId: number) { + const socketId = await this.prisma.online_users.findMany({ + where: { + user_id: userId, + }, + }); + return socketId; + } } From 137dda024686e471f8cb97b6c0effd3efcdfa79b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 14:29:59 +0900 Subject: [PATCH 045/414] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=BC=93=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EB=94=94=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD;=20=EA=B0=9D=EC=B2=B4=20->=20client=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index cbebee8..e678e53 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -152,7 +152,10 @@ export class ChatService { where: { user_id: userId, }, + select: { + client_id: true, + }, }); - return socketId; + return socketId[0].client_id; } } From 77fff55b9f82da39802d7d6fdc2c89263106c449 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 14:39:34 +0900 Subject: [PATCH 046/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 73 +++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 4ce60fc..5cdb457 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -18,23 +18,34 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 유저 소켓 접속 async handleConnection(client: Socket) { - const userId = client.handshake.query.userId; - client.data.userId = userId; + const userId = +client.handshake.query.userId; + client.data.userId = userId; // userId 넘버로 저장 + // 유저 온라인 -> DB에 저장 await this.chatService.addUserOnline(userId, client.id); console.log(`User ${userId} connected with socket ID ${client.id}`); + + // 유저가 참여한 전체 채널 조회 + const channels = await this.chatService.getAllChannels(userId); + + // channel(유저가 참여한 전체 채널) 배열 형태로 전송 + client.emit('fetchChanneles', channels); } // 유저 소켓 접속 해제 - handleDisconnect(client: Socket) { - console.log('유저 연결 해제', client.data.userId); - // 유저가 연결 끊을 때, 온라인 유저 목록에서 삭제 - this.onlineUsers.delete(client.data.userId); + async handleDisconnect(client: Socket) { + // 유저 아이디 소켓 객체(client)에서 가져옴 + const userId = client.data.userId; + + // 온라인 여부 DB에서 삭제 + await this.chatService.deleteUserOnline(userId); + + console.log(`User ${userId} disconnected.`); } - // 채팅방 참여 - @SubscribeMessage('joinChannel') - async handleJoinChannel( + // 1대1 새 채팅방 생성 (userId1(클라이언트/본인), userId2(상대방)) + @SubscribeMessage('createChannel') + async handleCreateChannel( @MessageBody() data: { userId1: number; userId2: number }, @ConnectedSocket() client: Socket ) { @@ -43,28 +54,50 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 채널 id 조회 const channelId = await this.chatService.getChannelId(userId1, userId2); + // 채널 객체 + const channel = { channelId }; + // 채널에 유저 참여 client.join(channelId.toString()); console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); // B 유저 온라인 여부 확인 - const targetSocket = this.onlineUsers.get(userId2.toString()); + const targetSocket = await this.chatService.getSocketId(userId2); + + // 온라인 일때 if (targetSocket) { - // 온라인 - targetSocket.join(channelId.toString()); // B 유저도 채널에 입장 - targetSocket.emit('channelJoined', { channelId: channelId.toString() }); - console.log( - `client ${userId2} ${channelId}번 채팅방 입장 (온라인 바로 입장)` + // 유저2의 소켓 가져오기 + const user2Socket = this.server.sockets.sockets.get( + targetSocket.toString() ); + // 유저2의 채널 리스트에 해당 채널 추가 + user2Socket.emit('channelAdded', channel); + console.log(`channel ${channelId} added in ${userId2} channel list`); } else { - // 오프라인 - // 채팅 요청에 올려둠 - this.chatRequest.set(userId2.toString(), channelId.toString()); - console.log(`client ${userId2} 오프라인 `); + // 오프라인 일때 + console.log(`User ${userId2} is not connected.`); } // 클라이언트에 채널id 전달 - client.emit('channelJoined', { channelId: channelId.toString() }); + client.emit('channelJoined', channel); + } + + // 채널 참여 + @SubscribeMessage('joinChannel') + async handleJoinChannel( + @MessageBody() data: { userId: number; channelId: string }, + @ConnectedSocket() client: Socket + ) { + const { userId, channelId } = data; + + // 채널 참여 + client.join(channelId); + + // 채널 객체 + const channel = { channelId }; + + // 클라이언트에 채널id 전달 + client.emit('channelJoined', channel); } // 메세지 송수신 From 23ef31ac53f47d697712e4f14c11f5357f7bb722 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 16:17:39 +0900 Subject: [PATCH 047/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e678e53..7fa049f 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -63,7 +63,7 @@ export class ChatService { // 유저 정보 확인 async getSenderProfile(userId) { - const data = await this.prisma.user.findUnique({ + const result = await this.prisma.user.findUnique({ where: { id: userId, }, @@ -77,6 +77,8 @@ export class ChatService { auth_provider: true, }, }); + const { id, ...resData } = result; + const data = { user_id: id, ...resData }; return data; } // 메세지 상태 업데이트 From 5b5fbf101be4f6ddc163f9f5076f9fc55b3044ba Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 18:16:38 +0900 Subject: [PATCH 048/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20v3=20?= =?UTF-8?q?=EB=A7=88=EC=B9=A8=EB=82=B4=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 6b70ffa..b94e9a4 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -68,10 +68,13 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 온라인 일때 if (targetSocket) { // 유저2의 소켓 가져오기 - const user2Socket = this.server.sockets.sockets.get( - targetSocket.toString() + const sockets = await this.server.fetchSockets(); + const user2Socket = sockets.find( + socket => socket.id === targetSocket.toString() ); + // 유저2의 채널 리스트에 해당 채널 추가 + client.emit('channelAdded', channel); user2Socket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); } else { @@ -80,7 +83,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { } // 클라이언트에 채널id 전달 - client.emit('channelJoined', channel); + client.emit('channelCreated', channel); } // 채널 참여 @@ -92,8 +95,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const { userId, channelId } = data; // 채널 참여 - client.join(channelId); - + client.join(channelId.toString()); + console.log(`유저 ${userId} 채널 ${channelId} 참여`); // 채널 객체 const channel = { channelId }; From e0e86c10ab178c90667224fb4e1441eebe3fb4e8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 21:14:16 +0900 Subject: [PATCH 049/414] =?UTF-8?q?[Feat]=20Feed=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ src/feed/feed.controller.spec.ts | 20 ++++++++++++++++++++ src/feed/feed.controller.ts | 7 +++++++ src/feed/feed.module.ts | 9 +++++++++ src/feed/feed.service.spec.ts | 18 ++++++++++++++++++ src/feed/feed.service.ts | 4 ++++ 6 files changed, 60 insertions(+) create mode 100644 src/feed/feed.controller.spec.ts create mode 100644 src/feed/feed.controller.ts create mode 100644 src/feed/feed.module.ts create mode 100644 src/feed/feed.service.spec.ts create mode 100644 src/feed/feed.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 29b5416..a6a799f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; +import { FeedModule } from './feed/feed.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -12,6 +13,7 @@ import { ChatModule } from './chat/chat.module'; AuthModule, // Auth 모듈 추가 UserModule, ChatModule, + FeedModule, ], providers: [ChatGateway], }) diff --git a/src/feed/feed.controller.spec.ts b/src/feed/feed.controller.spec.ts new file mode 100644 index 0000000..d80197a --- /dev/null +++ b/src/feed/feed.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FeedController } from './feed.controller'; +import { FeedService } from './feed.service'; + +describe('FeedController', () => { + let controller: FeedController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FeedController], + providers: [FeedService], + }).compile(); + + controller = module.get(FeedController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts new file mode 100644 index 0000000..fbd09ba --- /dev/null +++ b/src/feed/feed.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { FeedService } from './feed.service'; + +@Controller('feed') +export class FeedController { + constructor(private readonly feedService: FeedService) {} +} diff --git a/src/feed/feed.module.ts b/src/feed/feed.module.ts new file mode 100644 index 0000000..962d476 --- /dev/null +++ b/src/feed/feed.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { FeedService } from './feed.service'; +import { FeedController } from './feed.controller'; + +@Module({ + controllers: [FeedController], + providers: [FeedService], +}) +export class FeedModule {} diff --git a/src/feed/feed.service.spec.ts b/src/feed/feed.service.spec.ts new file mode 100644 index 0000000..38beba7 --- /dev/null +++ b/src/feed/feed.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FeedService } from './feed.service'; + +describe('FeedService', () => { + let service: FeedService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FeedService], + }).compile(); + + service = module.get(FeedService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts new file mode 100644 index 0000000..6aa77a2 --- /dev/null +++ b/src/feed/feed.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FeedService {} From afb3dd1f6fcf1196a2ac1f92dcd7361861456e26 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 14 Jan 2025 21:15:53 +0900 Subject: [PATCH 050/414] =?UTF-8?q?[Feat]=20Prisma=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.module.ts | 2 ++ src/feed/feed.service.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.module.ts b/src/feed/feed.module.ts index 962d476..a63c411 100644 --- a/src/feed/feed.module.ts +++ b/src/feed/feed.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { FeedService } from './feed.service'; import { FeedController } from './feed.controller'; +import { PrismaModule } from '@src/prisma/prisma.module'; @Module({ + imports: [PrismaModule], controllers: [FeedController], providers: [FeedService], }) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 6aa77a2..4d20091 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -1,4 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() -export class FeedService {} +export class FeedService { + constructor(private readonly prisma: PrismaService) {} +} From 4c6473a1116b4e774e03942048487f7b3802fd51 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 14 Jan 2025 23:02:01 +0900 Subject: [PATCH 051/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B0=98=ED=99=98=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 12 +++++- src/modules/user/user.service.ts | 59 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 4bbfcb3..1711ffc 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,7 +1,17 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; +import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; +@UseGuards(JwtAuthGuard) @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} + + @Get(':userId') + async getUserProfile(@Param('userId') userId: string, @Req() req) { + const loggedInUserId = req.user?.user_id; + const numUserId = parseInt(userId); // 인증된 사용자 ID + console.log(loggedInUserId); + return this.userService.getUserProfile(loggedInUserId, numUserId); + } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 757992d..c1ed9e7 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,8 +1,63 @@ -import { Injectable } from '@nestjs/common'; - +import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} + + async getUserProfile(loggedInUserId: number, targetUserId: number) { + // 사용자 정보를 가져옴 + try { + const user = await this.prisma.user.findUnique({ + where: { id: targetUserId }, + include: { + role: true, // 역할 정보 (아티스트/프로그래머/디자이너) + ArtistData: true, // 아티스트 관련 데이터 + ProgrammerData: true, // 프로그래머 관련 데이터 + Resume: true, // 이력서 정보 + }, + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // 직업군에 따른 맞춤 정보 생성 + let specificData = null; + if (user.role.name === 'Artist') { + specificData = { + soundcloudUrl: user.ArtistData?.soundcloud_url, + portfolioUrl: user.ArtistData?.portfolio_url, + }; + } else if (user.role.name === 'Programmer') { + specificData = { + githubUsername: user.ProgrammerData?.github_username, + githubUrl: user.ProgrammerData?.github_url, + commitCount: user.ProgrammerData?.commit_count, + contributionData: user.ProgrammerData?.contribution_data, + }; + } + + // 로그인한 사용자와 조회 대상 사용자가 같은지 확인 + const isOwnProfile = loggedInUserId === targetUserId; + + // 사용자 정보 반환 + return { + id: user.id, + email: user.email, // 본인만 이메일 확인 가능 + nickname: user.nickname, + profileUrl: user.profile_url, + role: user.role.name, + introduce: user.introduce, + applyCount: user.apply_count, + postCount: user.post_count, + createdAt: user.created_at, + updatedAt: user.updated_at, + specificData, // 직업군에 따른 데이터 + isOwnProfile, // 자신의 프로필 여부 + }; + } catch (err) { + console.error(err); + } + } } From 7d4546f84cdbe0fb3842457cebc5d370028cfdff Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 14 Jan 2025 23:10:05 +0900 Subject: [PATCH 052/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0,=20=ED=8C=94=EB=A1=9C=EC=9B=8C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 10 +++++++++- src/modules/user/user.service.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 1711ffc..0c1ebff 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -7,11 +7,19 @@ import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; export class UserController { constructor(private readonly userService: UserService) {} - @Get(':userId') + @Get(':userId/profile') async getUserProfile(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID console.log(loggedInUserId); return this.userService.getUserProfile(loggedInUserId, numUserId); } + + @Get(':userId/follow-relations') + async getFollowRelations(@Param('userId') userId: string, @Req() req) { + const loggedInUserId = req.user?.user_id; + const numUserId = parseInt(userId); // 인증된 사용자 ID + console.log(loggedInUserId); + return this.userService.getFollowRelations(loggedInUserId, numUserId); + } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index c1ed9e7..e2b4ab4 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -60,4 +60,32 @@ export class UserService { console.error(err); } } + + async getFollowRelations(loggedInUserId: number, targetUserId: number) { + // 로그인한 사용자와 조회 대상 사용자가 같은지 확인 + const isOwnProfile = loggedInUserId === targetUserId; + const followers = await this.prisma.follows.findMany({ + where: { followed_user_id: targetUserId }, + include: { following_user: true }, + }); + + const followings = await this.prisma.follows.findMany({ + where: { following_user_id: targetUserId }, + include: { followed_user: true }, + }); + + return { + isOwnProfile, + followers: followers.map(f => ({ + id: f.following_user.id, + nickname: f.following_user.nickname, + profileUrl: f.following_user.profile_url, + })), + followings: followings.map(f => ({ + id: f.followed_user.id, + nickname: f.followed_user.nickname, + profileUrl: f.followed_user.profile_url, + })), + }; + } } From b707a4bb28b1b732e166e887b95bb2249d1b6c18 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 14:19:20 +0900 Subject: [PATCH 053/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 19 ++++++++++- src/chat/chat.service.ts | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index fbe5711..d8f6819 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @@ -14,6 +14,23 @@ export class ChatController { return this.chatService.getAllChannels(userId); } + // 채널 메세지 조회 + @Get('channels/:id/messages') + @UseGuards(JwtAuthGuard) + async getChannelsMessages( + @Req() req: any, + @Param('id') channelId: number, + @Query('limit') limit: number, + @Query('currentPage') currentPage: number + ) { + return await this.chatService.getMessages( + req.user.user_id, + +channelId, + +limit, + +currentPage + ); + } + @Get('channels/:id') @UseGuards(JwtAuthGuard) async getChannel(@Param('id') id: number) { diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7fa049f..5fa2e33 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -160,4 +160,71 @@ export class ChatService { }); return socketId[0].client_id; } + + // 채널 메세지 조회 + async getMessages(userId, channelId, limit, currentPage) { + try { + // 유저 아이디가 채널에 속해있는지 확인 + const auth = await this.prisma.channel_users.findMany({ + where: { + user_id: userId, + channel_id: channelId, + }, + }); + + // 아닐 시 예외처리 + if (!auth.length) { + throw new Error(); + } + + // 메세지 데이터 조회 + const result = await this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + role: true, + profile_url: true, + auth_provider: true, + }, + }, + }, + orderBy: { + id: 'desc', + }, + take: limit, + }); + + // 메세지 데이터 양식화 + const data = result.map(msg => ({ + id: msg.id, + type: msg.type, + content: msg.content, + channelId: msg.channel_id, + date: msg.created_at, + user: { + id: msg.user.id, + email: msg.user.email, + nickname: msg.user.nickname, + role: msg.user.role.name, + profileUrl: msg.user.profile_url, + }, + })); + // 페이지네이션 + const pagenation = { + totalMessageCount: data.length, + currentPage: currentPage, + }; + // 응답데이터 {메세지데이터, 페이지네이션} + return { messages: data, pagenation }; + } catch (err) { + return err; + } + } } From a4f6730bbaa4c9ff9ea67e85cb3e16b5057b08f9 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 15:39:14 +0900 Subject: [PATCH 054/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 4 +-- src/chat/chat.service.ts | 70 +++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index d8f6819..5b0a9f5 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -33,7 +33,7 @@ export class ChatController { @Get('channels/:id') @UseGuards(JwtAuthGuard) - async getChannel(@Param('id') id: number) { - return this.chatService.getChannel(+id); + async getChannel(@Req() req: any, @Param('id') channelId: number) { + return this.chatService.getChannel(req.user.user_id, +channelId); } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5fa2e33..8761c3a 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -95,35 +95,67 @@ export class ChatService { return result; } - async getChannel(channelId: number) { + // 채널 개별 조회 + async getChannel(userId: number, channelId: number) { try { - const result = await this.prisma.channel.findUnique({ - where: { id: channelId }, - include: { - Channel_users: { select: { user_id: true } }, - Message: { take: 1, orderBy: { created_at: 'desc' } }, + // 유저 아이디가 채널에 속해있는지 확인 + const auth = await this.prisma.channel_users.findMany({ + where: { + user_id: userId, + channel_id: channelId, }, }); - const nickname = await this.prisma.user.findMany({ - where: { - id: { in: result.Channel_users.map(v => +v.user_id) }, + // 아닐 시 예외처리 + if (!auth.length) { + throw new Error(); + } + + // 채널 데이터 조회 + const result = await this.prisma.channel.findUnique({ + where: { id: channelId }, + include: { + Channel_users: { + select: { + user: { + select: { nickname: true }, + }, + }, + }, + Message: { + take: 1, + orderBy: { id: 'desc' }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + profile_url: true, + auth_provider: true, + role_id: true, + }, + }, + }, + }, }, - select: { nickname: true }, }); - const userNicnames = nickname.map(result => result.nickname); - - console.log(nickname); - const data = { - id: channelId, + // 채널 데이터 양식화 + const channel = { + channelId: result.id, title: result.name, - userNicnames, - lastMessageTime: result.Message[0].created_at, - lastMessage: result.Message[0].content, + type: result.Channel_users.length > 2 ? 'group' : 'private', + users: result.Channel_users.map(v => v.user.nickname), + lastMessage: result.Message[0], }; - return data; + const message = { + code: 200, + text: '데이터 패칭 성공', + }; + return { channel, message }; } catch (err) { return err; } From 8325ea24728b9ade2dc869f2fbd59a159e5f4b09 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 15:40:54 +0900 Subject: [PATCH 055/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 8761c3a..0c4f33c 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -253,8 +253,14 @@ export class ChatService { totalMessageCount: data.length, currentPage: currentPage, }; + + const message = { + code: 200, + text: '데이터 패칭 성공', + }; + // 응답데이터 {메세지데이터, 페이지네이션} - return { messages: data, pagenation }; + return { messages: data, pagenation, message }; } catch (err) { return err; } From a86b6f2b75b64780cb6680e3e9b4a8689c80f33b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 15:46:00 +0900 Subject: [PATCH 056/414] =?UTF-8?q?[Feat]=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 0c4f33c..f477535 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -108,7 +108,7 @@ export class ChatService { // 아닐 시 예외처리 if (!auth.length) { - throw new Error(); + throw new Error('권한 X'); } // 채널 데이터 조회 @@ -157,7 +157,7 @@ export class ChatService { }; return { channel, message }; } catch (err) { - return err; + return err.message; } } @@ -206,7 +206,7 @@ export class ChatService { // 아닐 시 예외처리 if (!auth.length) { - throw new Error(); + throw new Error('권한 X'); } // 메세지 데이터 조회 @@ -262,7 +262,7 @@ export class ChatService { // 응답데이터 {메세지데이터, 페이지네이션} return { messages: data, pagenation, message }; } catch (err) { - return err; + return err.message; } } } From e029d5e8840057a943029a51ee6d72a366b0adc1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 16:19:22 +0900 Subject: [PATCH 057/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 7 ++++- src/feed/feed.service.ts | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index fbd09ba..4368dcf 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,7 +1,12 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { FeedService } from './feed.service'; @Controller('feed') export class FeedController { constructor(private readonly feedService: FeedService) {} + @Get() + // 메인 페이지 조회 + async getAllFeed() { + return this.feedService.getAllFeeds(); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 4d20091..c23a160 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -4,4 +4,55 @@ import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() export class FeedService { constructor(private readonly prisma: PrismaService) {} + /** 게시글 전체 조회 / 남은 구현 과제 + * 유저 토큰 제공 시 좋아요 여부 + * 인기순 정렬 + * 예외 처리 + **/ + async getAllFeeds() { + try { + const result = await this.prisma.feedPost.findMany({ + include: { + user: { + select: { + id: true, + name: true, + email: true, + role: { + select: { name: true }, + }, + profile_url: true, + }, + }, + Tags: { + select: { + tag: { + select: { name: true }, + }, + }, + }, + }, + }); + + const posts = result.map(post => ({ + userId: post.user.id, + userName: post.user.name, + userRole: post.user.role.name, + userProfileUrl: post.user.profile_url, + title: post.title, + postId: post.id, + thumnailUrl: post.thumbnail_url, + content: post.content, + tags: post.Tags.map(v => v.tag.name), + commentCount: post.comment_count, + likeCount: post.like_count, + viewCount: post.view, + createdAt: post.created_at, + })); + + return { posts }; + } catch (err) { + return err; + } + } } From a7eda467f6d3300f0fd283ded7d7e207c14ca257 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 16:29:04 +0900 Subject: [PATCH 058/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20(=EA=B2=8C=EC=8B=9C=EA=B8=80)=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 8 +++++- src/feed/feed.service.ts | 49 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 4368dcf..00c7817 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { FeedService } from './feed.service'; @Controller('feed') @@ -9,4 +9,10 @@ export class FeedController { async getAllFeed() { return this.feedService.getAllFeeds(); } + + // 피드 조회 + @Get(':id') + async getFeed(@Param('id') feedId: number) { + return await this.feedService.getFeed(+feedId); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index c23a160..5f03fa8 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -4,7 +4,7 @@ import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() export class FeedService { constructor(private readonly prisma: PrismaService) {} - /** 게시글 전체 조회 / 남은 구현 과제 + /** 피드 전체 조회 / 남은 구현 과제 * 유저 토큰 제공 시 좋아요 여부 * 인기순 정렬 * 예외 처리 @@ -55,4 +55,51 @@ export class FeedService { return err; } } + + /** 피드 조회 (게시글 부분) / 남은 구현 과제 + * 유저 토큰 제공 시 좋아요 여부 + * 예외 처리 + **/ + async getFeed(feedId) { + const result = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + role: { + select: { name: true }, + }, + profile_url: true, + }, + }, + Tags: { + select: { + tag: { + select: { name: true }, + }, + }, + }, + }, + }); + + const post = { + userId: result.user.id, + userName: result.user.name, + userRole: result.user.role.name, + userProfileUrl: result.user.profile_url, + title: result.title, + postId: result.id, + thumnailUrl: result.thumbnail_url, + content: result.content, + tags: result.Tags.map(v => v.tag.name), + commentCount: result.comment_count, + likeCount: result.like_count, + viewCount: result.view, + createdAt: result.created_at, + }; + return { post }; + } } From ea80e2fb43c6ed26fe46a8a3ee83b3f7670ea43f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 16:46:59 +0900 Subject: [PATCH 059/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20(=EB=8C=93=EA=B8=80)=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 7 ++++++- src/feed/feed.service.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 00c7817..a32aa85 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -10,9 +10,14 @@ export class FeedController { return this.feedService.getAllFeeds(); } - // 피드 조회 + // 피드 조회 (게시글) @Get(':id') async getFeed(@Param('id') feedId: number) { return await this.feedService.getFeed(+feedId); } + + @Get(':id/comments') + async getFeedComments(@Param('id') feedId: number) { + return await this.feedService.getFeedComments(+feedId); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 5f03fa8..bdfc057 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -102,4 +102,37 @@ export class FeedService { }; return { post }; } + + async getFeedComments(feedId) { + const result = await this.prisma.feedComment.findMany({ + where: { + post_id: feedId, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + role: { + select: { name: true }, + }, + profile_url: true, + }, + }, + }, + }); + + const comments = result.map(c => ({ + commentId: c.id, + userId: c.user.id, + userName: c.user.name, + userRole: c.user.role.name, + userProfileUrl: c.user.profile_url, + comment: c.content, + createdAt: c.created_at, + })); + + return { comments }; + } } From 07925a337aae3594d2ad8467d9738e49ce8def52 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 17:05:08 +0900 Subject: [PATCH 060/414] =?UTF-8?q?[Fix]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EC=B1=84=EB=84=90=20id=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20number=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index b94e9a4..c5c6914 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -89,7 +89,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 채널 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( - @MessageBody() data: { userId: number; channelId: string }, + @MessageBody() data: { userId: number; channelId: number }, @ConnectedSocket() client: Socket ) { const { userId, channelId } = data; @@ -112,7 +112,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { type: string; content: string; userId: number; - channelId: string; + channelId: number; } ) { const { userId, ...resData } = data; @@ -132,6 +132,6 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 전달 데이터 양식 const sendData = { ...resData, user, date }; console.log(sendData); - this.server.to(data.channelId).emit('message', sendData); + this.server.to(data.channelId.toString()).emit('message', sendData); } } From 8b25b9aa0c97b1016ecc4feb81f47e7deb33fb6b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 17:43:30 +0900 Subject: [PATCH 061/414] =?UTF-8?q?[Refactor]=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EC=9B=A8=EC=9D=B4=20sendMessage=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=8A=A4=EB=84=A4=EC=9D=B4=ED=81=AC=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20->=20=EC=B9=B4=EB=A9=9C=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 1 + src/chat/chat.service.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index c5c6914..4e36f8f 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -127,6 +127,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 유저 정보 추가 const user = await this.chatService.getSenderProfile(userId); + const date = new Date(); // 전달 데이터 양식 diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index f477535..7f3d8b6 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -77,8 +77,16 @@ export class ChatService { auth_provider: true, }, }); - const { id, ...resData } = result; - const data = { user_id: id, ...resData }; + + const data = { + userId: result.id, + email: result.email, + nickname: result.nickname, + profileUrl: result.profile_url, + authProvide: result.auth_provider, + roleId: result.role_id, + }; + return data; } // 메세지 상태 업데이트 From 1fd2633f7308b5a4af1cd3077944f8151a70cb2e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 18:02:52 +0900 Subject: [PATCH 062/414] =?UTF-8?q?[Fix]=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 4e36f8f..b4e2fca 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -29,7 +29,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channels = await this.chatService.getAllChannels(userId); // channel(유저가 참여한 전체 채널) 배열 형태로 전송 - client.emit('fetchChanneles', channels); + client.emit('fetchChannels', channels); } // 유저 소켓 접속 해제 From 17657eb73bef874344035b83aa44e8a587a69823 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 18:06:13 +0900 Subject: [PATCH 063/414] =?UTF-8?q?[Feat]=20=EC=8A=A4=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20->=20=EC=B9=B4?= =?UTF-8?q?=EB=A9=9C=EC=BC=80=EC=9D=B4=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7f3d8b6..7f19aa2 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -99,8 +99,11 @@ export class ChatService { where: { user_id: id }, select: { channel_id: true }, }); + const data = result.map(v => ({ + channelId: v.channel_id, + })); - return result; + return data; } // 채널 개별 조회 From f4274a1172263cf3ae16e92d0d9f21d84e743809 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 18:43:23 +0900 Subject: [PATCH 064/414] =?UTF-8?q?[Refactor]=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=B3=84=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 69 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7f19aa2..5741d1b 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -89,9 +89,38 @@ export class ChatService { return data; } - // 메세지 상태 업데이트 - // ---- HTTP ---- + // 온라인 유저 DB에 저장 + async addUserOnline(userId, clientId) { + await this.prisma.online_users.create({ + data: { + user_id: userId, + client_id: clientId, + }, + }); + } + + // 오프라인 유저 DB에서 삭제 + async deleteUserOnline(userId: number) { + await this.prisma.online_users.deleteMany({ + where: { + user_id: userId, + }, + }); + } + + // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 + async getSocketId(userId: number) { + const socketId = await this.prisma.online_users.findMany({ + where: { + user_id: userId, + }, + select: { + client_id: true, + }, + }); + return socketId[0].client_id; + } // 유저가 참여한 채널 전체 조회 async getAllChannels(id: number) { @@ -106,6 +135,10 @@ export class ChatService { return data; } + // 메세지 상태 업데이트 + + // ---- HTTP ---- + // 채널 개별 조회 async getChannel(userId: number, channelId: number) { try { @@ -172,38 +205,6 @@ export class ChatService { } } - // 온라인 유저 DB에 저장 - async addUserOnline(userId, clientId) { - await this.prisma.online_users.create({ - data: { - user_id: userId, - client_id: clientId, - }, - }); - } - - // 오프라인 유저 DB에서 삭제 - async deleteUserOnline(userId: number) { - await this.prisma.online_users.deleteMany({ - where: { - user_id: userId, - }, - }); - } - - // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 - async getSocketId(userId: number) { - const socketId = await this.prisma.online_users.findMany({ - where: { - user_id: userId, - }, - select: { - client_id: true, - }, - }); - return socketId[0].client_id; - } - // 채널 메세지 조회 async getMessages(userId, channelId, limit, currentPage) { try { From 60a606bd412b0e15dc73540f7fd12c2d603935b4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 18:53:28 +0900 Subject: [PATCH 065/414] =?UTF-8?q?[Fix]=20=EC=B1=84=EB=84=90=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20totalMessageCount=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 49 +++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5741d1b..7e9aab0 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -222,28 +222,35 @@ export class ChatService { } // 메세지 데이터 조회 - const result = await this.prisma.message.findMany({ - where: { - channel_id: channelId, - }, - include: { - user: { - select: { - id: true, - email: true, - name: true, - nickname: true, - role: true, - profile_url: true, - auth_provider: true, + const [result, totalMessageCount] = await Promise.all([ + this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + role: true, + profile_url: true, + auth_provider: true, + }, }, }, - }, - orderBy: { - id: 'desc', - }, - take: limit, - }); + orderBy: { + id: 'desc', + }, + take: limit, + }), + this.prisma.message.count({ + where: { + channel_id: channelId, + }, + }), + ]); // 메세지 데이터 양식화 const data = result.map(msg => ({ @@ -262,7 +269,7 @@ export class ChatService { })); // 페이지네이션 const pagenation = { - totalMessageCount: data.length, + totalMessageCount, currentPage: currentPage, }; From 6f46d3f1df87ec543650e7fccbd34732354131d6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 20:08:18 +0900 Subject: [PATCH 066/414] [Feat] Prisma db push --- prisma/schema.prisma | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0682094..5f136d7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,6 @@ model User { email String @unique name String nickname String - password String? auth_provider String profile_url String? role_id Int @@ -25,6 +24,7 @@ model User { project_alert Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime @updatedAt + password String? ArtistData ArtistData? Channel_users Channel_users[] FeedComments FeedComment[] @@ -44,7 +44,7 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - + FeedCommentLikes FeedCommentLikes[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -196,20 +196,29 @@ model FeedPostTag { } model FeedComment { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - content String - image_url String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - post FeedPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + content String + image_url String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + post FeedPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + FeedCommentLikes FeedCommentLikes[] @@index([post_id], map: "FeedComment_post_id_fkey") @@index([user_id], map: "FeedComment_user_id_fkey") } +model FeedCommentLikes { + id Int @id @default(autoincrement()) + user_id Int + user User @relation(fields: [user_id], references: [id]) + comment_id Int + comment FeedComment @relation(fields: [comment_id], references: [id]) +} + model FeedLike { id Int @id @default(autoincrement()) user_id Int @@ -357,3 +366,9 @@ model Message_status { @@index([message_id], map: "Message_status_message_id_fkey") @@index([user_id], map: "Message_status_user_id_fkey") } + +model online_users { + id Int @id @default(autoincrement()) + client_id String + user_id Int +} From 39fffeeaaa9c55fe7fb351b28217931b90169f5c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:12:23 +0900 Subject: [PATCH 067/414] =?UTF-8?q?[Fix]=20DB=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=A0=90=20Pull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0682094..6ae23ed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,6 @@ model User { email String @unique name String nickname String - password String? auth_provider String profile_url String? role_id Int @@ -25,9 +24,11 @@ model User { project_alert Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime @updatedAt + password String? ArtistData ArtistData? Channel_users Channel_users[] FeedComments FeedComment[] + FeedCommentLikes FeedCommentLikes[] FeedLikes FeedLike[] FeedPosts FeedPost[] Followed Follows[] @relation("FollowedUsers") @@ -45,7 +46,6 @@ model User { UserLinks UserLink[] UserSkills UserSkill[] - @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") } @@ -196,15 +196,16 @@ model FeedPostTag { } model FeedComment { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - content String - image_url String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - post FeedPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + content String + image_url String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + post FeedPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + FeedCommentLikes FeedCommentLikes[] @@index([post_id], map: "FeedComment_post_id_fkey") @@index([user_id], map: "FeedComment_user_id_fkey") @@ -357,3 +358,20 @@ model Message_status { @@index([message_id], map: "Message_status_message_id_fkey") @@index([user_id], map: "Message_status_user_id_fkey") } + +model FeedCommentLikes { + id Int @id @default(autoincrement()) + user_id Int + comment_id Int + FeedComment FeedComment @relation(fields: [comment_id], references: [id]) + User User @relation(fields: [user_id], references: [id]) + + @@index([comment_id], map: "FeedCommentLikes_comment_id_fkey") + @@index([user_id], map: "FeedCommentLikes_user_id_fkey") +} + +model online_users { + id Int @id @default(autoincrement()) + client_id String + user_id Int +} From 33e2aae3ce57027a9a588b811ca72684eb975c06 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:12:41 +0900 Subject: [PATCH 068/414] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=84=A4=EC=A0=95=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 0c1ebff..327aff8 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,4 +1,13 @@ -import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Patch, + Req, + Res, + UseGuards, +} from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; @@ -22,4 +31,22 @@ export class UserController { console.log(loggedInUserId); return this.userService.getFollowRelations(loggedInUserId, numUserId); } + + @Get('setting') + async getUserSetting(@Req() req) { + const userId = req.user?.user_id; + return this.userService.getUserSetting(userId); + } + + @Patch('profile/nickname') + async patchUserNickname(@Req() req, @Body('nickname') nickname: string) { + const userId = req.user?.user_id; + return this.userService.patchUserNickname(userId, nickname); + } + + @Patch('profile/introduce') + async patchUserIntroduce(@Req() req, @Body('introduce') introduce: string) { + const userId = req.user?.user_id; + return this.userService.patchUserIntroduce(userId, introduce); + } } From a5c641fb7402d73d8f4dc15d52c660253ef92df4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:14:11 +0900 Subject: [PATCH 069/414] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=84=A4=EC=A0=95=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index e2b4ab4..617118e 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -88,4 +88,16 @@ export class UserService { })), }; } + + async patchUserNickname(userId: number, nickname: string) { + return Promise.resolve(undefined); + } + + async getUserSetting(userId: number) { + return Promise.resolve(undefined); + } + + async patchUserIntroduce(userId: number, introduce: string) { + return Promise.resolve(undefined); + } } From 174e0c10ea336cdc42bb534a423050d6531023db Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:21:20 +0900 Subject: [PATCH 070/414] =?UTF-8?q?[Refactor]=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=B9=B4=EB=A9=9C?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.controller.ts | 9 ++------- src/modules/auth/auth.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 945ded4..39416b3 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -74,15 +74,10 @@ export class AuthController { return res.status(HttpStatusCodes.OK).json(response); } - // @Post('login') - // async login( - // @Body() - // ) - // Role 선택 API @Put('roleselect') @UseGuards(JwtAuthGuard) async selectRole( - @Body('role_id') roleId: number, + @Body('roleId') roleId: number, @Req() req: any, @Res() res: Response ) { @@ -102,7 +97,7 @@ export class AuthController { @Post('refresh') async refreshAccessToken( - @Body('user_id') userId: number, // user_id는 숫자로 받음 + @Body('userId') userId: number, // user_id는 숫자로 받음 @Res() res: Response ) { if (userId === undefined || userId === null) { diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 01a067d..41eed42 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -203,7 +203,7 @@ export class AuthService { }); return { - user_id: newUser.id, + userId: newUser.id, email: newUser.email, nickname: newUser.nickname, }; @@ -236,7 +236,7 @@ export class AuthService { } private filterUserFields(user: any) { return { - user_id: user.id, + userId: user.id, email: user.email, name: user.name, nickname: user.nickname, From 9136bcc8bd6dd1a3165c4df3005443b93ebcaf60 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:27:08 +0900 Subject: [PATCH 071/414] =?UTF-8?q?[Fix]=20db=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 75e736a..bf519ca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,6 @@ model User { email String @unique name String nickname String - password String? auth_provider String profile_url String? role_id Int @@ -25,9 +24,11 @@ model User { project_alert Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime @updatedAt + password String? ArtistData ArtistData? Channel_users Channel_users[] FeedComments FeedComment[] + FeedCommentLikes FeedCommentLikes[] FeedLikes FeedLike[] FeedPosts FeedPost[] Followed Follows[] @relation("FollowedUsers") @@ -195,15 +196,16 @@ model FeedPostTag { } model FeedComment { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - content String - image_url String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - post FeedPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + content String + image_url String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + post FeedPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + FeedCommentLikes FeedCommentLikes[] @@index([post_id], map: "FeedComment_post_id_fkey") @@index([user_id], map: "FeedComment_user_id_fkey") @@ -362,3 +364,14 @@ model online_users { client_id String user_id Int } + +model FeedCommentLikes { + id Int @id @default(autoincrement()) + user_id Int + comment_id Int + FeedComment FeedComment @relation(fields: [comment_id], references: [id]) + User User @relation(fields: [user_id], references: [id]) + + @@index([comment_id], map: "FeedCommentLikes_comment_id_fkey") + @@index([user_id], map: "FeedCommentLikes_user_id_fkey") +} From 008a5b71fccb438ff79b50f5c342b83bdee93287 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:49:51 +0900 Subject: [PATCH 072/414] =?UTF-8?q?[Fix]=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=B9=B4=EB=A9=9C=EC=BC=80=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 41eed42..01dd5ab 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -240,9 +240,9 @@ export class AuthService { email: user.email, name: user.name, nickname: user.nickname, - profile_url: user.profile_url, - auth_provider: user.auth_provider, - role_id: user.role_id, + profileUrl: user.profile_url, + authProvider: user.auth_provider, + roleId: user.role_id, }; } From 62e61488caf52315502f17fe02376d3d2844493a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 15 Jan 2025 21:57:12 +0900 Subject: [PATCH 073/414] =?UTF-8?q?[Fix]=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=B9=B4=EB=A9=9C=EC=BC=80=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 39416b3..e665012 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -114,7 +114,7 @@ export class AuthController { code: 200, text: 'Access token이 성공적으로 갱신 되었습니다.', }, - access_token: newAccessToken, + accessToken: newAccessToken, }; return res.status(200).json(response); @@ -154,7 +154,7 @@ export class AuthController { return { message: 'Login successful', user: result.user, - access_token: result.accessToken, + accessToken: result.accessToken, }; } } From cd8e6f61b69c4ba3bc309404b02cb627826be74e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 15 Jan 2025 22:18:17 +0900 Subject: [PATCH 074/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EC=9D=B8=EA=B8=B0=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index bdfc057..bb016a5 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -6,7 +6,6 @@ export class FeedService { constructor(private readonly prisma: PrismaService) {} /** 피드 전체 조회 / 남은 구현 과제 * 유저 토큰 제공 시 좋아요 여부 - * 인기순 정렬 * 예외 처리 **/ async getAllFeeds() { @@ -32,6 +31,8 @@ export class FeedService { }, }, }, + // 인기순 정렬 : 좋아요 순 + orderBy: { like_count: 'desc' }, }); const posts = result.map(post => ({ From e4062838845855d070720508fc2e8cd8b3b1e872 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 14:25:14 +0900 Subject: [PATCH 075/414] =?UTF-8?q?[Feat]=20OptionalAuthGaurd=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 8 +++++--- src/modules/auth/guards/optional-auth.guard.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/modules/auth/guards/optional-auth.guard.ts diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index a32aa85..0b45cd8 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,12 +1,14 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import { FeedService } from './feed.service'; +import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; @Controller('feed') export class FeedController { constructor(private readonly feedService: FeedService) {} - @Get() // 메인 페이지 조회 - async getAllFeed() { + @Get() + @UseGuards(OptionalAuthGuard) + async getAllFeed(@Req() req) { return this.feedService.getAllFeeds(); } diff --git a/src/modules/auth/guards/optional-auth.guard.ts b/src/modules/auth/guards/optional-auth.guard.ts new file mode 100644 index 0000000..ef430c2 --- /dev/null +++ b/src/modules/auth/guards/optional-auth.guard.ts @@ -0,0 +1,10 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class OptionalAuthGuard extends AuthGuard('jwt') { + handleRequest(err, user, info) { + // 에러가 있거나 사용자가 없을 경우에도 통과 + return user || null; + } +} From 8c4f95e2d4db85680aedd0dc89110ec6e461016d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 14:45:41 +0900 Subject: [PATCH 076/414] =?UTF-8?q?[Feat]=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.ts b/src/main.ts index 0f27b22..07324d4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; import * as cookieParser from 'cookie-parser'; +import { ValidationPipe } from '@nestjs/common'; config(); async function bootstrap() { @@ -13,6 +14,15 @@ async function bootstrap() { credentials: true, exposedHeaders: ['Authorization'], }); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }) + ); + app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); } From f9b9b594670f8dd88b14b7a2eb4bb38c8480ad01 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 15:11:30 +0900 Subject: [PATCH 077/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=B6=94=EA=B0=80=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 12 +++++++++- src/feed/feed.service.ts | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 0b45cd8..140d004 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; import { FeedService } from './feed.service'; import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; +import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @Controller('feed') export class FeedController { @@ -18,8 +19,17 @@ export class FeedController { return await this.feedService.getFeed(+feedId); } + // 피드 조회 (댓글) @Get(':id/comments') async getFeedComments(@Param('id') feedId: number) { return await this.feedService.getFeedComments(+feedId); } + + // 좋아요 추가/ 제거 + @Post(':id/likes') + @UseGuards(JwtAuthGuard) + async handleFeedLikes(@Req() req, @Param('id') feedId: number) { + const userId = req.user.user_id; + return await this.feedService.handlePostLikes(feedId, userId); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index bb016a5..816512c 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -136,4 +136,48 @@ export class FeedService { return { comments }; } + + // 게시글 좋아요 추가/제거 + async handlePostLikes(feedId, userId) { + try { + const exist = await this.prisma.feedLike.findMany({ + where: { + post_id: feedId, + user_id: userId, + }, + }); + + if (exist.length) { + await this.prisma.feedLike.deleteMany({ + where: { + post_id: feedId, + user_id: userId, + }, + }); + + await this.prisma.feedPost.update({ + where: { id: feedId }, + data: { like_count: { decrement: 1 } }, + }); + + return { message: '좋아요 취소 ' }; + } else { + await this.prisma.feedLike.create({ + data: { + post_id: feedId, + user_id: userId, + }, + }); + + await this.prisma.feedPost.update({ + where: { id: feedId }, + data: { like_count: { increment: 1 } }, + }); + + return { message: '좋아요 추가 ' }; + } + } catch (err) { + return err; + } + } } From 73f4e22bcb7b8bee3bd45701a7dc2643ed939860 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 15:23:52 +0900 Subject: [PATCH 078/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 2 +- src/feed/feed.service.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 140d004..f048658 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -10,7 +10,7 @@ export class FeedController { @Get() @UseGuards(OptionalAuthGuard) async getAllFeed(@Req() req) { - return this.feedService.getAllFeeds(); + return this.feedService.getAllFeeds(req.user); } // 피드 조회 (게시글) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 816512c..28eb590 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -5,13 +5,18 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class FeedService { constructor(private readonly prisma: PrismaService) {} /** 피드 전체 조회 / 남은 구현 과제 - * 유저 토큰 제공 시 좋아요 여부 + * 썸네일 뽑기 * 예외 처리 **/ - async getAllFeeds() { + async getAllFeeds(user) { try { + const userId = user ? user.user_id : 0; + const result = await this.prisma.feedPost.findMany({ include: { + Likes: { + where: { user_id: userId }, + }, user: { select: { id: true, @@ -49,9 +54,11 @@ export class FeedService { likeCount: post.like_count, viewCount: post.view, createdAt: post.created_at, + liked: !!post.Likes.length, })); return { posts }; + // return result; } catch (err) { return err; } From cad5e44dc86048cef997ba53fd0b3788d47d2e84 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 15:24:16 +0900 Subject: [PATCH 079/414] =?UTF-8?q?[Refactor]=20=EA=B8=80=EB=A1=9C?= =?UTF-8?q?=EB=B2=8C=20=ED=8C=8C=EC=9D=B4=ED=94=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index f048658..8ae932c 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -16,13 +16,13 @@ export class FeedController { // 피드 조회 (게시글) @Get(':id') async getFeed(@Param('id') feedId: number) { - return await this.feedService.getFeed(+feedId); + return await this.feedService.getFeed(feedId); } // 피드 조회 (댓글) @Get(':id/comments') async getFeedComments(@Param('id') feedId: number) { - return await this.feedService.getFeedComments(+feedId); + return await this.feedService.getFeedComments(feedId); } // 좋아요 추가/ 제거 From da700e395c6572e2a15aab21627822a016a576fe Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 15:28:39 +0900 Subject: [PATCH 080/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 5 +++-- src/feed/feed.service.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 8ae932c..a0c6e88 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -15,8 +15,9 @@ export class FeedController { // 피드 조회 (게시글) @Get(':id') - async getFeed(@Param('id') feedId: number) { - return await this.feedService.getFeed(feedId); + @UseGuards(OptionalAuthGuard) + async getFeed(@Param('id') feedId: number, @Req() req) { + return await this.feedService.getFeed(feedId, req.user); } // 피드 조회 (댓글) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 28eb590..568a16b 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -65,13 +65,16 @@ export class FeedService { } /** 피드 조회 (게시글 부분) / 남은 구현 과제 - * 유저 토큰 제공 시 좋아요 여부 * 예외 처리 **/ - async getFeed(feedId) { + async getFeed(feedId, user) { + const userId = user ? user.user_id : 0; const result = await this.prisma.feedPost.findUnique({ where: { id: feedId }, include: { + Likes: { + where: { user_id: userId }, + }, user: { select: { id: true, @@ -107,6 +110,7 @@ export class FeedService { likeCount: result.like_count, viewCount: result.view, createdAt: result.created_at, + Liked: !!result.Likes.length, }; return { post }; } From 434d8291ab8a7e50a0f3742f25fc1c9b06ff4294 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 16 Jan 2025 15:32:18 +0900 Subject: [PATCH 081/414] =?UTF-8?q?[Add]=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7ca743e..f608ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/passport-github2": "^1.2.9", "@types/passport-google-oauth20": "^2.0.16", @@ -4122,6 +4123,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.17.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", diff --git a/package.json b/package.json index 49524a9..3273da8 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/passport-github2": "^1.2.9", "@types/passport-google-oauth20": "^2.0.16", From f47ba256c617dbdb998c92f369504c59095f8cbf Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 16 Jan 2025 15:32:33 +0900 Subject: [PATCH 082/414] =?UTF-8?q?[Feat]=20user=20API=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20=EB=BC=88=EB=8C=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 55 +++++++++++++++++++++++++++++ src/modules/user/user.service.ts | 40 ++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 327aff8..451ca0d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -4,12 +4,18 @@ import { Get, Param, Patch, + Post, Req, Res, UseGuards, + UseInterceptors, + UploadedFile, + BadRequestException, + Delete, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; +import { FileInterceptor } from '@nestjs/platform-express'; @UseGuards(JwtAuthGuard) @Controller('users') @@ -49,4 +55,53 @@ export class UserController { const userId = req.user?.user_id; return this.userService.patchUserIntroduce(userId, introduce); } + + @Patch('profile/status') + async patchUserStatus(@Req() req, @Body('status') statusId: number) { + const userId = req.user?.user_id; + return this.userService.patchUserStatus(userId, statusId); + } + + @Post('profile/skills') + async patchUserSkills(@Req() req, @Body('skills') skills: string[]) { + const userId = req.user?.user_id; + return this.userService.patchUserSkills(userId, skills); + } + + @Patch('profile/image') + @UseInterceptors(FileInterceptor('file')) + async patchProfileImage( + @Req() req, + @UploadedFile() file: Express.Multer.File + ) { + const userId = req.user?.user_id; + if (!file) { + throw new BadRequestException('파일이 업로드되지 않았습니다'); + } + const fileType = file.mimetype.split('/')[1]; + const updateUser = await this.userService.patchProfileImage( + userId, + file.buffer, + fileType + ); + return { + message: '프로필 이미지가 성공적으로 업데이트되었습니다.', + user: updateUser, + }; + } + + @Delete('profile/skills') + async deleteUserSkills(@Req() req, @Body('skills') skills: string[]) { + const userId = req.user?.user_id; + return this.userService.deleteUserSkills(userId, skills); + } + + @Patch('profile/notification') + async patchUserNotification( + @Req() req, + @Body('notification') notification: boolean + ) { + const userId = req.user?.user_id; + return this.userService.patchUserNotification(userId, notification); + } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 617118e..994fac8 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,9 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; +import { S3Service } from '@src/s3/s3.service'; @Injectable() export class UserService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly s3Service: S3Service + ) {} async getUserProfile(loggedInUserId: number, targetUserId: number) { // 사용자 정보를 가져옴 @@ -100,4 +104,38 @@ export class UserService { async patchUserIntroduce(userId: number, introduce: string) { return Promise.resolve(undefined); } + + async patchUserStatus(userId: number, status_id: number) { + return Promise.resolve(undefined); + } + + async patchUserSkills(userId: number, skills: string[]) { + return Promise.resolve(undefined); + } + + async patchProfileImage( + userId: number, + fileBuffer: Buffer, + fileType: string + ) { + const imageUrl = await this.s3Service.uploadImage( + userId, + fileBuffer, + fileType + ); + + return this.prisma.user.update({ + where: { id: userId }, + data: { profile_url: imageUrl }, + select: { id: true, nickname: true, profile_url: true }, + }); + } + + async deleteUserSkills(userId: number, skills: string[]) { + return Promise.resolve(undefined); + } + + async patchUserNotification(userId: number, notification: boolean) { + return Promise.resolve(undefined); + } } From 19b4aa4da77619cd753579fa674348c71bf3fef0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 16:14:30 +0900 Subject: [PATCH 083/414] =?UTF-8?q?[Docs]=20=EC=A0=95=EB=A6=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 4 ++++ src/feed/feed.service.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index a0c6e88..5ad0309 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -33,4 +33,8 @@ export class FeedController { const userId = req.user.user_id; return await this.feedService.handlePostLikes(feedId, userId); } + + /** 피드 등록 + * 등록 시 썸네일 추출 -> cheerio + */ } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 568a16b..9e09119 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -5,7 +5,7 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class FeedService { constructor(private readonly prisma: PrismaService) {} /** 피드 전체 조회 / 남은 구현 과제 - * 썸네일 뽑기 + * 최신순 정렬 * 예외 처리 **/ async getAllFeeds(user) { From 4d38ff923c6052808b009ee486f8eb38e6bfe999 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 16:36:33 +0900 Subject: [PATCH 084/414] =?UTF-8?q?[Refactor]=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 193 ++++++++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 75 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 9e09119..e22868a 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() @@ -6,7 +6,6 @@ export class FeedService { constructor(private readonly prisma: PrismaService) {} /** 피드 전체 조회 / 남은 구현 과제 * 최신순 정렬 - * 예외 처리 **/ async getAllFeeds(user) { try { @@ -58,94 +57,134 @@ export class FeedService { })); return { posts }; - // return result; } catch (err) { - return err; + console.log(err); + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); } } - /** 피드 조회 (게시글 부분) / 남은 구현 과제 - * 예외 처리 - **/ + // 피드 조회 (게시글 부분) async getFeed(feedId, user) { - const userId = user ? user.user_id : 0; - const result = await this.prisma.feedPost.findUnique({ - where: { id: feedId }, - include: { - Likes: { - where: { user_id: userId }, - }, - user: { - select: { - id: true, - name: true, - email: true, - role: { - select: { name: true }, + try { + const userId = user ? user.user_id : 0; + const result = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + include: { + Likes: { + where: { user_id: userId }, + }, + user: { + select: { + id: true, + name: true, + email: true, + role: { + select: { name: true }, + }, + profile_url: true, }, - profile_url: true, }, - }, - Tags: { - select: { - tag: { - select: { name: true }, + Tags: { + select: { + tag: { + select: { name: true }, + }, }, }, }, - }, - }); - - const post = { - userId: result.user.id, - userName: result.user.name, - userRole: result.user.role.name, - userProfileUrl: result.user.profile_url, - title: result.title, - postId: result.id, - thumnailUrl: result.thumbnail_url, - content: result.content, - tags: result.Tags.map(v => v.tag.name), - commentCount: result.comment_count, - likeCount: result.like_count, - viewCount: result.view, - createdAt: result.created_at, - Liked: !!result.Likes.length, - }; - return { post }; + }); + + if (!result) { + throw new HttpException( + '게시글을 찾을 수 없습니다', + HttpStatus.NOT_FOUND + ); + } + + const post = { + userId: result.user.id, + userName: result.user.name, + userRole: result.user.role.name, + userProfileUrl: result.user.profile_url, + title: result.title, + postId: result.id, + thumnailUrl: result.thumbnail_url, + content: result.content, + tags: result.Tags.map(v => v.tag.name), + commentCount: result.comment_count, + likeCount: result.like_count, + viewCount: result.view, + createdAt: result.created_at, + Liked: !!result.Likes.length, + }; + return { post }; + } catch (err) { + console.log(err); + if (err instanceof HttpException) { + throw err; + } + + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } } + // 피드 개별 조회 (댓글) async getFeedComments(feedId) { - const result = await this.prisma.feedComment.findMany({ - where: { - post_id: feedId, - }, - include: { - user: { - select: { - id: true, - name: true, - email: true, - role: { - select: { name: true }, + try { + const result = await this.prisma.feedComment.findMany({ + where: { + post_id: feedId, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + role: { + select: { name: true }, + }, + profile_url: true, }, - profile_url: true, }, }, - }, - }); - - const comments = result.map(c => ({ - commentId: c.id, - userId: c.user.id, - userName: c.user.name, - userRole: c.user.role.name, - userProfileUrl: c.user.profile_url, - comment: c.content, - createdAt: c.created_at, - })); - - return { comments }; + }); + + if (!result.length) { + throw new HttpException( + '게시글을 찾을 수 없습니다', + HttpStatus.NOT_FOUND + ); + } + + const comments = result.map(c => ({ + commentId: c.id, + userId: c.user.id, + userName: c.user.name, + userRole: c.user.role.name, + userProfileUrl: c.user.profile_url, + comment: c.content, + createdAt: c.created_at, + })); + + return { comments }; + } catch (err) { + console.log(err); + if (err instanceof HttpException) { + throw err; + } + + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } } // 게시글 좋아요 추가/제거 @@ -188,7 +227,11 @@ export class FeedService { return { message: '좋아요 추가 ' }; } } catch (err) { - return err; + console.log(err); + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); } } } From 59e8594d605f35cdc3cf199f57929ef45958b75a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 16:40:35 +0900 Subject: [PATCH 085/414] [Feat] Prisma db pull --- prisma/schema.prisma | 105 +++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5f136d7..d029c5e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,23 +28,23 @@ model User { ArtistData ArtistData? Channel_users Channel_users[] FeedComments FeedComment[] + FeedCommentLikes FeedCommentLikes[] FeedLikes FeedLike[] FeedPosts FeedPost[] Followed Follows[] @relation("FollowedUsers") Follows Follows[] @relation("UserFollows") Message Message[] Message_status Message_status[] + MyPageProject MyPageProject? + MyPageUserLink MyPageUserLink[] ProgrammerData ProgrammerData? - ProjectPosts Project? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] role Role @relation(fields: [role_id], references: [id]) status Status @relation(fields: [status_id], references: [id]) UserApplyProject UserApplyProject[] - UserLinks UserLink[] UserSkills UserSkill[] - FeedCommentLikes FeedCommentLikes[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -56,52 +56,26 @@ model Role { Users User[] } -model Project { - id Int @id @default(autoincrement()) - user_id Int @unique - title String - description String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) - ProjectLinks ProjectLink[] -} - -model ProjectLink { - id Int @id @default(autoincrement()) - project_id Int - type_id Int - url String - project Project @relation(fields: [project_id], references: [id]) - type LinkType @relation(fields: [type_id], references: [id]) - - @@index([project_id], map: "ProjectLink_project_id_fkey") - @@index([type_id], map: "ProjectLink_type_id_fkey") -} - model LinkType { - id Int @id @default(autoincrement()) - name String @unique - Links ProjectLink[] + id Int @id @default(autoincrement()) + name String @unique + MyPageProjectLink MyPageProjectLink[] } model ProgrammerData { - id Int @id @default(autoincrement()) - user_id Int @unique - github_username String - github_url String - commit_count Int - contribution_data String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + github_username String + user User @relation(fields: [user_id], references: [id]) } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int @unique - soundcloud_url String - portfolio_url String - music_data String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + music_data String? + music_url String + platform String + user User @relation(fields: [user_id], references: [id]) } model Status { @@ -137,16 +111,6 @@ model UserSkill { @@index([skill_id], map: "UserSkill_skill_id_fkey") } -model UserLink { - id Int @id @default(autoincrement()) - user_id Int - platform String - link String - user User @relation(fields: [user_id], references: [id]) - - @@index([user_id], map: "UserLink_user_id_fkey") -} - model Follows { id Int @id @default(autoincrement()) following_user_id Int @@ -214,9 +178,12 @@ model FeedComment { model FeedCommentLikes { id Int @id @default(autoincrement()) user_id Int - user User @relation(fields: [user_id], references: [id]) comment_id Int comment FeedComment @relation(fields: [comment_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@index([comment_id], map: "FeedCommentLikes_comment_id_fkey") + @@index([user_id], map: "FeedCommentLikes_user_id_fkey") } model FeedLike { @@ -372,3 +339,35 @@ model online_users { client_id String user_id Int } + +model MyPageProject { + id Int @id @default(autoincrement()) + user_id Int @unique + title String + description String + created_at DateTime @default(now()) + updated_at DateTime + User User @relation(fields: [user_id], references: [id]) + MyPageProjectLink MyPageProjectLink[] +} + +model MyPageProjectLink { + id Int @id @default(autoincrement()) + project_id Int + type_id Int + url String + MyPageProject MyPageProject @relation(fields: [project_id], references: [id]) + LinkType LinkType @relation(fields: [type_id], references: [id]) + + @@index([project_id], map: "ProjectLink_project_id_fkey") + @@index([type_id], map: "ProjectLink_type_id_fkey") +} + +model MyPageUserLink { + id Int @id @default(autoincrement()) + user_id Int + link String + User User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "UserLink_user_id_fkey") +} From 9b8dadf70a5b7346d9314571ca03e6f0519a2098 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 17:34:52 +0900 Subject: [PATCH 086/414] =?UTF-8?q?[Fix]=20=EC=B1=88=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 6 ++-- src/chat/chat.service.ts | 62 ++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index 5b0a9f5..c990308 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -20,14 +20,12 @@ export class ChatController { async getChannelsMessages( @Req() req: any, @Param('id') channelId: number, - @Query('limit') limit: number, - @Query('currentPage') currentPage: number + @Query('limit') limit: number ) { return await this.chatService.getMessages( req.user.user_id, +channelId, - +limit, - +currentPage + +limit ); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7e9aab0..8e1f4a9 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -206,7 +206,7 @@ export class ChatService { } // 채널 메세지 조회 - async getMessages(userId, channelId, limit, currentPage) { + async getMessages(userId, channelId, limit) { try { // 유저 아이디가 채널에 속해있는지 확인 const auth = await this.prisma.channel_users.findMany({ @@ -222,35 +222,28 @@ export class ChatService { } // 메세지 데이터 조회 - const [result, totalMessageCount] = await Promise.all([ - this.prisma.message.findMany({ - where: { - channel_id: channelId, - }, - include: { - user: { - select: { - id: true, - email: true, - name: true, - nickname: true, - role: true, - profile_url: true, - auth_provider: true, - }, + const result = await this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + role: true, + profile_url: true, + auth_provider: true, }, }, - orderBy: { - id: 'desc', - }, - take: limit, - }), - this.prisma.message.count({ - where: { - channel_id: channelId, - }, - }), - ]); + }, + orderBy: { + id: 'desc', + }, + take: limit, + }); // 메세지 데이터 양식화 const data = result.map(msg => ({ @@ -260,18 +253,15 @@ export class ChatService { channelId: msg.channel_id, date: msg.created_at, user: { - id: msg.user.id, + userId: msg.user.id, email: msg.user.email, + name: msg.user.name, nickname: msg.user.nickname, - role: msg.user.role.name, profileUrl: msg.user.profile_url, + authProvider: msg.user.auth_provider, + roleId: msg.user.role.id, }, })); - // 페이지네이션 - const pagenation = { - totalMessageCount, - currentPage: currentPage, - }; const message = { code: 200, @@ -279,7 +269,7 @@ export class ChatService { }; // 응답데이터 {메세지데이터, 페이지네이션} - return { messages: data, pagenation, message }; + return { messages: data, message }; } catch (err) { return err.message; } From 9ca083cd54da5d04dd99d1581b10775beea858b9 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 17:45:32 +0900 Subject: [PATCH 087/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 6 ++++-- src/chat/chat.service.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index c990308..5b0a9f5 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -20,12 +20,14 @@ export class ChatController { async getChannelsMessages( @Req() req: any, @Param('id') channelId: number, - @Query('limit') limit: number + @Query('limit') limit: number, + @Query('currentPage') currentPage: number ) { return await this.chatService.getMessages( req.user.user_id, +channelId, - +limit + +limit, + +currentPage ); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 8e1f4a9..3e22a60 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -206,7 +206,7 @@ export class ChatService { } // 채널 메세지 조회 - async getMessages(userId, channelId, limit) { + async getMessages(userId, channelId, limit, currentPage) { try { // 유저 아이디가 채널에 속해있는지 확인 const auth = await this.prisma.channel_users.findMany({ @@ -240,9 +240,10 @@ export class ChatService { }, }, orderBy: { - id: 'desc', + id: 'asc', }, take: limit, + skip: currentPage - 1, }); // 메세지 데이터 양식화 From 3faec29a52f7c292741acf4900b9f33c4ba7da07 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 18:00:13 +0900 Subject: [PATCH 088/414] =?UTF-8?q?[Fix]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20-=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=96=91=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 15 ++++++++++++--- src/chat/chat.service.ts | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index b4e2fca..6289ce6 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -115,23 +115,32 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { channelId: number; } ) { - const { userId, ...resData } = data; + const { userId } = data; // 메세지 데이터 저장 - await this.chatService.createMessage( + const messageData = await this.chatService.createMessage( data.type, data.channelId, userId, data.content ); + const messageId = messageData.id; + // 유저 정보 추가 const user = await this.chatService.getSenderProfile(userId); const date = new Date(); // 전달 데이터 양식 - const sendData = { ...resData, user, date }; + const sendData = { + type: data.type, + content: data.content, + channelId: data.channelId, + messageId, + user, + date, + }; console.log(sendData); this.server.to(data.channelId.toString()).emit('message', sendData); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 3e22a60..fb99534 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -51,7 +51,7 @@ export class ChatService { // 메세지 저장 async createMessage(type, channelId, userId, content) { - await this.prisma.message.create({ + return await this.prisma.message.create({ data: { type, content, @@ -81,6 +81,7 @@ export class ChatService { const data = { userId: result.id, email: result.email, + name: result.name, nickname: result.nickname, profileUrl: result.profile_url, authProvide: result.auth_provider, @@ -248,7 +249,7 @@ export class ChatService { // 메세지 데이터 양식화 const data = result.map(msg => ({ - id: msg.id, + messageId: msg.id, type: msg.type, content: msg.content, channelId: msg.channel_id, From 9562af285907e1470035654a35a3de9776c2dedf Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 18:42:44 +0900 Subject: [PATCH 089/414] =?UTF-8?q?[Feat]=20create-post=20DTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/create-post.dto.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/feed/dto/create-post.dto.ts diff --git a/src/feed/dto/create-post.dto.ts b/src/feed/dto/create-post.dto.ts new file mode 100644 index 0000000..923f3c4 --- /dev/null +++ b/src/feed/dto/create-post.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsArray, ArrayNotEmpty, IsNotEmpty } from 'class-validator'; + +export class CreatePostDto { + @IsString() + @IsNotEmpty() + title: string; + + @IsArray() + @ArrayNotEmpty() + @IsString({ each: true }) + tags: string[]; + + @IsString() + @IsNotEmpty() + content: string; +} From be1232225562ef51f3c9ba1db273ce220cbfb9fc Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 19:10:11 +0900 Subject: [PATCH 090/414] =?UTF-8?q?[Feat]=20Prisma=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20thumnail=5Furl=20=EC=98=B5=EC=85=94?= =?UTF-8?q?=EB=84=90,=20view,=20like=5Fcount,=20comment=5Fcount=20?= =?UTF-8?q?=EB=94=94=ED=8F=B4=ED=8A=B8=EA=B0=92=20=200=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d029c5e..4f75e57 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -128,12 +128,12 @@ model FeedPost { user_id Int title String content String - thumbnail_url String + thumbnail_url String? created_at DateTime @default(now()) updated_at DateTime @updatedAt - view Int - comment_count Int - like_count Int + view Int @default(0) + comment_count Int @default(0) + like_count Int @default(0) Comments FeedComment[] Likes FeedLike[] user User @relation(fields: [user_id], references: [id]) From c8a95a47d0f40a5cf0a59eb40273893184f05308 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 19:22:00 +0900 Subject: [PATCH 091/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 17 +++++++++++++++- src/feed/feed.service.ts | 40 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 5ad0309..edca440 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,7 +1,16 @@ -import { Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { FeedService } from './feed.service'; import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; +import { CreatePostDto } from './dto/create-post.dto'; @Controller('feed') export class FeedController { @@ -37,4 +46,10 @@ export class FeedController { /** 피드 등록 * 등록 시 썸네일 추출 -> cheerio */ + @Post() + @UseGuards(JwtAuthGuard) + async createPost(@Req() req, @Body() createPostDto: CreatePostDto) { + const userId = req.user.user_id; + return this.feedService.createPost(createPostDto, userId); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index e22868a..af7a2a0 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; +import { CreatePostDto } from './dto/create-post.dto'; @Injectable() export class FeedService { @@ -187,7 +188,7 @@ export class FeedService { } } - // 게시글 좋아요 추가/제거 + // 피드 좋아요 추가/제거 async handlePostLikes(feedId, userId) { try { const exist = await this.prisma.feedLike.findMany({ @@ -234,4 +235,41 @@ export class FeedService { ); } } + + // 피드 등록 + async createPost(dto: CreatePostDto, userId: number) { + const { title, tags, content } = dto; + try { + // FeedPost에 피드 데이터 저장 + const feedData = await this.prisma.feedPost.create({ + data: { + user_id: userId, + title, + content, + }, + }); + + // 태그 이름으로 태그 id 조회 + const tagIds = await this.prisma.feedTag.findMany({ + where: { name: { in: tags } }, + select: { id: true }, + }); + + // 태그 데이터 양식화 {post_id, tag_id} + const tagData = tagIds.map(tag => ({ + post_id: feedData.id, + tag_id: tag.id, + })); + + // 태그 데이터 저장 + await this.prisma.feedPostTag.createMany({ + data: tagData, + }); + + console.log(tagIds); + return dto; + } catch (err) { + throw err; + } + } } From fc9ad0827db570d6580f95e7a9e071e5888dbddf Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 19:23:27 +0900 Subject: [PATCH 092/414] [Chore] cheerio --- package-lock.json | 279 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 280 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7ca743e..3df09d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", + "cheerio": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", @@ -5297,6 +5298,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -5561,6 +5568,48 @@ "dev": true, "license": "MIT" }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6038,6 +6087,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -6224,6 +6301,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -6332,6 +6464,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/engine.io": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", @@ -6402,6 +6559,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7719,6 +7888,25 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9792,6 +9980,18 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/oauth": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", @@ -9999,6 +10199,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12203,6 +12440,15 @@ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", "license": "MIT" }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -12500,6 +12746,39 @@ "node": ">=4.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 49524a9..046ea34 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", + "cheerio": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", From 8825e4671eab54e2b84f3db24d58cdb885b7803c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 20:01:22 +0900 Subject: [PATCH 093/414] =?UTF-8?q?[Feat]=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20?= =?UTF-8?q?url=20=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index af7a2a0..4280747 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -1,6 +1,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; import { CreatePostDto } from './dto/create-post.dto'; +import * as cheerio from 'cheerio'; @Injectable() export class FeedService { @@ -237,15 +238,19 @@ export class FeedService { } // 피드 등록 - async createPost(dto: CreatePostDto, userId: number) { - const { title, tags, content } = dto; + async createPost(createPostDto: CreatePostDto, userId: number) { + const { title, tags, content } = createPostDto; try { + // 썸네일 url 추출 + const thumnailUrl = await this.getThumnailUrl(content); + // FeedPost에 피드 데이터 저장 const feedData = await this.prisma.feedPost.create({ data: { user_id: userId, title, content, + thumbnail_url: thumnailUrl, }, }); @@ -266,8 +271,17 @@ export class FeedService { data: tagData, }); - console.log(tagIds); - return dto; + return createPostDto; + } catch (err) { + throw err; + } + } + + async getThumnailUrl(text) { + try { + const $ = cheerio.load(text); + const thumnailUrl = $('img').first().attr('src'); + return thumnailUrl; } catch (err) { throw err; } From 799c2090274b2e0d73f42275fd6edeb2f6044992 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 20:16:29 +0900 Subject: [PATCH 094/414] =?UTF-8?q?[Feat]=20Prisma=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=9C=A0=EC=A0=80=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20profile=5Furl,=20=ED=94=BC=EB=93=9C=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20thumnail=5Furl=20@db.Text=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f75e57..c0cc557 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,7 @@ model User { name String nickname String auth_provider String - profile_url String? + profile_url String? @db.Text role_id Int introduce String? status_id Int @@ -127,7 +127,7 @@ model FeedPost { id Int @id @default(autoincrement()) user_id Int title String - content String + content String @db.Text thumbnail_url String? created_at DateTime @default(now()) updated_at DateTime @updatedAt From f9f09f2b61dd7b6cb4e18acf31be7a24de888efd Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 20:16:56 +0900 Subject: [PATCH 095/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 4280747..4fdfa10 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -271,7 +271,7 @@ export class FeedService { data: tagData, }); - return createPostDto; + return { success: true, message: '게시글 작성이 완료되었습니다.' }; } catch (err) { throw err; } From cbe45475a22a700751a6aba56883e3255731901b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 21:31:48 +0900 Subject: [PATCH 096/414] =?UTF-8?q?[Fix]=20=EC=B1=84=EB=84=90=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index fb99534..0b50d46 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -241,10 +241,10 @@ export class ChatService { }, }, orderBy: { - id: 'asc', + id: 'desc', }, take: limit, - skip: currentPage - 1, + skip: (currentPage - 1) * limit, }); // 메세지 데이터 양식화 From 42449ccd135ef5f63dcce1745857caf8a7589062 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 21:38:20 +0900 Subject: [PATCH 097/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=A1=9C=EC=A7=81=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94,=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 4fdfa10..a8d8b50 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -212,7 +212,7 @@ export class FeedService { data: { like_count: { decrement: 1 } }, }); - return { message: '좋아요 취소 ' }; + return { success: true, message: '좋아요가 취소되었습니다. ' }; } else { await this.prisma.feedLike.create({ data: { @@ -226,7 +226,7 @@ export class FeedService { data: { like_count: { increment: 1 } }, }); - return { message: '좋아요 추가 ' }; + return { success: true, message: '좋아요가 추가되었습니다. ' }; } } catch (err) { console.log(err); @@ -277,6 +277,7 @@ export class FeedService { } } + // 썸네일 추출 async getThumnailUrl(text) { try { const $ = cheerio.load(text); From 6f665883a8da087a200f89df9861bd07f3db3378 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 16 Jan 2025 22:44:11 +0900 Subject: [PATCH 098/414] =?UTF-8?q?[Feat]=20DB=20Scheme=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d903028..0e83570 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,14 +36,14 @@ model User { Message Message[] Message_status Message_status[] ProgrammerData ProgrammerData? - ProjectPosts Project? + MyPageProject MyPageProject? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] role Role @relation(fields: [role_id], references: [id]) status Status @relation(fields: [status_id], references: [id]) UserApplyProject UserApplyProject[] - UserLinks UserLink[] + UserLinks MyPageUserLink[] UserSkills UserSkill[] @@ -57,7 +57,7 @@ model Role { Users User[] } -model Project { +model MyPageProject { id Int @id @default(autoincrement()) user_id Int @unique title String @@ -65,15 +65,15 @@ model Project { created_at DateTime @default(now()) updated_at DateTime @updatedAt user User @relation(fields: [user_id], references: [id]) - ProjectLinks ProjectLink[] + ProjectLinks MyPageProjectLink[] } -model ProjectLink { +model MyPageProjectLink { id Int @id @default(autoincrement()) project_id Int type_id Int url String - project Project @relation(fields: [project_id], references: [id]) + project MyPageProject @relation(fields: [project_id], references: [id]) type LinkType @relation(fields: [type_id], references: [id]) @@index([project_id], map: "ProjectLink_project_id_fkey") @@ -83,25 +83,22 @@ model ProjectLink { model LinkType { id Int @id @default(autoincrement()) name String @unique - Links ProjectLink[] + Links MyPageProjectLink[] } model ProgrammerData { id Int @id @default(autoincrement()) user_id Int @unique github_username String - github_url String - commit_count Int - contribution_data String user User @relation(fields: [user_id], references: [id]) } model ArtistData { id Int @id @default(autoincrement()) user_id Int @unique - soundcloud_url String - portfolio_url String - music_data String + music_url String + platform String + music_data String? user User @relation(fields: [user_id], references: [id]) } @@ -138,10 +135,9 @@ model UserSkill { @@index([skill_id], map: "UserSkill_skill_id_fkey") } -model UserLink { +model MyPageUserLink { id Int @id @default(autoincrement()) user_id Int - platform String link String user User @relation(fields: [user_id], references: [id]) From 946643fcf6d218fad93df528597f9f08e9d468a6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 16 Jan 2025 22:44:35 +0900 Subject: [PATCH 099/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0=ED=9A=8C,=20=ED=8C=94=EB=A1=9C=EC=9A=B0,=20?= =?UTF-8?q?=ED=8C=94=EB=A1=9C=EC=9E=89=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 17 ++- src/modules/user/user.service.ts | 195 ++++++++++++++++++---------- 2 files changed, 135 insertions(+), 77 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 451ca0d..aa51cd6 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -22,20 +22,23 @@ import { FileInterceptor } from '@nestjs/platform-express'; export class UserController { constructor(private readonly userService: UserService) {} - @Get(':userId/profile') + @Get(':userId') async getUserProfile(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID - console.log(loggedInUserId); return this.userService.getUserProfile(loggedInUserId, numUserId); } - @Get(':userId/follow-relations') - async getFollowRelations(@Param('userId') userId: string, @Req() req) { - const loggedInUserId = req.user?.user_id; + @Get(':userId/followers') + async getUserFollowers(@Param('userId') userId: string) { + const numUserId = parseInt(userId); // 인증된 사용자 ID + return this.userService.getUserFollowers(numUserId); + } + + @Get(':userId/followers') + async getUserFollowings(@Param('userId') userId: string) { const numUserId = parseInt(userId); // 인증된 사용자 ID - console.log(loggedInUserId); - return this.userService.getFollowRelations(loggedInUserId, numUserId); + return this.userService.getUserFollowings(numUserId); } @Get('setting') diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 994fac8..00c245f 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -10,89 +10,144 @@ export class UserService { ) {} async getUserProfile(loggedInUserId: number, targetUserId: number) { - // 사용자 정보를 가져옴 - try { - const user = await this.prisma.user.findUnique({ - where: { id: targetUserId }, - include: { - role: true, // 역할 정보 (아티스트/프로그래머/디자이너) - ArtistData: true, // 아티스트 관련 데이터 - ProgrammerData: true, // 프로그래머 관련 데이터 - Resume: true, // 이력서 정보 + // 조회 대상 사용자의 프로필 정보 가져오기 + const user = await this.prisma.user.findUnique({ + where: { id: targetUserId }, + include: { + role: true, // 역할 정보 + status: true, // 상태 정보 + ArtistData: true, // 아티스트 데이터 + ProgrammerData: true, // 프로그래머 데이터 + UserLinks: true, // 연결된 링크 + MyPageProject: { + include: { + ProjectLinks: { + include: { + type: true, // LinkType 포함 + }, + }, + }, }, - }); - - if (!user) { - throw new NotFoundException('사용자를 찾을 수 없습니다.'); - } - - // 직업군에 따른 맞춤 정보 생성 - let specificData = null; - if (user.role.name === 'Artist') { - specificData = { - soundcloudUrl: user.ArtistData?.soundcloud_url, - portfolioUrl: user.ArtistData?.portfolio_url, - }; - } else if (user.role.name === 'Programmer') { - specificData = { - githubUsername: user.ProgrammerData?.github_username, - githubUrl: user.ProgrammerData?.github_url, - commitCount: user.ProgrammerData?.commit_count, - contributionData: user.ProgrammerData?.contribution_data, - }; - } - - // 로그인한 사용자와 조회 대상 사용자가 같은지 확인 - const isOwnProfile = loggedInUserId === targetUserId; - - // 사용자 정보 반환 - return { - id: user.id, - email: user.email, // 본인만 이메일 확인 가능 - nickname: user.nickname, - profileUrl: user.profile_url, - role: user.role.name, - introduce: user.introduce, - applyCount: user.apply_count, - postCount: user.post_count, - createdAt: user.created_at, - updatedAt: user.updated_at, - specificData, // 직업군에 따른 데이터 - isOwnProfile, // 자신의 프로필 여부 - }; - } catch (err) { - console.error(err); + }, + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); } - } - async getFollowRelations(loggedInUserId: number, targetUserId: number) { - // 로그인한 사용자와 조회 대상 사용자가 같은지 확인 - const isOwnProfile = loggedInUserId === targetUserId; - const followers = await this.prisma.follows.findMany({ + // 로그인한 사용자가 해당 유저를 팔로우하고 있는지 확인 + const isFollowing = await this.prisma.follows.findFirst({ + where: { + following_user_id: loggedInUserId, + followed_user_id: targetUserId, + }, + }); + + // 팔로워 수와 팔로잉 수 계산 + const followerCount = await this.prisma.follows.count({ where: { followed_user_id: targetUserId }, - include: { following_user: true }, }); - const followings = await this.prisma.follows.findMany({ + const followingCount = await this.prisma.follows.count({ where: { following_user_id: targetUserId }, - include: { followed_user: true }, }); + // 사용자 직업군에 따른 맞춤 데이터 생성 + let specificData = null; + if (user.role.name === 'Artist') { + specificData = { + musicUrl: user.ArtistData?.music_url, + platform: user.ArtistData?.platform, + }; + } else if ( + user.role.name === 'Programmer' || + user.role.name === 'Designer' + ) { + specificData = { + githubUsername: user.ProgrammerData?.github_username, + myPageProjects: user.MyPageProject + ? { + title: user.MyPageProject.title, + description: user.MyPageProject.description, + links: user.MyPageProject.ProjectLinks.map(link => ({ + type: link.type.name, + url: link.url, + })), + } + : null, + }; + } + + // 반환 데이터 구성 return { - isOwnProfile, - followers: followers.map(f => ({ - id: f.following_user.id, - nickname: f.following_user.nickname, - profileUrl: f.following_user.profile_url, - })), - followings: followings.map(f => ({ - id: f.followed_user.id, - nickname: f.followed_user.nickname, - profileUrl: f.followed_user.profile_url, - })), + id: user.id, + nickname: user.nickname, + profileUrl: user.profile_url, + role: user.role.name, + introduce: user.introduce, + status: user.status.name, + applyCount: user.apply_count, + postCount: user.post_count, + followerCount, // 팔로워 수 + followingCount, // 팔로잉 수 + userLinks: user.UserLinks.map(link => ({ + url: link.link, + })), // 연결된 링크 + specificData, // 직업군 맞춤 데이터 + isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 + isFollowing: !!isFollowing, // 팔로우 여부 확인 }; } + async getUserFollowers(targetUserId: number) { + // 특정 사용자를 팔로우하는 사용자 목록 조회 + const followers = await this.prisma.follows.findMany({ + where: { + followed_user_id: targetUserId, // 타겟 유저를 팔로우하는 사용자 + }, + include: { + following_user: { + select: { + id: true, + nickname: true, + profile_url: true, // 프로필 이미지 + }, + }, + }, + }); + + // 반환 데이터 생성 + return followers.map(follower => ({ + id: follower.following_user.id, + nickname: follower.following_user.nickname, + profileUrl: follower.following_user.profile_url, + })); + } + + async getUserFollowings(targetUserId: number) { + const followings = await this.prisma.follows.findMany({ + where: { + following_user_id: targetUserId, // 타겟 유저가 팔로우한 사용자 + }, + include: { + followed_user: { + select: { + id: true, + nickname: true, + profile_url: true, // 프로필 이미지 + }, + }, + }, + }); + + // 반환 데이터 생성 + return followings.map(following => ({ + id: following.followed_user.id, + nickname: following.followed_user.nickname, + profileUrl: following.followed_user.profile_url, + })); + } + async patchUserNickname(userId: number, nickname: string) { return Promise.resolve(undefined); } From 69dcee757ba5e9d38d52a9478fed25388cf00ae4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 16 Jan 2025 22:44:46 +0900 Subject: [PATCH 100/414] =?UTF-8?q?[Feat]=20User=20Module=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index aedd372..cd02609 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -3,9 +3,10 @@ import { AuthModule } from '@modules/auth/auth.module'; import { UserService } from './user.service'; import { UserController } from './user.controller'; import { PrismaService } from '@prisma/prisma.service'; +import { S3Module } from '@src/s3/s3.module'; @Module({ - imports: [AuthModule], // AuthModule을 가져옴 + imports: [AuthModule, S3Module], // AuthModule을 가져옴 controllers: [UserController], providers: [UserService, PrismaService], exports: [UserService], From 00bd306beb11bf4bad66697bb015e74e28d3685b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 16 Jan 2025 22:53:16 +0900 Subject: [PATCH 101/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 16 +++++++++++++--- src/feed/feed.service.ts | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index edca440..b8cbaa0 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -43,13 +43,23 @@ export class FeedController { return await this.feedService.handlePostLikes(feedId, userId); } - /** 피드 등록 - * 등록 시 썸네일 추출 -> cheerio - */ + // 피드 등록 @Post() @UseGuards(JwtAuthGuard) async createPost(@Req() req, @Body() createPostDto: CreatePostDto) { const userId = req.user.user_id; return this.feedService.createPost(createPostDto, userId); } + + // 댓글 등록 + @Post(':id/comment') + @UseGuards(JwtAuthGuard) + async createComment( + @Req() req, + @Param('id') feedId: number, + @Body() comment + ) { + const userId = req.user.user_id; + return this.feedService.createComment(userId, feedId, comment.content); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index a8d8b50..591d4a5 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -287,4 +287,24 @@ export class FeedService { throw err; } } + + // 댓글 등록 + async createComment(userId, feedId, content) { + try { + await this.prisma.feedComment.create({ + data: { + user_id: userId, + post_id: feedId, + content: content, + }, + }); + return { success: true, message: '댓글 등록이 완료되었습니다.' }; + } catch (err) { + console.log(err); + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } } From 8db46de4ef1f8c179ef4e6c9f20ff88ff008939e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 00:09:37 +0900 Subject: [PATCH 102/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 0b50d46..5bbbfac 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -163,7 +163,15 @@ export class ChatService { Channel_users: { select: { user: { - select: { nickname: true }, + select: { + id: true, + email: true, + name: true, + nickname: true, + profile_url: true, + auth_provider: true, + role_id: true, + }, }, }, }, @@ -192,10 +200,23 @@ export class ChatService { channelId: result.id, title: result.name, type: result.Channel_users.length > 2 ? 'group' : 'private', - users: result.Channel_users.map(v => v.user.nickname), - lastMessage: result.Message[0], + users: result.Channel_users.map(res => ({ + userId: res.user.id, + email: res.user.email, + name: res.user.name, + nickname: res.user.nickname, + profileUrl: res.user.profile_url, + authProvider: res.user.auth_provider, + roleId: res.user.role_id, + })), + lastMessage: result.Message.map(res => ({ + type: res.type, + content: res.content, + channelId: res.channel_id, + date: res.created_at, + userId: res.user_id, + })), }; - const message = { code: 200, text: '데이터 패칭 성공', From 48b9e7a583699f29be0b9f7000cf17275a47b63c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 00:11:34 +0900 Subject: [PATCH 103/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index 5b0a9f5..cba02c0 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -34,6 +34,6 @@ export class ChatController { @Get('channels/:id') @UseGuards(JwtAuthGuard) async getChannel(@Req() req: any, @Param('id') channelId: number) { - return this.chatService.getChannel(req.user.user_id, +channelId); + return await this.chatService.getChannel(req.user.user_id, +channelId); } } From 7256c58f119b9bdc97eae6f2aeebc47493b34099 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 00:15:04 +0900 Subject: [PATCH 104/414] =?UTF-8?q?[Fix]=20=EC=B1=84=EB=84=90=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20lastMessage=20=EB=B0=B0=EC=97=B4=20->=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5bbbfac..2dadb6d 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -209,13 +209,13 @@ export class ChatService { authProvider: res.user.auth_provider, roleId: res.user.role_id, })), - lastMessage: result.Message.map(res => ({ - type: res.type, - content: res.content, - channelId: res.channel_id, - date: res.created_at, - userId: res.user_id, - })), + lastMessage: { + type: result.Message[0].type, + content: result.Message[0].content, + channelId: result.Message[0].channel_id, + date: result.Message[0].created_at, + userId: result.Message[0].user_id, + }, }; const message = { code: 200, From 565479b8ba23bc85663ac433fd71aa49dad1b0a3 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 00:16:53 +0900 Subject: [PATCH 105/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20channelAdded=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 6289ce6..e14ae46 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -56,7 +56,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channelId = await this.chatService.getChannelId(userId1, userId2); // 채널 객체 - const channel = { channelId }; + const channelData = await this.chatService.getChannel(userId1, channelId); + const channel = channelData.channel; // 채널에 유저 참여 client.join(channelId.toString()); From e439e76df0db8b8b0f89a1c89194bb75e76e79dd Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 00:32:59 +0900 Subject: [PATCH 106/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20FeedPost=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 591d4a5..59dee44 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -291,6 +291,7 @@ export class FeedService { // 댓글 등록 async createComment(userId, feedId, content) { try { + // 댓글 데이터 저장 await this.prisma.feedComment.create({ data: { user_id: userId, @@ -298,6 +299,13 @@ export class FeedService { content: content, }, }); + + // 피드 댓글 카운트 증가 + await this.prisma.feedPost.update({ + where: { id: feedId }, + data: { comment_count: { increment: 1 } }, + }); + return { success: true, message: '댓글 등록이 완료되었습니다.' }; } catch (err) { console.log(err); From 77c3b7dee76d25daa1216c5e5fc8ce09a611def1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 09:46:23 +0900 Subject: [PATCH 107/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9D=91=EB=8B=B5=20=EC=96=91=EC=8B=9D?= =?UTF-8?q?=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC,=20?= =?UTF-8?q?=EC=B1=84=EB=84=90=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B1=84=EB=84=90=20=EA=B0=9D=EC=B2=B4=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 139 +++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 2dadb6d..1482270 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -129,11 +129,14 @@ export class ChatService { where: { user_id: id }, select: { channel_id: true }, }); - const data = result.map(v => ({ - channelId: v.channel_id, - })); - return data; + const channels = []; + for (const res of result) { + const channel = await this.getChannleObj(res.channel_id); + channels.push(channel); + } + + return channels; } // 메세지 상태 업데이트 @@ -156,67 +159,8 @@ export class ChatService { throw new Error('권한 X'); } - // 채널 데이터 조회 - const result = await this.prisma.channel.findUnique({ - where: { id: channelId }, - include: { - Channel_users: { - select: { - user: { - select: { - id: true, - email: true, - name: true, - nickname: true, - profile_url: true, - auth_provider: true, - role_id: true, - }, - }, - }, - }, - Message: { - take: 1, - orderBy: { id: 'desc' }, - include: { - user: { - select: { - id: true, - email: true, - name: true, - nickname: true, - profile_url: true, - auth_provider: true, - role_id: true, - }, - }, - }, - }, - }, - }); + const channel = await this.getChannleObj(channelId); - // 채널 데이터 양식화 - const channel = { - channelId: result.id, - title: result.name, - type: result.Channel_users.length > 2 ? 'group' : 'private', - users: result.Channel_users.map(res => ({ - userId: res.user.id, - email: res.user.email, - name: res.user.name, - nickname: res.user.nickname, - profileUrl: res.user.profile_url, - authProvider: res.user.auth_provider, - roleId: res.user.role_id, - })), - lastMessage: { - type: result.Message[0].type, - content: result.Message[0].content, - channelId: result.Message[0].channel_id, - date: result.Message[0].created_at, - userId: result.Message[0].user_id, - }, - }; const message = { code: 200, text: '데이터 패칭 성공', @@ -227,6 +171,73 @@ export class ChatService { } } + // 채널 객체 리턴 로직 + async getChannleObj(channelId) { + // 채널 데이터 조회 + const result = await this.prisma.channel.findUnique({ + where: { id: channelId }, + include: { + Channel_users: { + select: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + profile_url: true, + auth_provider: true, + role_id: true, + }, + }, + }, + }, + Message: { + take: 1, + orderBy: { id: 'desc' }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + profile_url: true, + auth_provider: true, + role_id: true, + }, + }, + }, + }, + }, + }); + + // 채널 데이터 양식화 + const channel = { + channelId: result.id, + title: result.name, + type: result.Channel_users.length > 2 ? 'group' : 'private', + users: result.Channel_users.map(res => ({ + userId: res.user.id, + email: res.user.email, + name: res.user.name, + nickname: res.user.nickname, + profileUrl: res.user.profile_url, + authProvider: res.user.auth_provider, + roleId: res.user.role_id, + })), + lastMessage: { + type: result.Message[0].type, + content: result.Message[0].content, + channelId: result.Message[0].channel_id, + date: result.Message[0].created_at, + userId: result.Message[0].user_id, + }, + }; + + return channel; + } + // 채널 메세지 조회 async getMessages(userId, channelId, limit, currentPage) { try { From 54a74bfe908f08a6142f306178b0c71063b343b0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 11:06:19 +0900 Subject: [PATCH 108/414] [Fix] db update --- prisma/schema.prisma | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0e83570..95122ad 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model User { role_id Int introduce String? status_id Int + job_detail String? // "카테고리 / 직무명" 형식으로 저장 apply_count Int? @default(0) post_count Int? @default(0) push_alert Boolean @default(false) @@ -94,12 +95,10 @@ model ProgrammerData { } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int @unique - music_url String - platform String - music_data String? - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + music_url String + user User @relation(fields: [user_id], references: [id]) } model Status { @@ -136,10 +135,10 @@ model UserSkill { } model MyPageUserLink { - id Int @id @default(autoincrement()) - user_id Int - link String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + link String + user User @relation(fields: [user_id], references: [id]) @@index([user_id], map: "UserLink_user_id_fkey") } @@ -160,13 +159,13 @@ model FeedPost { id Int @id @default(autoincrement()) user_id Int title String - content String - thumbnail_url String + content String @db.Text + thumbnail_url String? created_at DateTime @default(now()) updated_at DateTime @updatedAt - view Int - comment_count Int - like_count Int + view Int @default(0) + comment_count Int @default(0) + like_count Int @default(0) Comments FeedComment[] Likes FeedLike[] user User @relation(fields: [user_id], references: [id]) From 5e7490b7889a89a64046128146aef3e13d3b7d4d Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 11:06:47 +0900 Subject: [PATCH 109/414] =?UTF-8?q?[Feat]=20user=20Profile=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=EB=93=A4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 76 ++++++- src/modules/user/user.service.ts | 340 ++++++++++++++++++++++++++-- 2 files changed, 392 insertions(+), 24 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index aa51cd6..87bb949 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -12,6 +12,9 @@ import { UploadedFile, BadRequestException, Delete, + Put, + HttpException, + HttpStatus, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; @@ -41,7 +44,24 @@ export class UserController { return this.userService.getUserFollowings(numUserId); } - @Get('setting') + @Post('projects') + async addProject(@Req() req, @Body() projectData: any) { + const userId = req.user?.user_id; + return this.userService.addProject(userId, projectData); + } + + @Put('projects/:projectId') + async updateProject( + @Req() req, + @Param('projectId') projectId: string, + @Body() projectData: any + ) { + const userId = req.user?.user_id; + const numProjectId = parseInt(projectId, 10); + return this.userService.updateProject(userId, numProjectId, projectData); + } + + @Get('profile/settings') async getUserSetting(@Req() req) { const userId = req.user?.user_id; return this.userService.getUserSetting(userId); @@ -60,7 +80,7 @@ export class UserController { } @Patch('profile/status') - async patchUserStatus(@Req() req, @Body('status') statusId: number) { + async patchUserStatus(@Req() req, @Body('statusId') statusId: number) { const userId = req.user?.user_id; return this.userService.patchUserStatus(userId, statusId); } @@ -68,7 +88,13 @@ export class UserController { @Post('profile/skills') async patchUserSkills(@Req() req, @Body('skills') skills: string[]) { const userId = req.user?.user_id; - return this.userService.patchUserSkills(userId, skills); + return this.userService.addUserSkills(userId, skills); + } + + @Delete('profile/skills') + async deleteUserSkills(@Req() req, @Body('skills') skills: string[]) { + const userId = req.user?.user_id; + return this.userService.deleteUserSkills(userId, skills); } @Patch('profile/image') @@ -93,18 +119,46 @@ export class UserController { }; } - @Delete('profile/skills') - async deleteUserSkills(@Req() req, @Body('skills') skills: string[]) { - const userId = req.user?.user_id; - return this.userService.deleteUserSkills(userId, skills); - } - @Patch('profile/notification') async patchUserNotification( @Req() req, - @Body('notification') notification: boolean + @Body('notification') + notifications: { + pushAlert: boolean; + followingAlert: boolean; + projectAlert: boolean; + } ) { const userId = req.user?.user_id; - return this.userService.patchUserNotification(userId, notification); + return this.userService.patchUserNotification(userId, notifications); + } + + @Post('profile/links') + async addUserLinks(@Req() req, @Body('links') links: { url: string }[]) { + const userId = req.user?.user_id; + return this.userService.addUserLinks(userId, links); + } + + @Delete('profile/links') + async deleteUserLinks(@Req() req, @Body('linkIds') linkIds: number[]) { + const userId = req.user?.user_id; + return this.userService.deleteUserLinks(userId, linkIds); + } + + @Delete('account') + async deleteAccount(@Req() req) { + const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 + if (!userId) { + throw new HttpException( + '유효하지 않은 사용자입니다.', + HttpStatus.FORBIDDEN + ); + } + + await this.userService.deleteAccount(userId); + + return { + message: '계정이 성공적으로 삭제되었습니다.', + }; } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 00c245f..34ed841 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; import { S3Service } from '@src/s3/s3.service'; @@ -148,24 +152,179 @@ export class UserService { })); } - async patchUserNickname(userId: number, nickname: string) { - return Promise.resolve(undefined); + async addProject(userId: number, projectData: any) { + const { title, description, links } = projectData; + + // 작업물 추가 + const newProject = await this.prisma.myPageProject.create({ + data: { + user_id: userId, + title, + description, + ProjectLinks: { + create: links.map(link => ({ + url: link.url, + type_id: link.typeId, // LinkType의 ID를 사용 + })), + }, + }, + include: { + ProjectLinks: { + include: { type: true }, + }, + }, + }); + + return { + id: newProject.id, + title: newProject.title, + description: newProject.description, + links: newProject.ProjectLinks.map(link => ({ + id: link.id, + url: link.url, + type: link.type.name, + })), + }; + } + + async updateProject(userId: number, projectId: number, projectData: any) { + const { title, description, links } = projectData; + + // 기존 프로젝트 확인 + const existingProject = await this.prisma.myPageProject.findFirst({ + where: { + id: projectId, + user_id: userId, + }, + }); + + if (!existingProject) { + throw new NotFoundException('작업물을 찾을 수 없습니다.'); + } + + // 프로젝트 업데이트 + const updatedProject = await this.prisma.myPageProject.update({ + where: { id: projectId }, + data: { + title, + description, + ProjectLinks: { + deleteMany: {}, // 기존 링크 삭제 + create: links.map(link => ({ + url: link.url, + type_id: link.typeId, // LinkType의 ID를 사용 + })), + }, + }, + include: { + ProjectLinks: { + include: { type: true }, + }, + }, + }); + + return { + id: updatedProject.id, + title: updatedProject.title, + description: updatedProject.description, + links: updatedProject.ProjectLinks.map(link => ({ + id: link.id, + url: link.url, + type: link.type.name, + })), + }; } async getUserSetting(userId: number) { - return Promise.resolve(undefined); + // 사용자 정보를 가져옵니다. + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { + UserLinks: true, // 링크 정보만 포함 + UserSkills: { + include: { + skill: true, // Skill 정보를 포함 + }, + }, + status: true, // 상태 정보 포함 + }, + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // 데이터 반환 + return { + nickname: user.nickname, + profileUrl: user.profile_url, + introduce: user.introduce, + status: user.status?.name, + links: user.UserLinks.map(link => ({ + id: link.id, + url: link.link, // 링크 정보만 반환 + })), + skills: user.UserSkills.map(skill => skill.skill.name), // 기술 스택 + notifications: { + pushAlert: user.push_alert, + followingAlert: user.following_alert, + projectAlert: user.project_alert, + }, + }; } - async patchUserIntroduce(userId: number, introduce: string) { - return Promise.resolve(undefined); + async patchUserNickname(userId: number, nickname: string) { + if (!nickname || nickname.trim().length === 0) { + throw new BadRequestException('닉네임은 비어 있을 수 없습니다.'); + } + + // 닉네임 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { + nickname, + }, + }); + + return { + message: '닉네임이 성공적으로 업데이트되었습니다.', + nickname: updatedUser.nickname, + }; } - async patchUserStatus(userId: number, status_id: number) { - return Promise.resolve(undefined); + async patchUserIntroduce(userId: number, introduce: string) { + // 사용자의 한 줄 소개 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { introduce }, + }); + + return { + message: '사용자의 소개가 성공적으로 업데이트되었습니다.', + introduce: updatedUser.introduce, + }; } - async patchUserSkills(userId: number, skills: string[]) { - return Promise.resolve(undefined); + async patchUserStatus(userId: number, statusId: number) { + // Status ID가 유효한지 확인 + const status = await this.prisma.status.findUnique({ + where: { id: statusId }, + }); + + if (!status) { + throw new NotFoundException('유효하지 않은 상태 ID입니다.'); + } + + // 사용자의 상태 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { status_id: statusId }, + }); + + return { + message: '사용자의 상태가 성공적으로 업데이트되었습니다.', + status: status.name, + }; } async patchProfileImage( @@ -186,11 +345,166 @@ export class UserService { }); } + async patchUserNotification( + userId: number, + notifications: { + pushAlert: boolean; + followingAlert: boolean; + projectAlert: boolean; + } + ) { + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { + push_alert: notifications.pushAlert, + following_alert: notifications.followingAlert, + project_alert: notifications.projectAlert, + }, + }); + + return { + message: '알림 설정이 성공적으로 업데이트되었습니다.', + notifications: { + pushAlert: updatedUser.push_alert, + followingAlert: updatedUser.following_alert, + projectAlert: updatedUser.project_alert, + }, + }; + } + + async addUserSkills(userId: number, skills: string[]) { + // 기존에 없는 스킬만 추가 + const existingSkills = await this.prisma.skill.findMany({ + where: { name: { in: skills } }, + }); + + const existingSkillNames = existingSkills.map(skill => skill.name); + + // 새로운 스킬만 추가 + const newSkills = skills.filter( + skill => !existingSkillNames.includes(skill) + ); + + // 새 스킬 DB에 추가 + const createdSkills = await Promise.all( + newSkills.map(skill => + this.prisma.skill.upsert({ + where: { name: skill }, + update: {}, + create: { name: skill }, + }) + ) + ); + + // User와 Skill 관계 연결 + const skillIds = [...existingSkills, ...createdSkills].map( + skill => skill.id + ); + await Promise.all( + skillIds.map(skillId => + this.prisma.userSkill.upsert({ + where: { user_id_skill_id: { user_id: userId, skill_id: skillId } }, + update: {}, + create: { user_id: userId, skill_id: skillId }, + }) + ) + ); + + return { + message: '기술 스택이 성공적으로 추가되었습니다', + skills: skills, + }; + } + async deleteUserSkills(userId: number, skills: string[]) { - return Promise.resolve(undefined); + // 스킬 ID 가져오기 + const skillRecords = await this.prisma.skill.findMany({ + where: { name: { in: skills } }, + }); + + const skillIds = skillRecords.map(skill => skill.id); + + // User와 Skill 관계 삭제 + await this.prisma.userSkill.deleteMany({ + where: { + user_id: userId, + skill_id: { in: skillIds }, + }, + }); + + return { + message: '기술 스택이 성공적으로 삭제되었습니다', + skills, + }; } - async patchUserNotification(userId: number, notification: boolean) { - return Promise.resolve(undefined); + async addUserLinks(userId: number, links: { url: string }[]) { + const createdLinks = await this.prisma.myPageUserLink.createMany({ + data: links.map(link => ({ + user_id: userId, + link: link.url, + })), + }); + + return { + message: '링크가 성공적으로 추가되었습니다.', + count: createdLinks.count, + }; + } + + async deleteUserLinks(userId: number, linkIds: number[]) { + const deletedLinks = await this.prisma.myPageUserLink.deleteMany({ + where: { + id: { in: linkIds }, + user_id: userId, + }, + }); + + return { + message: '링크가 성공적으로 삭제되었습니다.', + count: deletedLinks.count, + }; + } + + async deleteAccount(userId: number) { + // 유저가 존재하지 않을 경우 처리 + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // 관련 데이터 삭제 (예: 팔로우, 프로젝트, 댓글 등) + await this.prisma.$transaction([ + this.prisma.follows.deleteMany({ + where: { + OR: [{ following_user_id: userId }, { followed_user_id: userId }], + }, + }), + this.prisma.myPageUserLink.deleteMany({ + where: { user_id: userId }, + }), + this.prisma.projectSave.deleteMany({ + where: { user_id: userId }, + }), + this.prisma.feedLike.deleteMany({ + where: { user_id: userId }, + }), + this.prisma.feedComment.deleteMany({ + where: { user_id: userId }, + }), + this.prisma.feedPost.deleteMany({ + where: { user_id: userId }, + }), + this.prisma.user.delete({ + where: { id: userId }, + }), + ]); + + return { + message: '사용자와 관련된 모든 데이터가 삭제되었습니다.', + }; } } From a971e62138091811c47ca5cebe679690ea3ef073 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 11:12:27 +0900 Subject: [PATCH 110/414] =?UTF-8?q?[Feat]=20user=20Profile=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=EB=93=A4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 34ed841..83f0a0f 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -61,7 +61,6 @@ export class UserService { if (user.role.name === 'Artist') { specificData = { musicUrl: user.ArtistData?.music_url, - platform: user.ArtistData?.platform, }; } else if ( user.role.name === 'Programmer' || From 6da4cd24ceaea6eeac3d3b4f4af66ac14e51142f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 14:16:28 +0900 Subject: [PATCH 111/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EC=B5=9C=EC=8B=A0=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 5 +++-- src/feed/feed.service.ts | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index b8cbaa0..b9f3596 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -4,6 +4,7 @@ import { Get, Param, Post, + Query, Req, UseGuards, } from '@nestjs/common'; @@ -18,8 +19,8 @@ export class FeedController { // 메인 페이지 조회 @Get() @UseGuards(OptionalAuthGuard) - async getAllFeed(@Req() req) { - return this.feedService.getAllFeeds(req.user); + async getAllFeed(@Req() req, @Query('latest') latest: boolean) { + return this.feedService.getAllFeeds(req.user, latest); } // 피드 조회 (게시글) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 59dee44..68046c8 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -9,10 +9,12 @@ export class FeedService { /** 피드 전체 조회 / 남은 구현 과제 * 최신순 정렬 **/ - async getAllFeeds(user) { + async getAllFeeds(user, latest) { try { const userId = user ? user.user_id : 0; + const orderKey = latest ? 'created_at' : 'like_count'; + const result = await this.prisma.feedPost.findMany({ include: { Likes: { @@ -38,7 +40,7 @@ export class FeedService { }, }, // 인기순 정렬 : 좋아요 순 - orderBy: { like_count: 'desc' }, + orderBy: { [orderKey]: 'desc' }, }); const posts = result.map(post => ({ From 5189949985b35dc01ee2d6583f88166e19c4d28c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 14:24:46 +0900 Subject: [PATCH 112/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91=EC=8B=9D=ED=99=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC,=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 63 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 68046c8..331b482 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -25,6 +25,7 @@ export class FeedService { id: true, name: true, email: true, + nickname: true, role: { select: { name: true }, }, @@ -43,22 +44,11 @@ export class FeedService { orderBy: { [orderKey]: 'desc' }, }); - const posts = result.map(post => ({ - userId: post.user.id, - userName: post.user.name, - userRole: post.user.role.name, - userProfileUrl: post.user.profile_url, - title: post.title, - postId: post.id, - thumnailUrl: post.thumbnail_url, - content: post.content, - tags: post.Tags.map(v => v.tag.name), - commentCount: post.comment_count, - likeCount: post.like_count, - viewCount: post.view, - createdAt: post.created_at, - liked: !!post.Likes.length, - })); + const posts = []; + for (const res of result) { + const post = await this.getPostObj(res); + posts.push(post); + } return { posts }; } catch (err) { @@ -85,6 +75,7 @@ export class FeedService { id: true, name: true, email: true, + nickname: true, role: { select: { name: true }, }, @@ -108,22 +99,8 @@ export class FeedService { ); } - const post = { - userId: result.user.id, - userName: result.user.name, - userRole: result.user.role.name, - userProfileUrl: result.user.profile_url, - title: result.title, - postId: result.id, - thumnailUrl: result.thumbnail_url, - content: result.content, - tags: result.Tags.map(v => v.tag.name), - commentCount: result.comment_count, - likeCount: result.like_count, - viewCount: result.view, - createdAt: result.created_at, - Liked: !!result.Likes.length, - }; + const post = await this.getPostObj(result); + return { post }; } catch (err) { console.log(err); @@ -138,6 +115,28 @@ export class FeedService { } } + // 피드 데이터 응답 양식 + async getPostObj(result) { + const post = { + userId: result.user.id, + userName: result.user.name, + userNickname: result.user.nickname, + userRole: result.user.role.name, + userProfileUrl: result.user.profile_url, + title: result.title, + postId: result.id, + thumnailUrl: result.thumbnail_url, + content: result.content, + tags: result.Tags.map(v => v.tag.name), + commentCount: result.comment_count, + likeCount: result.like_count, + viewCount: result.view, + createdAt: result.created_at, + Liked: !!result.Likes.length, + }; + return post; + } + // 피드 개별 조회 (댓글) async getFeedComments(feedId) { try { From b814673fc447703f91f7419149c17c97fa09eb71 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 14:29:55 +0900 Subject: [PATCH 113/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9D=91=EB=8B=B5=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 331b482..33bd870 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -6,9 +6,7 @@ import * as cheerio from 'cheerio'; @Injectable() export class FeedService { constructor(private readonly prisma: PrismaService) {} - /** 피드 전체 조회 / 남은 구현 과제 - * 최신순 정렬 - **/ + // 피드 전체 조회 async getAllFeeds(user, latest) { try { const userId = user ? user.user_id : 0; @@ -132,7 +130,7 @@ export class FeedService { likeCount: result.like_count, viewCount: result.view, createdAt: result.created_at, - Liked: !!result.Likes.length, + isLiked: !!result.Likes.length, }; return post; } From a309318f42986d1d7598700cd02291ed563953a2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 15:54:44 +0900 Subject: [PATCH 114/414] =?UTF-8?q?[Feat]=20s3=20route=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/s3/s3.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s3/s3.service.ts b/src/s3/s3.service.ts index 1b0775f..927bddc 100644 --- a/src/s3/s3.service.ts +++ b/src/s3/s3.service.ts @@ -18,7 +18,7 @@ export class S3Service { fileType: string ): Promise { try { - const fileName = `mypli_users/profile_${crypto.randomUUID()}.${fileType}`; + const fileName = `pad_users/profile_${crypto.randomUUID()}.${fileType}`; const uploadResult = await this.s3 .upload({ Bucket: this.bucketName, From ec6fe6b963bf11f6f12a4629c5faba6eb913c6c9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 15:55:00 +0900 Subject: [PATCH 115/414] [Fix] Scheme Update --- prisma/schema.prisma | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 95122ad..149095f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -111,7 +111,8 @@ model Resume { id Int @id @default(autoincrement()) user_id Int title String - introduce String + portfolio_url String? + detail String @db.Text user User @relation(fields: [user_id], references: [id]) @@index([user_id], map: "Resume_user_id_fkey") @@ -239,7 +240,7 @@ model ProjectPost { Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] - + created_at DateTime @default(now()) @@index([user_id], map: "ProjectPost_user_id_fkey") @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") } From 12cd362ccac23d1422473fe5d821fcd9740625e3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 15:55:40 +0900 Subject: [PATCH 116/414] =?UTF-8?q?[Feat]=20=EC=83=81=EC=84=B8=20=EC=A7=81?= =?UTF-8?q?=EB=AC=B4=20=EB=B3=80=EA=B2=BD,=20=EC=9D=B4=EB=A0=A5=EC=84=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EC=88=98=EC=A0=95,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20=ED=94=BC=EB=93=9C=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=EC=85=98=20=ED=97=88=EB=B8=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 87bb949..ee316da 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -15,6 +15,7 @@ import { Put, HttpException, HttpStatus, + Query, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; @@ -85,6 +86,16 @@ export class UserController { return this.userService.patchUserStatus(userId, statusId); } + @Patch('profile/job') + async updateUserJobDetail( + @Req() req, + @Body('category') category: string, + @Body('jobDetail') jobDetail: string + ) { + const userId = req.user?.user_id; + return this.userService.updateUserJobDetail(userId, category, jobDetail); + } + @Post('profile/skills') async patchUserSkills(@Req() req, @Body('skills') skills: string[]) { const userId = req.user?.user_id; @@ -161,4 +172,55 @@ export class UserController { message: '계정이 성공적으로 삭제되었습니다.', }; } + + @Get('profile/resume/:userId') + async getUserResume(@Req() req, @Param('userId') targetUserId: number) { + const loggedInUserId = req.user?.user_id; + return this.userService.getUserResume(loggedInUserId, targetUserId); + } + + @Post('profile/resume') + async createUserResume( + @Req() req, + @Body() body: { title: string; portfolioUrl?: string; detail: string } + ) { + const userId = req.user?.user_id; + return this.userService.createUserResume(userId, body); + } + + // 지원서 수정 + @Patch('profile/resume/:resumeId') + async updateUserResume( + @Req() req, + @Param('resumeId') resumeId: number, + @Body() body: { title?: string; portfolioUrl?: string; detail?: string } + ) { + const userId = req.user?.user_id; + return this.userService.updateUserResume(userId, resumeId, body); + } + + @Delete(':resumeId') + async deleteUserResume(@Req() req, @Param('resumeId') resumeId: number) { + const userId = req.user?.user_id; + return this.userService.deleteUserResume(userId, resumeId); + } + + @Get(':userId/feeds') + async getUserFeedPosts( + @Param('userId') userId: number, + @Query('page') page: number = 1, + @Query('limit') limit: number = 10 + ) { + return this.userService.getFeeds(userId, page, limit); + } + + @Get(':userId/connection-hub') + async getUserConnectionHubProjects( + @Param('userId') userId: number, + @Query('type') type: 'applied' | 'created', + @Query('page') page: number = 1, + @Query('limit') limit: number = 10 + ) { + return this.userService.getConnectionHubProjects(userId, type, page, limit); + } } From 137245d917a959f9c9d5b2fe9e58b54dee764adf Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 17 Jan 2025 15:55:46 +0900 Subject: [PATCH 117/414] =?UTF-8?q?[Feat]=20=EC=83=81=EC=84=B8=20=EC=A7=81?= =?UTF-8?q?=EB=AC=B4=20=EB=B3=80=EA=B2=BD,=20=EC=9D=B4=EB=A0=A5=EC=84=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EC=88=98=EC=A0=95,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20=ED=94=BC=EB=93=9C=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=EC=85=98=20=ED=97=88=EB=B8=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 276 ++++++++++++++++++++++++++++++- 1 file changed, 275 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 83f0a0f..1f2a918 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -67,7 +68,7 @@ export class UserService { user.role.name === 'Designer' ) { specificData = { - githubUsername: user.ProgrammerData?.github_username, + githubUsername: user.ProgrammerData?.github_username || null, myPageProjects: user.MyPageProject ? { title: user.MyPageProject.title, @@ -291,6 +292,24 @@ export class UserService { }; } + async updateUserJobDetail( + userId: number, + category: string, + jobDetail: string + ) { + const jobDetailString = `${category} / ${jobDetail}`; + + const user = await this.prisma.user.update({ + where: { id: userId }, + data: { job_detail: jobDetailString }, + }); + + return { + message: '직무 정보가 성공적으로 업데이트되었습니다.', + jobDetail: user.job_detail, + }; + } + async patchUserIntroduce(userId: number, introduce: string) { // 사용자의 한 줄 소개 업데이트 const updatedUser = await this.prisma.user.update({ @@ -506,4 +525,259 @@ export class UserService { message: '사용자와 관련된 모든 데이터가 삭제되었습니다.', }; } + + async getUserResume(loggedInUserId: number, targetUserId: number) { + // 지원서와 사용자 정보를 함께 조회 + const resume = await this.prisma.resume.findFirst({ + where: { user_id: targetUserId }, + select: { + title: true, + portfolio_url: true, + detail: true, + user: { + select: { + id: true, + nickname: true, + job_detail: true, // 직무 상세 + UserSkills: { + // 기술 스택 + include: { + skill: true, // 기술 이름 + }, + }, + }, + }, + }, + }); + + if (!resume) { + throw new NotFoundException('지원서를 찾을 수 없습니다.'); + } + + // 자기 자신의 프로필인지 확인 + const isOwnProfile = loggedInUserId === targetUserId; + + // 반환 데이터 구성 + return { + title: resume.title, + jobDetail: resume.user.job_detail, // 직무 상세 + skills: resume.user.UserSkills.map(userSkill => userSkill.skill.name), // 기술 스택 이름 리스트 + portfolioUrl: resume.portfolio_url, + detail: resume.detail, + user: { + id: resume.user.id, + nickname: resume.user.nickname, + }, + isOwnProfile, // 본인 프로필 여부 + }; + } + + // 지원서 생성 + async createUserResume( + userId: number, + data: { title: string; portfolioUrl?: string; detail: string } + ) { + const newResume = await this.prisma.resume.create({ + data: { + user_id: userId, + title: data.title, + portfolio_url: data.portfolioUrl, + detail: data.detail, // detail 정보를 introduce 필드에 저장 + }, + }); + + return newResume; + } + + // 지원서 수정 + async updateUserResume( + userId: number, + resumeId: number, + data: { title?: string; portfolioUrl?: string; detail?: string } + ) { + // 해당 지원서가 본인의 것인지 확인 + const resume = await this.prisma.resume.findUnique({ + where: { id: resumeId }, + }); + + if (!resume) { + throw new NotFoundException('지원서를 찾을 수 없습니다.'); + } + + if (resume.user_id !== userId) { + throw new ForbiddenException('권한이 없습니다.'); + } + + // 업데이트 로직 + const updatedResume = await this.prisma.resume.update({ + where: { id: resumeId }, + data: { + title: data.title, + portfolio_url: data.portfolioUrl, + detail: data.detail, + }, + }); + + return updatedResume; + } + + // 지원서 삭제 + async deleteUserResume(userId: number, resumeId: number) { + // 해당 지원서가 본인의 것인지 확인 + const resume = await this.prisma.resume.findUnique({ + where: { id: resumeId }, + }); + + if (!resume) { + throw new NotFoundException('지원서를 찾을 수 없습니다.'); + } + + if (resume.user_id !== userId) { + throw new ForbiddenException('권한이 없습니다.'); + } + + // 삭제 로직 + await this.prisma.resume.delete({ + where: { id: resumeId }, + }); + + return { message: '지원서가 삭제되었습니다.' }; + } + + async getFeeds(userId: number, page: number = 1, limit: number = 10) { + // Offset 계산 + const offset = (page - 1) * limit; + + // 피드 조회 + const feeds = await this.prisma.feedPost.findMany({ + skip: offset, + take: limit, + orderBy: { created_at: 'desc' }, // 최신 순 정렬 + include: { + user: { + select: { + id: true, + nickname: true, + profile_url: true, + }, + }, + Tags: { + include: { + tag: true, // 태그 이름 가져오기 + }, + }, + }, + }); + + // 총 피드 개수 (페이지네이션 용) + const totalCount = await this.prisma.feedPost.count(); + + // 반환 데이터 구성 + return { + feeds: feeds.map(feed => ({ + id: feed.id, + title: feed.title, + content: feed.content, + thumbnailUrl: feed.thumbnail_url, + createdAt: feed.created_at, + view: feed.view, + likeCount: feed.like_count, + commentCount: feed.comment_count, + user: { + id: feed.user.id, + nickname: feed.user.nickname, + profileUrl: feed.user.profile_url, + }, + tags: feed.Tags.map(tag => tag.tag.name), // 태그 리스트 + })), + totalCount, + currentPage: page, + totalPages: Math.ceil(totalCount / limit), + }; + } + + async getConnectionHubProjects( + userId: number, + type: 'applied' | 'created', + page: number = 1, + limit: number = 10 + ) { + const offset = (page - 1) * limit; + + let projectsQuery, totalCountQuery; + + // 쿼리 조건 설정 + if (type === 'applied') { + projectsQuery = this.prisma.userApplyProject.findMany({ + where: { user_id: userId }, + skip: offset, + take: limit, + include: { + post: { + include: { + Tags: { + include: { tag: true }, // 태그 정보 포함 + }, + }, + }, + }, + orderBy: { + post: { + created_at: 'desc', // post의 created_at 기준으로 정렬 + }, + }, + }); + + totalCountQuery = this.prisma.userApplyProject.count({ + where: { user_id: userId }, + }); + } else if (type === 'created') { + projectsQuery = this.prisma.projectPost.findMany({ + where: { user_id: userId }, + skip: offset, + take: limit, + include: { + Tags: { + include: { tag: true }, // 태그 정보 포함 + }, + }, + orderBy: { created_at: 'desc' }, + }); + + totalCountQuery = this.prisma.projectPost.count({ + where: { user_id: userId }, + }); + } else { + throw new BadRequestException('유효하지 않은 타입입니다.'); + } + + // 쿼리 실행 + const [projects, totalCount] = await Promise.all([ + projectsQuery, + totalCountQuery, + ]); + + // 데이터 매핑 + const formattedProjects = projects.map(project => { + const projectData = type === 'applied' ? project.post : project; + return { + projectPostId: projectData.id, + title: projectData.title, + content: projectData.content, + thumbnailUrl: projectData.thumbnail_url, + startDate: projectData.start_date, + duration: `${projectData.unit}`, + recruiting: projectData.recruiting, + view: projectData.view, + tags: projectData.Tags.map(tag => tag.tag.name), + }; + }); + + return { + projects: formattedProjects, + totalCount, + currentPage: page, + totalPages: Math.ceil(totalCount / limit), + }; + } } From bde60e377608cbdb9a3f54fd3dcdfa9d4d336e80 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 16:29:16 +0900 Subject: [PATCH 118/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 9 +++++++ src/feed/feed.service.ts | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index b9f3596..3c89f5e 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, Param, Post, @@ -52,6 +53,14 @@ export class FeedController { return this.feedService.createPost(createPostDto, userId); } + // 피드 삭제 + @Delete(':id') + @UseGuards(JwtAuthGuard) + async deletePost(@Req() req, @Param('id') feedId: number) { + const userId = req.user.user_id; + return this.feedService.deletePost(userId, feedId); + } + // 댓글 등록 @Post(':id/comment') @UseGuards(JwtAuthGuard) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 33bd870..fbeb5a1 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -276,6 +276,58 @@ export class FeedService { } } + // 피드 삭제 + async deletePost(userId, feedId) { + try { + // 권한 확인 + const auth = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + select: { user_id: true }, + }); + if (!auth) { + throw new HttpException( + '게시글을 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + // 권한 예외 처리 + if (userId !== auth.user_id) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + + // 트랜잭션으로 한번에 처리 + // 태그(매핑 테이블), 댓글, 좋아요(매핑 테이블), 게시글 삭제 + await this.prisma.$transaction([ + this.prisma.feedPostTag.deleteMany({ + where: { post_id: feedId }, + }), + + this.prisma.feedComment.deleteMany({ + where: { post_id: feedId }, + }), + + this.prisma.feedLike.deleteMany({ + where: { post_id: feedId }, + }), + + this.prisma.feedPost.delete({ + where: { id: feedId }, + }), + ]); + + return { success: true, message: '피드가 삭제되었습니다.' }; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } + + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + // 썸네일 추출 async getThumnailUrl(text) { try { From 8f21de91446b5b9ad602b306f95b0212ddbca530 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 16:44:08 +0900 Subject: [PATCH 119/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 12 +++++++++ src/feed/feed.service.ts | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 3c89f5e..3c9a5df 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -72,4 +72,16 @@ export class FeedController { const userId = req.user.user_id; return this.feedService.createComment(userId, feedId, comment.content); } + + // 댓글 삭제 + @Delete(':id/comment/:commentId') + @UseGuards(JwtAuthGuard) + async deleteComment( + @Req() req, + @Param('id') feedId: number, + @Param('commentId') commentId: number + ) { + const userId = req.user.user_id; + return this.feedService.deleteComment(userId, feedId, commentId); + } } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index fbeb5a1..af8fa57 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -366,4 +366,54 @@ export class FeedService { ); } } + + // 댓글 삭제 + async deleteComment(userId, feedId, commentId) { + try { + // 권한 확인 + const auth = await this.prisma.feedComment.findUnique({ + where: { id: commentId }, + select: { user_id: true, post_id: true }, + }); + + // 권한 예외 처리 + if (!auth) { + throw new HttpException( + '게시글을 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + + if (feedId !== auth.post_id) { + throw new HttpException('잘못된 요청입니다', HttpStatus.BAD_REQUEST); + } + + if (userId !== auth.user_id) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + + await this.prisma.$transaction([ + // 댓글 삭제 + this.prisma.feedComment.delete({ + where: { id: commentId }, + }), + // 피드 댓글 수 감소 + this.prisma.feedPost.update({ + where: { id: feedId }, + data: { comment_count: { decrement: 1 } }, + }), + ]); + + return { success: true, message: '댓글이 삭제되었습니다.' }; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } + + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } } From 099e8a8c83672b0a4e5c05d5b6db43ae636a0214 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 17:31:56 +0900 Subject: [PATCH 120/414] =?UTF-8?q?[Refactor]=20post=20->=20feed=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 10 +++++----- src/feed/feed.service.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 3c9a5df..8bbaf2d 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -42,23 +42,23 @@ export class FeedController { @UseGuards(JwtAuthGuard) async handleFeedLikes(@Req() req, @Param('id') feedId: number) { const userId = req.user.user_id; - return await this.feedService.handlePostLikes(feedId, userId); + return await this.feedService.handleFeedLikes(feedId, userId); } // 피드 등록 @Post() @UseGuards(JwtAuthGuard) - async createPost(@Req() req, @Body() createPostDto: CreatePostDto) { + async createFeed(@Req() req, @Body() createPostDto: CreatePostDto) { const userId = req.user.user_id; - return this.feedService.createPost(createPostDto, userId); + return this.feedService.createFeed(createPostDto, userId); } // 피드 삭제 @Delete(':id') @UseGuards(JwtAuthGuard) - async deletePost(@Req() req, @Param('id') feedId: number) { + async deleteFeed(@Req() req, @Param('id') feedId: number) { const userId = req.user.user_id; - return this.feedService.deletePost(userId, feedId); + return this.feedService.deleteFeed(userId, feedId); } // 댓글 등록 diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index af8fa57..6d021d1 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -189,7 +189,7 @@ export class FeedService { } // 피드 좋아요 추가/제거 - async handlePostLikes(feedId, userId) { + async handleFeedLikes(feedId, userId) { try { const exist = await this.prisma.feedLike.findMany({ where: { @@ -237,7 +237,7 @@ export class FeedService { } // 피드 등록 - async createPost(createPostDto: CreatePostDto, userId: number) { + async createFeed(createPostDto: CreatePostDto, userId: number) { const { title, tags, content } = createPostDto; try { // 썸네일 url 추출 @@ -277,7 +277,7 @@ export class FeedService { } // 피드 삭제 - async deletePost(userId, feedId) { + async deleteFeed(userId, feedId) { try { // 권한 확인 const auth = await this.prisma.feedPost.findUnique({ From ea28751e87d79492ab570cd85b2e710d11893598 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 17:38:03 +0900 Subject: [PATCH 121/414] =?UTF-8?q?[Refactor]=20create-post.dto=20->=20fee?= =?UTF-8?q?d.dto=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/{create-post.dto.ts => feed.dto.ts} | 2 +- src/feed/feed.controller.ts | 6 +++--- src/feed/feed.service.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/feed/dto/{create-post.dto.ts => feed.dto.ts} (89%) diff --git a/src/feed/dto/create-post.dto.ts b/src/feed/dto/feed.dto.ts similarity index 89% rename from src/feed/dto/create-post.dto.ts rename to src/feed/dto/feed.dto.ts index 923f3c4..076bfd0 100644 --- a/src/feed/dto/create-post.dto.ts +++ b/src/feed/dto/feed.dto.ts @@ -1,6 +1,6 @@ import { IsString, IsArray, ArrayNotEmpty, IsNotEmpty } from 'class-validator'; -export class CreatePostDto { +export class FeedDto { @IsString() @IsNotEmpty() title: string; diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 8bbaf2d..89390d1 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -12,7 +12,7 @@ import { import { FeedService } from './feed.service'; import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; -import { CreatePostDto } from './dto/create-post.dto'; +import { FeedDto } from './dto/feed.dto'; @Controller('feed') export class FeedController { @@ -48,9 +48,9 @@ export class FeedController { // 피드 등록 @Post() @UseGuards(JwtAuthGuard) - async createFeed(@Req() req, @Body() createPostDto: CreatePostDto) { + async createFeed(@Req() req, @Body() feedDto: FeedDto) { const userId = req.user.user_id; - return this.feedService.createFeed(createPostDto, userId); + return this.feedService.createFeed(feedDto, userId); } // 피드 삭제 diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 6d021d1..6be89a7 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; -import { CreatePostDto } from './dto/create-post.dto'; +import { FeedDto } from './dto/feed.dto'; import * as cheerio from 'cheerio'; @Injectable() @@ -237,8 +237,8 @@ export class FeedService { } // 피드 등록 - async createFeed(createPostDto: CreatePostDto, userId: number) { - const { title, tags, content } = createPostDto; + async createFeed(feedDto: FeedDto, userId: number) { + const { title, tags, content } = feedDto; try { // 썸네일 url 추출 const thumnailUrl = await this.getThumnailUrl(content); From e10e69b9ba29d87b4584f37438a86d67b80a05bc Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 19:16:41 +0900 Subject: [PATCH 122/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 12 ++++++++ src/feed/feed.service.ts | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 89390d1..4811505 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -4,6 +4,7 @@ import { Delete, Get, Param, + Patch, Post, Query, Req, @@ -53,6 +54,17 @@ export class FeedController { return this.feedService.createFeed(feedDto, userId); } + @Patch(':id') + @UseGuards(JwtAuthGuard) + async updateFeed( + @Req() req, + @Body() feedDto: FeedDto, + @Param('id') feedId: number + ) { + const userId = req.user.user_id; + return this.feedService.updateFeed(feedDto, feedId, userId); + } + // 피드 삭제 @Delete(':id') @UseGuards(JwtAuthGuard) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 6be89a7..9f015d8 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -276,6 +276,62 @@ export class FeedService { } } + // 피드 수정 + async updateFeed(feedDto: FeedDto, feedId: number, userId: number) { + try { + const { title, tags, content } = feedDto; + + const thumnailUrl = (await this.getThumnailUrl(content)) || null; + + // 권한 확인 + const originData = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + select: { + user_id: true, + Tags: { select: { tag: { select: { name: true } } } }, + }, + }); + + if (originData.user_id !== userId) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + + // 태그 이름으로 태그 id 조회 + const tagIds = await this.prisma.feedTag.findMany({ + where: { name: { in: tags } }, + select: { id: true }, + }); + + const tagData = tagIds.map(tag => ({ + post_id: feedId, + tag_id: tag.id, + })); + + await this.prisma.feedPost.update({ + where: { id: feedId }, + data: { + title, + content, + thumbnail_url: thumnailUrl, + Tags: { + deleteMany: {}, + + create: tagData.map(tag => ({ tag_id: tag.tag_id })), + }, + }, + }); + } catch (err) { + if (err instanceof HttpException) { + throw err; + } + + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + // 피드 삭제 async deleteFeed(userId, feedId) { try { From 22c0c54ad07fdad87f3bc92067e2c84f582a6392 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 19:19:58 +0900 Subject: [PATCH 123/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=8D=B0=EC=BD=94=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B3=80=EA=B2=BD;=20Patch=20->=20Put?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 4811505..353aa1c 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -4,8 +4,8 @@ import { Delete, Get, Param, - Patch, Post, + Put, Query, Req, UseGuards, @@ -54,7 +54,8 @@ export class FeedController { return this.feedService.createFeed(feedDto, userId); } - @Patch(':id') + // 피드 수정 + @Put(':id') @UseGuards(JwtAuthGuard) async updateFeed( @Req() req, From ebe4a10a88a00232ef84b4e960c1fe95d99e3ab8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 19:22:56 +0900 Subject: [PATCH 124/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9D=91=EB=8B=B5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 9f015d8..aac0d6d 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -320,6 +320,8 @@ export class FeedService { }, }, }); + + return { success: true, message: '피드 수정이 완료되었습니다.' }; } catch (err) { if (err instanceof HttpException) { throw err; From e5764f25a9cda54645313101d491770c51a2cbcb Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 17 Jan 2025 19:28:36 +0900 Subject: [PATCH 125/414] [Feat] Prisma db pull --- prisma/schema.prisma | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c0cc557..e29b273 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,7 @@ model User { name String nickname String auth_provider String - profile_url String? @db.Text + profile_url String? role_id Int introduce String? status_id Int @@ -25,6 +25,7 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? + job_detail String? ArtistData ArtistData? Channel_users Channel_users[] FeedComments FeedComment[] @@ -70,12 +71,10 @@ model ProgrammerData { } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int @unique - music_data String? - music_url String - platform String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + music_url String + user User @relation(fields: [user_id], references: [id]) } model Status { @@ -85,11 +84,12 @@ model Status { } model Resume { - id Int @id @default(autoincrement()) - user_id Int - title String - introduce String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + title String + detail String @db.Text + portfolio_url String? + user User @relation(fields: [user_id], references: [id]) @@index([user_id], map: "Resume_user_id_fkey") } @@ -212,6 +212,7 @@ model ProjectPost { applicant_count Int view Int saved_count Int + created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) work_type WorkType @relation(fields: [work_type_id], references: [id]) From 99262919f73d08aea4752e2731df6a3f238114c0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 00:03:20 +0900 Subject: [PATCH 126/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=96=91=EC=8B=9D=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=98=B5=EC=85=94=EB=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 1482270..fa7fac2 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -227,11 +227,11 @@ export class ChatService { roleId: res.user.role_id, })), lastMessage: { - type: result.Message[0].type, - content: result.Message[0].content, - channelId: result.Message[0].channel_id, - date: result.Message[0].created_at, - userId: result.Message[0].user_id, + type: result.Message[0]?.type, + content: result.Message[0]?.content, + channelId: result.Message[0]?.channel_id, + date: result.Message[0]?.created_at, + userId: result.Message[0]?.user_id, }, }; From 4198cacf8f980559553adce6a62ce8cae5d27f67 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 01:20:26 +0900 Subject: [PATCH 127/414] =?UTF-8?q?[Feat]=20delete=20Project=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 1f2a918..1a299d3 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -235,6 +235,36 @@ export class UserService { }; } + async deleteProject(userId: number, projectId: number) { + // 1. 프로젝트 존재 여부 확인 + const existingProject = await this.prisma.myPageProject.findFirst({ + where: { + id: projectId, + user_id: userId, + }, + }); + + if (!existingProject) { + throw new NotFoundException('작업물을 찾을 수 없습니다.'); + } + + // 2. 트랜잭션을 사용하여 ProjectLinks와 myPageProject 삭제 + await this.prisma.$transaction([ + this.prisma.myPageProjectLink.deleteMany({ + where: { project_id: projectId }, + }), + this.prisma.myPageProject.delete({ + where: { id: projectId }, + }), + ]); + + // 3. 반환 데이터 구성 + return { + message: '작업물이 성공적으로 삭제되었습니다.', + projectId, + }; + } + async getUserSetting(userId: number) { // 사용자 정보를 가져옵니다. const user = await this.prisma.user.findUnique({ From 4941cc29f0838c7801e0a431e63ecccecc782f05 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 01:38:03 +0900 Subject: [PATCH 128/414] =?UTF-8?q?[Feat]=20=EC=95=84=ED=8B=B0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=97=85=EB=AC=BC=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 1a299d3..9c5f91c 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -810,4 +810,74 @@ export class UserService { totalPages: Math.ceil(totalCount / limit), }; } + + async addArtistWork(userId: number, musicUrl: string) { + const addMusicUrl = musicUrl; + + if (!addMusicUrl) { + throw new BadRequestException('음악 URL이 필요합니다.'); + } + + const newWork = await this.prisma.artistData.create({ + data: { + user_id: userId, + music_url: addMusicUrl, + }, + }); + + return { + id: newWork.id, + musicUrl: newWork.music_url, + }; + } + + async updateArtistWork(userId: number, workId: number, musicUrl: string) { + const newMusicUrl = musicUrl; + + if (!newMusicUrl) { + throw new BadRequestException('음악 URL이 필요합니다.'); + } + + const existingWork = await this.prisma.artistData.findFirst({ + where: { + id: workId, + user_id: userId, + }, + }); + + if (!existingWork) { + throw new NotFoundException('작업물을 찾을 수 없습니다.'); + } + + const updatedWork = await this.prisma.artistData.update({ + where: { id: workId }, + data: { + music_url: newMusicUrl, + }, + }); + + return { + id: updatedWork.id, + musicUrl: updatedWork.music_url, + }; + } + + async deleteArtistWork(userId: number, workId: number) { + const existingWork = await this.prisma.artistData.findFirst({ + where: { + id: workId, + user_id: userId, + }, + }); + + if (!existingWork) { + throw new NotFoundException('작업물을 찾을 수 없습니다.'); + } + + await this.prisma.artistData.delete({ + where: { id: workId }, + }); + + return { message: '작업물이 삭제되었습니다.' }; + } } From 956a509fac66bdc9a554fd383e63521fa5be90d3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 01:41:10 +0900 Subject: [PATCH 129/414] =?UTF-8?q?[Feat]=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= =?UTF-8?q?=EB=93=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 70 ++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index ee316da..40cab9d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -62,6 +62,46 @@ export class UserController { return this.userService.updateProject(userId, numProjectId, projectData); } + @Delete('projects/:projectId') + async deleteProject(@Req() req, @Param('projectId') projectId: string) { + const userId = req.user?.user_id; + const numProjectId = parseInt(projectId, 10); + return this.userService.deleteProject(userId, numProjectId); + } + + @Post('artist/works') + async addWork(@Req() req, @Body() musicUrl: string) { + const userId = req.user?.user_id; + return this.userService.addArtistWork(userId, musicUrl); + } + + @Put('artist/works/:workId') + async updateWork( + @Req() req, + @Param('workId') workId: string, + @Body() musicUrl: string + ) { + const userId = req.user?.user_id; + const numWorkId = parseInt(workId, 10); + return this.userService.updateArtistWork(userId, numWorkId, musicUrl); + } + + @Delete('artist/works/:workId') + async deleteWork(@Req() req, @Param('workId') workId: string) { + const userId = req.user?.user_id; + const numWorkId = parseInt(workId, 10); + return this.userService.deleteArtistWork(userId, numWorkId); + } + + @Patch('githubNickname') + async updateGithubUsername( + @Req() req, + @Body('githubUsername') githubUsername: string + ) { + const userId = req.user?.user_id; + return this.userService.updateGithubUsername(userId, githubUsername); + } + @Get('profile/settings') async getUserSetting(@Req() req) { const userId = req.user?.user_id; @@ -174,9 +214,10 @@ export class UserController { } @Get('profile/resume/:userId') - async getUserResume(@Req() req, @Param('userId') targetUserId: number) { + async getUserResume(@Req() req, @Param('userId') targetUserId: string) { const loggedInUserId = req.user?.user_id; - return this.userService.getUserResume(loggedInUserId, targetUserId); + const numUserId = parseInt(targetUserId, 10); + return this.userService.getUserResume(loggedInUserId, numUserId); } @Post('profile/resume') @@ -192,35 +233,44 @@ export class UserController { @Patch('profile/resume/:resumeId') async updateUserResume( @Req() req, - @Param('resumeId') resumeId: number, + @Param('resumeId') resumeId: string, @Body() body: { title?: string; portfolioUrl?: string; detail?: string } ) { const userId = req.user?.user_id; - return this.userService.updateUserResume(userId, resumeId, body); + const numResumeId = parseInt(resumeId, 10); + return this.userService.updateUserResume(userId, numResumeId, body); } @Delete(':resumeId') - async deleteUserResume(@Req() req, @Param('resumeId') resumeId: number) { + async deleteUserResume(@Req() req, @Param('resumeId') resumeId: string) { const userId = req.user?.user_id; - return this.userService.deleteUserResume(userId, resumeId); + const numResumeId = parseInt(resumeId, 10); + return this.userService.deleteUserResume(userId, numResumeId); } @Get(':userId/feeds') async getUserFeedPosts( - @Param('userId') userId: number, + @Param('userId') userId: string, @Query('page') page: number = 1, @Query('limit') limit: number = 10 ) { - return this.userService.getFeeds(userId, page, limit); + const numUserId = parseInt(userId, 10); + return this.userService.getFeeds(numUserId, page, limit); } @Get(':userId/connection-hub') async getUserConnectionHubProjects( - @Param('userId') userId: number, + @Param('userId') userId: string, @Query('type') type: 'applied' | 'created', @Query('page') page: number = 1, @Query('limit') limit: number = 10 ) { - return this.userService.getConnectionHubProjects(userId, type, page, limit); + const numUserId = parseInt(userId, 10); + return this.userService.getConnectionHubProjects( + numUserId, + type, + page, + limit + ); } } From 5e69c6387b9bbe50bff93f206520ba3b8c28e3a9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 01:41:27 +0900 Subject: [PATCH 130/414] =?UTF-8?q?[Feat]=20=EA=B0=9C=EB=B0=9C=EC=9E=90/?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B4=EB=84=88=20=EA=B9=83=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 9c5f91c..4a5e215 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -880,4 +880,30 @@ export class UserService { return { message: '작업물이 삭제되었습니다.' }; } + + async updateGithubUsername(userId: number, githubUsername: string) { + if (!githubUsername) { + throw new NotFoundException('깃허브 닉네임이 필요합니다.'); + } + + // 기존 ProgrammerData 확인 + const programmerData = await this.prisma.programmerData.findFirst({ + where: { user_id: userId }, + }); + + if (!programmerData) { + // 프로그래머 데이터가 없는 경우 새로 생성 + const newData = await this.prisma.programmerData.create({ + data: { + user_id: userId, + github_username: githubUsername, + }, + }); + + return { + message: '깃허브 닉네임이 성공적으로 추가되었습니다.', + githubUsername: newData.github_username, + }; + } + } } From d045fd673cd6c62b358a387d616f1a035e37bd62 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 02:10:52 +0900 Subject: [PATCH 131/414] =?UTF-8?q?[Add]=20Swagger=20Package=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bfd3e6..91eff12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.15", - "@nestjs/swagger": "^8.1.0", + "@nestjs/swagger": "^8.1.1", "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", @@ -2811,9 +2811,9 @@ "license": "MIT" }, "node_modules/@nestjs/swagger": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.0.tgz", - "integrity": "sha512-8hzH+r/31XshzXHC9vww4T0xjDAxMzvOaT1xAOvvY1LtXTWyNRCUP2iQsCYJOnnMrR+vydWjvRZiuB3hdvaHxA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.1.tgz", + "integrity": "sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "^0.15.0", diff --git a/package.json b/package.json index fc8fd6a..31cb79b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.15", - "@nestjs/swagger": "^8.1.0", + "@nestjs/swagger": "^8.1.1", "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", From a33e8454111d6622b3d117efe3e0c9e4e953d2e4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 02:11:06 +0900 Subject: [PATCH 132/414] =?UTF-8?q?[Feat]=20Swagger=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main.ts b/src/main.ts index 07324d4..49378e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; import * as cookieParser from 'cookie-parser'; import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; config(); async function bootstrap() { @@ -23,6 +24,21 @@ async function bootstrap() { }) ); + const config = new DocumentBuilder() + .setTitle('PAD API') + .setDescription('PAD를 위한 REST API 문서입니다.') + .setVersion('1.0') + .addBearerAuth() // JWT 인증 추가 + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api-docs', app, document, { + swaggerOptions: { + url: '/api-docs', // Swagger 문서 경로와 맞추기 위해 설정 + }, + customSiteTitle: 'API 문서', + }); + app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); } From 28283c0c4f7768320203a05635df7cabd17aabdc Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 13:38:57 +0900 Subject: [PATCH 133/414] =?UTF-8?q?[Refactor]=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/dto/response.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/dto/response.dto.ts b/src/common/dto/response.dto.ts index ed37552..45c783c 100644 --- a/src/common/dto/response.dto.ts +++ b/src/common/dto/response.dto.ts @@ -1,4 +1,4 @@ -export class ApiResponse { +export class MyApiResponse { message: { code: number; // HTTP 상태 코드 text: string; // 메시지 From 37fd3bfc3593e412ea575b5ade97c55bfc66aea4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 13:39:14 +0900 Subject: [PATCH 134/414] =?UTF-8?q?[Refactor]=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 01dd5ab..3ed4d54 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -217,8 +217,8 @@ export class AuthService { }); if (!user || user.password !== password) { throw new HttpException( - 'Invalid email or password', - HttpStatus.UNAUTHORIZED + '유효하지 않는 이메일 혹은 비밀번호 입니다', + HttpStatus.FORBIDDEN ); } From 12ec171d315f037b731378dcd252987f9447ee14 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 13:39:36 +0900 Subject: [PATCH 135/414] =?UTF-8?q?[Feat]=20=EC=9D=BC=EB=B0=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20Sw?= =?UTF-8?q?agger=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.controller.ts | 96 +++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e665012..e5f6cf5 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -9,14 +9,16 @@ import { HttpException, Res, HttpStatus, + HttpCode, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from '@src/modules/auth/auth.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { JwtService } from '@nestjs/jwt'; -import { ApiResponse } from '@common/dto/response.dto'; +import { MyApiResponse } from '@common/dto/response.dto'; import { HttpStatusCodes } from '@common/constants/http-status-code'; import { Response } from 'express'; +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; @Controller('auth') export class AuthController { constructor( @@ -130,10 +132,51 @@ export class AuthController { await this.authService.deleteRefreshToken(userId); return res .status(HttpStatusCodes.OK) - .json(new ApiResponse(HttpStatusCodes.OK, '로그아웃 성공')); + .json(new MyApiResponse(HttpStatusCodes.OK, '로그아웃 성공')); } @Post('signup') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: '회원가입', + description: '이메일, 닉네임, 비밀번호로 회원가입을 합니다.', + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: '회원가입 성공', + schema: { + example: { + message: '일반 회원가입 성공', + user: { + userId: 1, + email: 'user@example.com', + nickname: 'nickname123', + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '이메일이 이미 존재할 경우', + schema: { + example: { + statusCode: 400, + message: 'Email already exists', + }, + }, + }) + @ApiBody({ + description: '회원가입 요청 데이터', + schema: { + type: 'object', + properties: { + email: { type: 'string', example: 'user@example.com' }, + nickname: { type: 'string', example: 'nickname123' }, + password: { type: 'string', example: 'password123' }, + }, + required: ['email', 'nickname', 'password'], + }, + }) async signup( @Body() body: { email: string; nickname: string; password: string } ) { @@ -143,16 +186,61 @@ export class AuthController { body.password ); return { - message: 'Signup successful', + message: '일반 회원가입 성공', user: result, }; } @Post('login') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '로그인', + description: '이메일과 비밀번호로 로그인을 수행합니다.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: '로그인 성공', + schema: { + example: { + message: '일반 로그인 성공', + user: { + userId: 1, + email: 'user@example.com', + name: 'nickname123', + nickname: 'nickname123', + profileUrl: null, + authProvider: 'pad', + roleId: 1, + }, + accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }, + }, + }) + @ApiResponse({ + status: HttpStatus.FORBIDDEN, + description: '잘못된 이메일 또는 비밀번호', + schema: { + example: { + statusCode: 401, + message: '유효하지 않는 이메일 또는 비밀번호 입니다', + }, + }, + }) + @ApiBody({ + description: '로그인 요청 데이터', + schema: { + type: 'object', + properties: { + email: { type: 'string', example: 'user@example.com' }, + password: { type: 'string', example: 'password123' }, + }, + required: ['email', 'password'], + }, + }) async login(@Body() body: { email: string; password: string }) { const result = await this.authService.login(body.email, body.password); return { - message: 'Login successful', + message: '일반 로그인 성공', user: result.user, accessToken: result.accessToken, }; From df2238460a4c7cc0b1b715a806e602d0e89bfc24 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 17:39:20 +0900 Subject: [PATCH 136/414] =?UTF-8?q?[Refactor]=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=9D=91=EB=8B=B5=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20message=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index aac0d6d..79c5404 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -6,6 +6,7 @@ import * as cheerio from 'cheerio'; @Injectable() export class FeedService { constructor(private readonly prisma: PrismaService) {} + // 피드 전체 조회 async getAllFeeds(user, latest) { try { @@ -47,8 +48,10 @@ export class FeedService { const post = await this.getPostObj(res); posts.push(post); } - - return { posts }; + return { + posts, + message: { code: 200, message: '전체 피드를 정상적으로 조회했습니다.' }, + }; } catch (err) { console.log(err); throw new HttpException( @@ -99,7 +102,10 @@ export class FeedService { const post = await this.getPostObj(result); - return { post }; + return { + post, + message: { code: 200, message: '개별 피드를 정상적으로 조회했습니다.' }, + }; } catch (err) { console.log(err); if (err instanceof HttpException) { @@ -174,7 +180,13 @@ export class FeedService { createdAt: c.created_at, })); - return { comments }; + return { + comments, + message: { + code: 200, + message: '개별 피드(댓글)를 정상적으로 조회했습니다.', + }, + }; } catch (err) { console.log(err); if (err instanceof HttpException) { @@ -211,7 +223,7 @@ export class FeedService { data: { like_count: { decrement: 1 } }, }); - return { success: true, message: '좋아요가 취소되었습니다. ' }; + return { message: { code: 200, message: '좋아요가 취소되었습니다.' } }; } else { await this.prisma.feedLike.create({ data: { @@ -225,7 +237,7 @@ export class FeedService { data: { like_count: { increment: 1 } }, }); - return { success: true, message: '좋아요가 추가되었습니다. ' }; + return { message: { code: 200, message: '좋아요가 추가되었습니다.' } }; } } catch (err) { console.log(err); @@ -270,7 +282,10 @@ export class FeedService { data: tagData, }); - return { success: true, message: '게시글 작성이 완료되었습니다.' }; + return { + message: { code: 201, message: '피드 작성이 완료되었습니다.' }, + post: { id: feedData.id }, + }; } catch (err) { throw err; } @@ -321,7 +336,7 @@ export class FeedService { }, }); - return { success: true, message: '피드 수정이 완료되었습니다.' }; + return { message: { code: 200, message: '피드 수정이 완료되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; @@ -373,7 +388,7 @@ export class FeedService { }), ]); - return { success: true, message: '피드가 삭제되었습니다.' }; + return { message: { code: 200, message: '피드가 삭제되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; @@ -415,7 +430,7 @@ export class FeedService { data: { comment_count: { increment: 1 } }, }); - return { success: true, message: '댓글 등록이 완료되었습니다.' }; + return { message: { code: 201, message: '댓글 등록이 완료되었습니다.' } }; } catch (err) { console.log(err); throw new HttpException( @@ -461,8 +476,7 @@ export class FeedService { data: { comment_count: { decrement: 1 } }, }), ]); - - return { success: true, message: '댓글이 삭제되었습니다.' }; + return { message: { code: 200, message: '댓글이 삭제되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; From 66fb93e89257c0ad2e7d2db8f2a7e554c3411f41 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 19:35:14 +0900 Subject: [PATCH 137/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 48 +++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 4a5e215..176fdb8 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -40,14 +40,6 @@ export class UserService { throw new NotFoundException('사용자를 찾을 수 없습니다.'); } - // 로그인한 사용자가 해당 유저를 팔로우하고 있는지 확인 - const isFollowing = await this.prisma.follows.findFirst({ - where: { - following_user_id: loggedInUserId, - followed_user_id: targetUserId, - }, - }); - // 팔로워 수와 팔로잉 수 계산 const followerCount = await this.prisma.follows.count({ where: { followed_user_id: targetUserId }, @@ -84,20 +76,50 @@ export class UserService { // 반환 데이터 구성 return { - id: user.id, - nickname: user.nickname, + userId: user.id, profileUrl: user.profile_url, role: user.role.name, - introduce: user.introduce, status: user.status.name, applyCount: user.apply_count, postCount: user.post_count, followerCount, // 팔로워 수 followingCount, // 팔로잉 수 + specificData, // 직업군 맞춤 데이터 + isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 + }; + } + + async getUserProfileHeader(loggedInUserId: number, targetUserId: number) { + const user = await this.prisma.user.findUnique({ + where: { id: targetUserId }, + include: { + role: true, // 역할 정보 + UserLinks: true, // 연결된 링크 + }, + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // 로그인한 사용자가 해당 유저를 팔로우하고 있는지 확인 + const isFollowing = await this.prisma.follows.findFirst({ + where: { + following_user_id: loggedInUserId, + followed_user_id: targetUserId, + }, + }); + + // 반환 데이터 구성 + return { + userId: user.id, + nickname: user.nickname, + profileUrl: user.profile_url, + role: user.role.name, + introduce: user.introduce, userLinks: user.UserLinks.map(link => ({ url: link.link, - })), // 연결된 링크 - specificData, // 직업군 맞춤 데이터 + })), // 연결 isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 isFollowing: !!isFollowing, // 팔로우 여부 확인 }; From 29b2e1212481a07c6d5be1ef893744b406d1ee59 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 20:16:50 +0900 Subject: [PATCH 138/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20->=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95/=EC=82=AD=EC=A0=9C=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 79c5404..7df7cac 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -299,15 +299,9 @@ export class FeedService { const thumnailUrl = (await this.getThumnailUrl(content)) || null; // 권한 확인 - const originData = await this.prisma.feedPost.findUnique({ - where: { id: feedId }, - select: { - user_id: true, - Tags: { select: { tag: { select: { name: true } } } }, - }, - }); + const auth = await this.feedAuth(userId, feedId); - if (originData.user_id !== userId) { + if (!auth) { throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); } @@ -353,18 +347,9 @@ export class FeedService { async deleteFeed(userId, feedId) { try { // 권한 확인 - const auth = await this.prisma.feedPost.findUnique({ - where: { id: feedId }, - select: { user_id: true }, - }); + const auth = await this.feedAuth(userId, feedId); + if (!auth) { - throw new HttpException( - '게시글을 찾을 수 없습니다.', - HttpStatus.NOT_FOUND - ); - } - // 권한 예외 처리 - if (userId !== auth.user_id) { throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); } @@ -488,4 +473,14 @@ export class FeedService { ); } } + + // 게시글 권한 확인 + async feedAuth(userId: number, feedId: number) { + const auth = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + select: { user_id: true }, + }); + + return auth.user_id === userId; + } } From ae83af1c7669346d533f82322b404e495aceacff Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 20:21:31 +0900 Subject: [PATCH 139/414] =?UTF-8?q?[Refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC;=20=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 7df7cac..0f45816 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -429,24 +429,9 @@ export class FeedService { async deleteComment(userId, feedId, commentId) { try { // 권한 확인 - const auth = await this.prisma.feedComment.findUnique({ - where: { id: commentId }, - select: { user_id: true, post_id: true }, - }); + const auth = await this.commentAuth(userId, feedId, commentId); - // 권한 예외 처리 if (!auth) { - throw new HttpException( - '게시글을 찾을 수 없습니다.', - HttpStatus.NOT_FOUND - ); - } - - if (feedId !== auth.post_id) { - throw new HttpException('잘못된 요청입니다', HttpStatus.BAD_REQUEST); - } - - if (userId !== auth.user_id) { throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); } @@ -483,4 +468,18 @@ export class FeedService { return auth.user_id === userId; } + + // 댓글 권한 확인 + async commentAuth(userId: number, feedId: number, commentId: number) { + const auth = await this.prisma.feedComment.findUnique({ + where: { id: commentId }, + select: { user_id: true, post_id: true }, + }); + + if (feedId !== auth.post_id) { + throw new HttpException('잘못된 요청입니다', HttpStatus.BAD_REQUEST); + } + + return auth.user_id == userId; + } } From 34d1ddda54d80ed412e2053a40c4a5665faa073f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 20:27:54 +0900 Subject: [PATCH 140/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 18 ++++++++++++++++++ src/feed/feed.service.ts | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 353aa1c..57941b7 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -86,6 +86,24 @@ export class FeedController { return this.feedService.createComment(userId, feedId, comment.content); } + // 댓글 수정 + @Put(':id/comment/:commentId') + @UseGuards(JwtAuthGuard) + async updateComment( + @Req() req, + @Param('id') feedId: number, + @Param('commentId') commentId: number, + @Body() comment + ) { + const userId = req.user.user_id; + return await this.feedService.updateComment( + userId, + feedId, + commentId, + comment.content + ); + } + // 댓글 삭제 @Delete(':id/comment/:commentId') @UseGuards(JwtAuthGuard) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 0f45816..f251bea 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -459,6 +459,23 @@ export class FeedService { } } + // 댓글 수정 + async updateComment(userId, feedId, commentId, content) { + // 권한 확인 + const auth = await this.commentAuth(userId, feedId, commentId); + + if (!auth) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + + await this.prisma.feedComment.update({ + where: { id: commentId }, + data: { content }, + }); + + return { message: { code: 200, message: '댓글 수정이 완료되었습니다.' } }; + } + // 게시글 권한 확인 async feedAuth(userId: number, feedId: number) { const auth = await this.prisma.feedPost.findUnique({ From 780365521f7a914e89160b54e15d2b4ff1fccb5a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 20:37:35 +0900 Subject: [PATCH 141/414] =?UTF-8?q?[Feat]=20CommentDto=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84;=20=EB=8C=93=EA=B8=80=20=EB=93=B1=EB=A1=9D/=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/comment.dto.ts | 7 +++++++ src/feed/feed.controller.ts | 9 +++++---- src/feed/feed.service.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/feed/dto/comment.dto.ts diff --git a/src/feed/dto/comment.dto.ts b/src/feed/dto/comment.dto.ts new file mode 100644 index 0000000..5b5ca54 --- /dev/null +++ b/src/feed/dto/comment.dto.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CommentDto { + @IsString() + @IsNotEmpty() + content: string; +} diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 57941b7..7389cfd 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -14,6 +14,7 @@ import { FeedService } from './feed.service'; import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { FeedDto } from './dto/feed.dto'; +import { CommentDto } from './dto/comment.dto'; @Controller('feed') export class FeedController { @@ -80,10 +81,10 @@ export class FeedController { async createComment( @Req() req, @Param('id') feedId: number, - @Body() comment + @Body() commentDto: CommentDto ) { const userId = req.user.user_id; - return this.feedService.createComment(userId, feedId, comment.content); + return this.feedService.createComment(userId, feedId, commentDto); } // 댓글 수정 @@ -93,14 +94,14 @@ export class FeedController { @Req() req, @Param('id') feedId: number, @Param('commentId') commentId: number, - @Body() comment + @Body() commentDto: CommentDto ) { const userId = req.user.user_id; return await this.feedService.updateComment( userId, feedId, commentId, - comment.content + commentDto ); } diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index f251bea..0e4db2c 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -2,6 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; import { FeedDto } from './dto/feed.dto'; import * as cheerio from 'cheerio'; +import { CommentDto } from './dto/comment.dto'; @Injectable() export class FeedService { @@ -398,14 +399,15 @@ export class FeedService { } // 댓글 등록 - async createComment(userId, feedId, content) { + async createComment(userId, feedId, commentDto: CommentDto) { try { // 댓글 데이터 저장 + const content = commentDto.content; await this.prisma.feedComment.create({ data: { user_id: userId, post_id: feedId, - content: content, + content, }, }); @@ -460,7 +462,7 @@ export class FeedService { } // 댓글 수정 - async updateComment(userId, feedId, commentId, content) { + async updateComment(userId, feedId, commentId, commentDto: CommentDto) { // 권한 확인 const auth = await this.commentAuth(userId, feedId, commentId); @@ -468,6 +470,7 @@ export class FeedService { throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); } + const content = commentDto.content; await this.prisma.feedComment.update({ where: { id: commentId }, data: { content }, From 8ed251935ed2c0f4012fa6472fae0c503b911d32 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 20:39:27 +0900 Subject: [PATCH 142/414] =?UTF-8?q?[Feat]=20user=20=EA=B4=80=EB=A0=A8=20AP?= =?UTF-8?q?I=EB=93=A4=20=EC=88=98=EC=A0=95=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 52 ++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 176fdb8..f3d7c38 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -62,15 +62,15 @@ export class UserService { specificData = { githubUsername: user.ProgrammerData?.github_username || null, myPageProjects: user.MyPageProject - ? { - title: user.MyPageProject.title, - description: user.MyPageProject.description, - links: user.MyPageProject.ProjectLinks.map(link => ({ + ? user.MyPageProject.map(project => ({ + title: project.title, + description: project.description, + links: project.ProjectLinks.map(link => ({ type: link.type.name, url: link.url, })), - } - : null, + })) + : [], }; } @@ -117,9 +117,7 @@ export class UserService { profileUrl: user.profile_url, role: user.role.name, introduce: user.introduce, - userLinks: user.UserLinks.map(link => ({ - url: link.link, - })), // 연결 + userLinks: user.UserLinks.map(link => link.link), // 단순 URL 배열로 변경 isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 isFollowing: !!isFollowing, // 팔로우 여부 확인 }; @@ -198,7 +196,7 @@ export class UserService { }); return { - id: newProject.id, + myPageProjectId: newProject.id, title: newProject.title, description: newProject.description, links: newProject.ProjectLinks.map(link => ({ @@ -246,7 +244,7 @@ export class UserService { }); return { - id: updatedProject.id, + myPageProjectId: updatedProject.id, title: updatedProject.title, description: updatedProject.description, links: updatedProject.ProjectLinks.map(link => ({ @@ -509,8 +507,33 @@ export class UserService { } async addUserLinks(userId: number, links: { url: string }[]) { + // 넘어온 URL 목록 + const urls = links.map(link => link.url); + + // 이미 존재하는 URL 조회 + const existingLinks = await this.prisma.myPageUserLink.findMany({ + where: { + user_id: userId, + link: { in: urls }, + }, + select: { link: true }, + }); + + // 이미 존재하는 URL 필터링 + const existingUrls = existingLinks.map(link => link.link); + const newLinks = links.filter(link => !existingUrls.includes(link.url)); + + // 추가할 URL이 없으면 바로 반환 + if (newLinks.length === 0) { + return { + message: '추가할 링크가 없습니다.', + count: 0, + }; + } + + // 새 URL만 추가 const createdLinks = await this.prisma.myPageUserLink.createMany({ - data: links.map(link => ({ + data: newLinks.map(link => ({ user_id: userId, link: link.url, })), @@ -611,15 +634,12 @@ export class UserService { // 반환 데이터 구성 return { + userId: resume.user.id, title: resume.title, jobDetail: resume.user.job_detail, // 직무 상세 skills: resume.user.UserSkills.map(userSkill => userSkill.skill.name), // 기술 스택 이름 리스트 portfolioUrl: resume.portfolio_url, detail: resume.detail, - user: { - id: resume.user.id, - nickname: resume.user.nickname, - }, isOwnProfile, // 본인 프로필 여부 }; } From b9edcb3940edeec70b7e829bca865e0fa3ac8e14 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 20:39:42 +0900 Subject: [PATCH 143/414] [Feat] DB update --- prisma/schema.prisma | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 149095f..fbc4aa3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,7 +26,7 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? - ArtistData ArtistData? + ArtistData ArtistData[] Channel_users Channel_users[] FeedComments FeedComment[] FeedCommentLikes FeedCommentLikes[] @@ -37,7 +37,7 @@ model User { Message Message[] Message_status Message_status[] ProgrammerData ProgrammerData? - MyPageProject MyPageProject? + MyPageProject MyPageProject[] ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] @@ -60,13 +60,15 @@ model Role { model MyPageProject { id Int @id @default(autoincrement()) - user_id Int @unique + user_id Int title String description String created_at DateTime @default(now()) updated_at DateTime @updatedAt user User @relation(fields: [user_id], references: [id]) ProjectLinks MyPageProjectLink[] + + @@index([user_id], map: "MyPageProject_user_id_idx") // 인덱스 추가 } model MyPageProjectLink { @@ -96,7 +98,7 @@ model ProgrammerData { model ArtistData { id Int @id @default(autoincrement()) - user_id Int @unique + user_id Int music_url String user User @relation(fields: [user_id], references: [id]) } From abe2f61f9f9f37f6e49aee13d8cc164d89bfcb0e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 20:40:11 +0900 Subject: [PATCH 144/414] [Fix] user Controller Update --- src/modules/user/user.controller.ts | 178 +++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 40cab9d..d21f98c 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -16,36 +16,207 @@ import { HttpException, HttpStatus, Query, + HttpCode, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { FileInterceptor } from '@nestjs/platform-express'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, +} from '@nestjs/swagger'; +@ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Get(':userId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '유저 프로필 조회', + description: '특정 유저의 프로필을 조회합니다.', + }) + @ApiParam({ + name: 'userId', + description: '조회하려는 유저의 ID', + schema: { type: 'string', example: '1' }, + }) + @ApiResponse({ + status: HttpStatus.OK, + description: '유저 프로필 조회 성공', + schema: { + example: { + id: 1, + name: 'John Doe', + nickname: 'nickname123', + profileUrl: 'https://example.com/profile.jpg', + introduce: '안녕하세요!', + followers: 10, + followings: 5, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: '유저를 찾을 수 없는 경우', + schema: { + example: { + statusCode: 404, + message: 'User not found', + }, + }, + }) async getUserProfile(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID return this.userService.getUserProfile(loggedInUserId, numUserId); } + @Get(':userId/headers') + async getUserProfileHeader(@Param('userId') userId: string, @Req() req) { + const loggedInUserId = req.user?.user_id; + const numUserId = parseInt(userId); // 인증된 사용자 ID + return this.userService.getUserProfileHeader(loggedInUserId, numUserId); + } + @Get(':userId/followers') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '유저 팔로워 조회', + description: '특정 유저의 팔로워 목록을 조회합니다.', + }) + @ApiParam({ + name: 'userId', + description: '조회하려는 유저의 ID', + schema: { type: 'string', example: '1' }, + }) + @ApiResponse({ + status: HttpStatus.OK, + description: '유저 팔로워 조회 성공', + schema: { + example: [ + { id: 1, name: 'Follower1', nickname: 'nick1', profileUrl: null }, + { + id: 2, + name: 'Follower2', + nickname: 'nick2', + profileUrl: 'https://example.com/avatar.jpg', + }, + ], + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: '유저를 찾을 수 없는 경우', + schema: { + example: { + statusCode: 404, + message: 'User not found', + }, + }, + }) async getUserFollowers(@Param('userId') userId: string) { const numUserId = parseInt(userId); // 인증된 사용자 ID return this.userService.getUserFollowers(numUserId); } - @Get(':userId/followers') + @Get(':userId/following') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '유저 팔로잉 조회', + description: '특정 유저가 팔로우 중인 사용자 목록을 조회합니다.', + }) + @ApiParam({ + name: 'userId', + description: '조회하려는 유저의 ID', + schema: { type: 'string', example: '1' }, + }) + @ApiResponse({ + status: HttpStatus.OK, + description: '유저 팔로잉 조회 성공', + schema: { + example: [ + { id: 3, name: 'Following1', nickname: 'nick3', profileUrl: null }, + { + id: 4, + name: 'Following2', + nickname: 'nick4', + profileUrl: 'https://example.com/avatar.jpg', + }, + ], + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: '유저를 찾을 수 없는 경우', + schema: { + example: { + statusCode: 404, + message: 'User not found', + }, + }, + }) async getUserFollowings(@Param('userId') userId: string) { const numUserId = parseInt(userId); // 인증된 사용자 ID return this.userService.getUserFollowings(numUserId); } @Post('projects') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: '프로젝트 추가', + description: '로그인한 유저가 자신의 프로젝트를 추가합니다.', + }) + @ApiBody({ + description: '추가할 프로젝트 데이터', + schema: { + type: 'object', + properties: { + title: { type: 'string', example: '프로젝트 제목' }, + description: { type: 'string', example: '프로젝트 설명' }, + links: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', example: 'https://github.com/project' }, + typeId: { type: 'number', example: 1 }, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: '프로젝트 추가 성공', + schema: { + example: { + id: 1, + title: '프로젝트 제목', + description: '프로젝트 설명', + links: [ + { id: 1, url: 'https://github.com/project', type: 'GitHub' }, + { id: 2, url: 'https://project.com', type: 'Website' }, + ], + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '잘못된 입력 데이터', + schema: { + example: { + statusCode: 400, + message: 'Invalid project data', + }, + }, + }) async addProject(@Req() req, @Body() projectData: any) { const userId = req.user?.user_id; return this.userService.addProject(userId, projectData); @@ -185,9 +356,10 @@ export class UserController { } @Post('profile/links') - async addUserLinks(@Req() req, @Body('links') links: { url: string }[]) { + async addUserLinks(@Req() req, @Body('links') links: string[]) { const userId = req.user?.user_id; - return this.userService.addUserLinks(userId, links); + const formattedLinks = links.map(url => ({ url })); + return this.userService.addUserLinks(userId, formattedLinks); } @Delete('profile/links') From ce14510c62e2297fd61ace9c2454abd7bfa32b67 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 20:56:15 +0900 Subject: [PATCH 145/414] =?UTF-8?q?[Feat]=20GetFeedsQueryDto=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/getFeedsQuery.dto.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/feed/dto/getFeedsQuery.dto.ts diff --git a/src/feed/dto/getFeedsQuery.dto.ts b/src/feed/dto/getFeedsQuery.dto.ts new file mode 100644 index 0000000..7be8310 --- /dev/null +++ b/src/feed/dto/getFeedsQuery.dto.ts @@ -0,0 +1,22 @@ +import { IsOptional, IsInt, IsBoolean } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +export class GetFeedsQueryDto { + // 최신순 + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true') + latest?: boolean; + + // 리밋 + @IsOptional() + @IsInt() + @Type(() => Number) + limit?: number; + + // 커서 + @IsOptional() + @IsInt() + @Type(() => Number) + cursor?: number; +} From de70f64745470a9dee505ec5571e5ccbe4622eb9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 21:20:00 +0900 Subject: [PATCH 146/414] [Fix] user Controller Update --- src/modules/user/user.controller.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index d21f98c..bea2081 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -241,7 +241,7 @@ export class UserController { } @Post('artist/works') - async addWork(@Req() req, @Body() musicUrl: string) { + async addWork(@Req() req, @Body('musicUrl') musicUrl: string) { const userId = req.user?.user_id; return this.userService.addArtistWork(userId, musicUrl); } @@ -330,15 +330,7 @@ export class UserController { throw new BadRequestException('파일이 업로드되지 않았습니다'); } const fileType = file.mimetype.split('/')[1]; - const updateUser = await this.userService.patchProfileImage( - userId, - file.buffer, - fileType - ); - return { - message: '프로필 이미지가 성공적으로 업데이트되었습니다.', - user: updateUser, - }; + return this.userService.patchProfileImage(userId, file.buffer, fileType); } @Patch('profile/notification') From 3e0b2e1d029889556c1483d0e7feec29e03d02e7 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 21:20:17 +0900 Subject: [PATCH 147/414] =?UTF-8?q?[Fix]=20user=20Service=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index f3d7c38..9bad84e 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -53,7 +53,7 @@ export class UserService { let specificData = null; if (user.role.name === 'Artist') { specificData = { - musicUrl: user.ArtistData?.music_url, + works: user.ArtistData.map(works => works.music_url), // 단순 URL 배열로 변환 }; } else if ( user.role.name === 'Programmer' || @@ -406,11 +406,19 @@ export class UserService { fileType ); - return this.prisma.user.update({ + const user = await this.prisma.user.update({ where: { id: userId }, data: { profile_url: imageUrl }, select: { id: true, nickname: true, profile_url: true }, }); + return { + message: '프로필 이미지가 성공적으로 업데이트되었습니다.', + user: { + userId: user.id, + nickname: user.nickname, + profileUrl: user.profile_url, + }, + }; } async patchUserNotification( From 370f9c68cb6363ba688a82af83c2aa15efaca8a6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 21:45:55 +0900 Subject: [PATCH 148/414] =?UTF-8?q?[Fix]=20user=20Service=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EA=B0=92=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 144 ++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 20 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 9bad84e..8ad5ba3 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -76,6 +76,10 @@ export class UserService { // 반환 데이터 구성 return { + message: { + code: 200, + text: '유저 프로필 조회에 성공했습니다', + }, userId: user.id, profileUrl: user.profile_url, role: user.role.name, @@ -112,6 +116,10 @@ export class UserService { // 반환 데이터 구성 return { + message: { + code: 200, + text: '유저 프로필(헤더 부분) 조회에 성공했습니다', + }, userId: user.id, nickname: user.nickname, profileUrl: user.profile_url, @@ -196,6 +204,10 @@ export class UserService { }); return { + message: { + code: 200, + text: '마이페이지에 프로젝트 추가에 성공했습니다', + }, myPageProjectId: newProject.id, title: newProject.title, description: newProject.description, @@ -244,6 +256,10 @@ export class UserService { }); return { + message: { + code: 200, + text: '마이페이지에 프로젝트 수정에 성공했습니다', + }, myPageProjectId: updatedProject.id, title: updatedProject.title, description: updatedProject.description, @@ -280,7 +296,10 @@ export class UserService { // 3. 반환 데이터 구성 return { - message: '작업물이 성공적으로 삭제되었습니다.', + message: { + code: 200, + text: '마이페이지에 프로젝트 삭제에 성공했습니다', + }, projectId, }; } @@ -306,6 +325,10 @@ export class UserService { // 데이터 반환 return { + message: { + code: 200, + text: '유저 정보 세팅페이지 정보 조회에 성공했습니다', + }, nickname: user.nickname, profileUrl: user.profile_url, introduce: user.introduce, @@ -337,7 +360,10 @@ export class UserService { }); return { - message: '닉네임이 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '유저 닉네임이 성공적으로 변경되었습니다', + }, nickname: updatedUser.nickname, }; } @@ -355,7 +381,10 @@ export class UserService { }); return { - message: '직무 정보가 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '직무 정보가 성공적으로 업데이트되었습니다.', + }, jobDetail: user.job_detail, }; } @@ -368,7 +397,10 @@ export class UserService { }); return { - message: '사용자의 소개가 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '사용자의 소개가 성공적으로 업데이트되었습니다.', + }, introduce: updatedUser.introduce, }; } @@ -390,7 +422,10 @@ export class UserService { }); return { - message: '사용자의 상태가 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '사용자의 상태가 성공적으로 업데이트되었습니다.', + }, status: status.name, }; } @@ -412,7 +447,10 @@ export class UserService { select: { id: true, nickname: true, profile_url: true }, }); return { - message: '프로필 이미지가 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '프로필 이미지가 성공적으로 업데이트되었습니다.', + }, user: { userId: user.id, nickname: user.nickname, @@ -439,7 +477,10 @@ export class UserService { }); return { - message: '알림 설정이 성공적으로 업데이트되었습니다.', + message: { + code: 200, + text: '알림 설정이 성공적으로 업데이트되었습니다.', + }, notifications: { pushAlert: updatedUser.push_alert, followingAlert: updatedUser.following_alert, @@ -487,7 +528,10 @@ export class UserService { ); return { - message: '기술 스택이 성공적으로 추가되었습니다', + message: { + code: 200, + text: '기술 스택이 성공적으로 추가되었습니다', + }, skills: skills, }; } @@ -509,7 +553,10 @@ export class UserService { }); return { - message: '기술 스택이 성공적으로 삭제되었습니다', + message: { + code: 200, + text: '기술 스택이 성공적으로 삭제되었습니다', + }, skills, }; } @@ -534,7 +581,10 @@ export class UserService { // 추가할 URL이 없으면 바로 반환 if (newLinks.length === 0) { return { - message: '추가할 링크가 없습니다.', + message: { + code: 200, + text: '추가할 링크가 없습니다.', + }, count: 0, }; } @@ -548,7 +598,10 @@ export class UserService { }); return { - message: '링크가 성공적으로 추가되었습니다.', + message: { + code: 200, + text: '링크가 성공적으로 추가되었습니다.', + }, count: createdLinks.count, }; } @@ -562,7 +615,10 @@ export class UserService { }); return { - message: '링크가 성공적으로 삭제되었습니다.', + message: { + code: 200, + text: '링크가 성공적으로 삭제되었습니다.', + }, count: deletedLinks.count, }; } @@ -605,7 +661,10 @@ export class UserService { ]); return { - message: '사용자와 관련된 모든 데이터가 삭제되었습니다.', + message: { + code: 200, + text: '사용자와 관련된 모든 데이터가 삭제되었습니다.', + }, }; } @@ -642,6 +701,10 @@ export class UserService { // 반환 데이터 구성 return { + message: { + code: 200, + text: '사용자 이력서 조회에 성공했습니다.', + }, userId: resume.user.id, title: resume.title, jobDetail: resume.user.job_detail, // 직무 상세 @@ -666,7 +729,13 @@ export class UserService { }, }); - return newResume; + return { + message: { + code: 200, + text: '사용자 이력서 작성에 성공했습니다.', + }, + newResume, + }; } // 지원서 수정 @@ -698,7 +767,13 @@ export class UserService { }, }); - return updatedResume; + return { + message: { + code: 200, + text: '사용자 이력서 수정에 성공했습니다.', + }, + updatedResume, + }; } // 지원서 삭제 @@ -721,7 +796,12 @@ export class UserService { where: { id: resumeId }, }); - return { message: '지원서가 삭제되었습니다.' }; + return { + message: { + code: 200, + text: '사용자 이력서 삭제에 성공했습니다.', + }, + }; } async getFeeds(userId: number, page: number = 1, limit: number = 10) { @@ -754,6 +834,10 @@ export class UserService { // 반환 데이터 구성 return { + message: { + code: 200, + text: '사용자 피드 조회에 성공했습니다.', + }, feeds: feeds.map(feed => ({ id: feed.id, title: feed.title, @@ -854,6 +938,10 @@ export class UserService { }); return { + message: { + code: 200, + text: '사용자 커넥션허브 조회에 성공했습니다.', + }, projects: formattedProjects, totalCount, currentPage: page, @@ -876,7 +964,11 @@ export class UserService { }); return { - id: newWork.id, + message: { + code: 200, + text: '아티스트 작업물 추가에 성공했습니다.', + }, + musicId: newWork.id, musicUrl: newWork.music_url, }; } @@ -907,7 +999,11 @@ export class UserService { }); return { - id: updatedWork.id, + message: { + code: 200, + text: '아티스트 작업물 수정에 성공했습니다.', + }, + musicId: updatedWork.id, musicUrl: updatedWork.music_url, }; } @@ -928,7 +1024,12 @@ export class UserService { where: { id: workId }, }); - return { message: '작업물이 삭제되었습니다.' }; + return { + message: { + code: 200, + text: '아티스트 작업물 삭제에 성공했습니다.', + }, + }; } async updateGithubUsername(userId: number, githubUsername: string) { @@ -951,7 +1052,10 @@ export class UserService { }); return { - message: '깃허브 닉네임이 성공적으로 추가되었습니다.', + message: { + code: 200, + text: '깃허브 유저네임 등록에 성공했습니다.', + }, githubUsername: newData.github_username, }; } From 400f8e88a3466a4d821db5b82801d1731e97bd7e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 21:53:07 +0900 Subject: [PATCH 149/414] =?UTF-8?q?[Fix]=20user=20Service=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EA=B0=92=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 8ad5ba3..5fb7053 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -149,11 +149,17 @@ export class UserService { }); // 반환 데이터 생성 - return followers.map(follower => ({ - id: follower.following_user.id, - nickname: follower.following_user.nickname, - profileUrl: follower.following_user.profile_url, - })); + return { + message: { + code: 200, + text: '팔로잉 목록 조회에 성공했습니다', + }, + followerUsers: followers.map(follower => ({ + userId: follower.following_user.id, + nickname: follower.following_user.nickname, + profileUrl: follower.following_user.profile_url, + })), + }; } async getUserFollowings(targetUserId: number) { @@ -173,11 +179,17 @@ export class UserService { }); // 반환 데이터 생성 - return followings.map(following => ({ - id: following.followed_user.id, - nickname: following.followed_user.nickname, - profileUrl: following.followed_user.profile_url, - })); + return { + message: { + code: 200, + text: '팔로잉 목록 조회에 성공했습니다', + }, + followingUsers: followings.map(following => ({ + userId: following.followed_user.id, + nickname: following.followed_user.nickname, + profileUrl: following.followed_user.profile_url, + })), + }; } async addProject(userId: number, projectData: any) { From 496abc439b79eb980cc295f40a29c317e9b293c9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 22:10:31 +0900 Subject: [PATCH 150/414] =?UTF-8?q?[Feat]=20=ED=8C=94=EB=A1=9C=EC=9A=B0,?= =?UTF-8?q?=20=ED=8C=94=EB=A1=9C=EC=9B=8C=20=EC=A1=B0=ED=9A=8C=20API=20Swa?= =?UTF-8?q?gger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 110 ++-------------------------- 1 file changed, 5 insertions(+), 105 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index bea2081..df2c0b8 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -21,6 +21,7 @@ import { import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { FileInterceptor } from '@nestjs/platform-express'; +import { GetUserFollowersDocs, GetUserFollowingsDocs } from './docs/user.docs'; import { ApiBearerAuth, ApiBody, @@ -36,41 +37,6 @@ export class UserController { constructor(private readonly userService: UserService) {} @Get(':userId') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: '유저 프로필 조회', - description: '특정 유저의 프로필을 조회합니다.', - }) - @ApiParam({ - name: 'userId', - description: '조회하려는 유저의 ID', - schema: { type: 'string', example: '1' }, - }) - @ApiResponse({ - status: HttpStatus.OK, - description: '유저 프로필 조회 성공', - schema: { - example: { - id: 1, - name: 'John Doe', - nickname: 'nickname123', - profileUrl: 'https://example.com/profile.jpg', - introduce: '안녕하세요!', - followers: 10, - followings: 5, - }, - }, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: '유저를 찾을 수 없는 경우', - schema: { - example: { - statusCode: 404, - message: 'User not found', - }, - }, - }) async getUserProfile(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID @@ -85,82 +51,16 @@ export class UserController { } @Get(':userId/followers') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: '유저 팔로워 조회', - description: '특정 유저의 팔로워 목록을 조회합니다.', - }) - @ApiParam({ - name: 'userId', - description: '조회하려는 유저의 ID', - schema: { type: 'string', example: '1' }, - }) - @ApiResponse({ - status: HttpStatus.OK, - description: '유저 팔로워 조회 성공', - schema: { - example: [ - { id: 1, name: 'Follower1', nickname: 'nick1', profileUrl: null }, - { - id: 2, - name: 'Follower2', - nickname: 'nick2', - profileUrl: 'https://example.com/avatar.jpg', - }, - ], - }, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: '유저를 찾을 수 없는 경우', - schema: { - example: { - statusCode: 404, - message: 'User not found', - }, - }, - }) + @GetUserFollowersDocs.ApiOperation + @GetUserFollowersDocs.ApiResponse async getUserFollowers(@Param('userId') userId: string) { const numUserId = parseInt(userId); // 인증된 사용자 ID return this.userService.getUserFollowers(numUserId); } @Get(':userId/following') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: '유저 팔로잉 조회', - description: '특정 유저가 팔로우 중인 사용자 목록을 조회합니다.', - }) - @ApiParam({ - name: 'userId', - description: '조회하려는 유저의 ID', - schema: { type: 'string', example: '1' }, - }) - @ApiResponse({ - status: HttpStatus.OK, - description: '유저 팔로잉 조회 성공', - schema: { - example: [ - { id: 3, name: 'Following1', nickname: 'nick3', profileUrl: null }, - { - id: 4, - name: 'Following2', - nickname: 'nick4', - profileUrl: 'https://example.com/avatar.jpg', - }, - ], - }, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: '유저를 찾을 수 없는 경우', - schema: { - example: { - statusCode: 404, - message: 'User not found', - }, - }, - }) + @GetUserFollowingsDocs.ApiOperation + @GetUserFollowingsDocs.ApiResponse async getUserFollowings(@Param('userId') userId: string) { const numUserId = parseInt(userId); // 인증된 사용자 ID return this.userService.getUserFollowings(numUserId); From b16b8a23e325a2bebf456acc44315bf6538d343d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 22:22:34 +0900 Subject: [PATCH 151/414] [Feat] Prisma db pull --- prisma/schema.prisma | 62 +++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 149095f..b3fdb29 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,6 @@ model User { role_id Int introduce String? status_id Int - job_detail String? // "카테고리 / 직무명" 형식으로 저장 apply_count Int? @default(0) post_count Int? @default(0) push_alert Boolean @default(false) @@ -26,7 +25,8 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? - ArtistData ArtistData? + job_detail String? + ArtistData ArtistData[] Channel_users Channel_users[] FeedComments FeedComment[] FeedCommentLikes FeedCommentLikes[] @@ -36,18 +36,17 @@ model User { Follows Follows[] @relation("UserFollows") Message Message[] Message_status Message_status[] + MyPageProject MyPageProject[] + UserLinks MyPageUserLink[] ProgrammerData ProgrammerData? - MyPageProject MyPageProject? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] role Role @relation(fields: [role_id], references: [id]) status Status @relation(fields: [status_id], references: [id]) UserApplyProject UserApplyProject[] - UserLinks MyPageUserLink[] UserSkills UserSkill[] - @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") } @@ -59,46 +58,50 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) - user_id Int @unique + id Int @id @default(autoincrement()) + user_id Int title String description String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + user User @relation(fields: [user_id], references: [id]) ProjectLinks MyPageProjectLink[] + + @@index([user_id]) } model MyPageProjectLink { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) project_id Int type_id Int url String - project MyPageProject @relation(fields: [project_id], references: [id]) - type LinkType @relation(fields: [type_id], references: [id]) + project MyPageProject @relation(fields: [project_id], references: [id]) + type LinkType @relation(fields: [type_id], references: [id]) @@index([project_id], map: "ProjectLink_project_id_fkey") @@index([type_id], map: "ProjectLink_type_id_fkey") } model LinkType { - id Int @id @default(autoincrement()) - name String @unique + id Int @id @default(autoincrement()) + name String @unique Links MyPageProjectLink[] } model ProgrammerData { - id Int @id @default(autoincrement()) - user_id Int @unique - github_username String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + github_username String + user User @relation(fields: [user_id], references: [id]) } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int @unique - music_url String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + music_url String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "ArtistData_user_id_fkey") } model Status { @@ -108,12 +111,12 @@ model Status { } model Resume { - id Int @id @default(autoincrement()) - user_id Int - title String + id Int @id @default(autoincrement()) + user_id Int + title String + detail String @db.Text portfolio_url String? - detail String @db.Text - user User @relation(fields: [user_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([user_id], map: "Resume_user_id_fkey") } @@ -234,13 +237,14 @@ model ProjectPost { applicant_count Int view Int saved_count Int + created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) work_type WorkType @relation(fields: [work_type_id], references: [id]) Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] - created_at DateTime @default(now()) + @@index([user_id], map: "ProjectPost_user_id_fkey") @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") } From 5bbbbb18e8bcb660f1569a9bceea9ee270ceafba Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 22:28:49 +0900 Subject: [PATCH 152/414] =?UTF-8?q?[Fix]=20Response=20=EA=B0=92=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 5fb7053..a39d2b1 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -81,7 +81,6 @@ export class UserService { text: '유저 프로필 조회에 성공했습니다', }, userId: user.id, - profileUrl: user.profile_url, role: user.role.name, status: user.status.name, applyCount: user.apply_count, From 362757169669c1fe3b4e880522aa7f3baecb5ea7 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 18 Jan 2025 22:29:17 +0900 Subject: [PATCH 153/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20API=EB=93=A4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 58 ++++------------------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index df2c0b8..34ae51a 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -21,7 +21,7 @@ import { import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { FileInterceptor } from '@nestjs/platform-express'; -import { GetUserFollowersDocs, GetUserFollowingsDocs } from './docs/user.docs'; +import { GetUserFollowersDocs, GetUserFollowingsDocs, GetUserProfileDocs, GetUserProfileHeaderDocs } from './docs/user.docs'; import { ApiBearerAuth, ApiBody, @@ -37,6 +37,9 @@ export class UserController { constructor(private readonly userService: UserService) {} @Get(':userId') + @GetUserProfileDocs.ApiOperation + @GetUserProfileDocs.ApiParam + @GetUserProfileDocs.ApiResponse async getUserProfile(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID @@ -44,6 +47,9 @@ export class UserController { } @Get(':userId/headers') + @GetUserProfileHeaderDocs.ApiOperation + @GetUserProfileHeaderDocs.ApiParam + @GetUserProfileHeaderDocs.ApiResponse async getUserProfileHeader(@Param('userId') userId: string, @Req() req) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(userId); // 인증된 사용자 ID @@ -67,56 +73,6 @@ export class UserController { } @Post('projects') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ - summary: '프로젝트 추가', - description: '로그인한 유저가 자신의 프로젝트를 추가합니다.', - }) - @ApiBody({ - description: '추가할 프로젝트 데이터', - schema: { - type: 'object', - properties: { - title: { type: 'string', example: '프로젝트 제목' }, - description: { type: 'string', example: '프로젝트 설명' }, - links: { - type: 'array', - items: { - type: 'object', - properties: { - url: { type: 'string', example: 'https://github.com/project' }, - typeId: { type: 'number', example: 1 }, - }, - }, - }, - }, - }, - }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: '프로젝트 추가 성공', - schema: { - example: { - id: 1, - title: '프로젝트 제목', - description: '프로젝트 설명', - links: [ - { id: 1, url: 'https://github.com/project', type: 'GitHub' }, - { id: 2, url: 'https://project.com', type: 'Website' }, - ], - }, - }, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: '잘못된 입력 데이터', - schema: { - example: { - statusCode: 400, - message: 'Invalid project data', - }, - }, - }) async addProject(@Req() req, @Body() projectData: any) { const userId = req.user?.user_id; return this.userService.addProject(userId, projectData); From 6e8e79d9383f0e7e714037226ec4c7eb37016a4a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 22:41:33 +0900 Subject: [PATCH 154/414] =?UTF-8?q?[Feat]=20=EC=A0=84=EC=B2=B4=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 5 +++-- src/feed/feed.service.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 7389cfd..6fd4684 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -15,6 +15,7 @@ import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard' import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { FeedDto } from './dto/feed.dto'; import { CommentDto } from './dto/comment.dto'; +import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; @Controller('feed') export class FeedController { @@ -22,8 +23,8 @@ export class FeedController { // 메인 페이지 조회 @Get() @UseGuards(OptionalAuthGuard) - async getAllFeed(@Req() req, @Query('latest') latest: boolean) { - return this.feedService.getAllFeeds(req.user, latest); + async getAllFeed(@Req() req, @Query() queryDto: GetFeedsQueryDto) { + return this.feedService.getAllFeeds(req.user, queryDto); } // 피드 조회 (게시글) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 0e4db2c..9f81acb 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -3,19 +3,22 @@ import { PrismaService } from '@src/prisma/prisma.service'; import { FeedDto } from './dto/feed.dto'; import * as cheerio from 'cheerio'; import { CommentDto } from './dto/comment.dto'; +import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; @Injectable() export class FeedService { constructor(private readonly prisma: PrismaService) {} // 피드 전체 조회 - async getAllFeeds(user, latest) { + async getAllFeeds(user, queryDto: GetFeedsQueryDto) { try { const userId = user ? user.user_id : 0; + const { latest = false, limit = 10, cursor = 0 } = queryDto; const orderKey = latest ? 'created_at' : 'like_count'; const result = await this.prisma.feedPost.findMany({ + where: cursor ? { id: { gt: cursor } } : {}, include: { Likes: { where: { user_id: userId }, @@ -40,6 +43,7 @@ export class FeedService { }, }, }, + take: limit, // 인기순 정렬 : 좋아요 순 orderBy: { [orderKey]: 'desc' }, }); @@ -49,8 +53,13 @@ export class FeedService { const post = await this.getPostObj(res); posts.push(post); } + + // 라스트 커서 + const lastCursor = posts[posts.length - 1]?.postId || null; + return { posts, + pagination: { lastCursor }, message: { code: 200, message: '전체 피드를 정상적으로 조회했습니다.' }, }; } catch (err) { From be43af20af2344d516e45f6357ec4a0b35dba8bb Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 18 Jan 2025 22:48:44 +0900 Subject: [PATCH 155/414] =?UTF-8?q?[Refactor]=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=EC=97=90=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 9f81acb..fba905e 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -72,7 +72,7 @@ export class FeedService { } // 피드 조회 (게시글 부분) - async getFeed(feedId, user) { + async getFeed(feedId: number, user) { try { const userId = user ? user.user_id : 0; const result = await this.prisma.feedPost.findUnique({ @@ -152,7 +152,7 @@ export class FeedService { } // 피드 개별 조회 (댓글) - async getFeedComments(feedId) { + async getFeedComments(feedId: number) { try { const result = await this.prisma.feedComment.findMany({ where: { @@ -211,7 +211,7 @@ export class FeedService { } // 피드 좋아요 추가/제거 - async handleFeedLikes(feedId, userId) { + async handleFeedLikes(feedId: number, userId: number) { try { const exist = await this.prisma.feedLike.findMany({ where: { @@ -354,7 +354,7 @@ export class FeedService { } // 피드 삭제 - async deleteFeed(userId, feedId) { + async deleteFeed(userId: number, feedId: number) { try { // 권한 확인 const auth = await this.feedAuth(userId, feedId); @@ -397,7 +397,7 @@ export class FeedService { } // 썸네일 추출 - async getThumnailUrl(text) { + async getThumnailUrl(text: string) { try { const $ = cheerio.load(text); const thumnailUrl = $('img').first().attr('src'); @@ -408,7 +408,7 @@ export class FeedService { } // 댓글 등록 - async createComment(userId, feedId, commentDto: CommentDto) { + async createComment(userId: number, feedId: number, commentDto: CommentDto) { try { // 댓글 데이터 저장 const content = commentDto.content; @@ -437,7 +437,7 @@ export class FeedService { } // 댓글 삭제 - async deleteComment(userId, feedId, commentId) { + async deleteComment(userId: number, feedId: number, commentId: number) { try { // 권한 확인 const auth = await this.commentAuth(userId, feedId, commentId); @@ -471,7 +471,12 @@ export class FeedService { } // 댓글 수정 - async updateComment(userId, feedId, commentId, commentDto: CommentDto) { + async updateComment( + userId: number, + feedId: number, + commentId: number, + commentDto: CommentDto + ) { // 권한 확인 const auth = await this.commentAuth(userId, feedId, commentId); From 6125bf7a192599e8fe06179b194ea9d237085468 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 00:49:05 +0900 Subject: [PATCH 156/414] =?UTF-8?q?[Feat]=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=84=A4=EC=9E=84=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/s3/s3.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/s3/s3.service.ts b/src/s3/s3.service.ts index 927bddc..9488740 100644 --- a/src/s3/s3.service.ts +++ b/src/s3/s3.service.ts @@ -15,10 +15,12 @@ export class S3Service { async uploadImage( userId: number, fileBuffer: Buffer, - fileType: string + fileType: string, + prefix?: string ): Promise { try { - const fileName = `pad_users/profile_${crypto.randomUUID()}.${fileType}`; + const filePrefix = prefix || 'pad_users/profile'; + const fileName = `${filePrefix}_${crypto.randomUUID()}.${fileType}`; const uploadResult = await this.s3 .upload({ Bucket: this.bucketName, From 9d2be296a79ae905005d3a744c5df67d82c15399 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 00:51:06 +0900 Subject: [PATCH 157/414] =?UTF-8?q?[Fix]=20=EC=9D=91=EB=8B=B5=20status=20c?= =?UTF-8?q?ode=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index a39d2b1..d6cac83 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -216,7 +216,7 @@ export class UserService { return { message: { - code: 200, + code: 201, text: '마이페이지에 프로젝트 추가에 성공했습니다', }, myPageProjectId: newProject.id, From 7f27a0e0d1739633b8d3984525ad2feeac0085d7 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 00:51:41 +0900 Subject: [PATCH 158/414] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 34ae51a..0e423f0 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -21,14 +21,8 @@ import { import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { FileInterceptor } from '@nestjs/platform-express'; -import { GetUserFollowersDocs, GetUserFollowingsDocs, GetUserProfileDocs, GetUserProfileHeaderDocs } from './docs/user.docs'; -import { - ApiBearerAuth, - ApiBody, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; +import { GetUserFollowersDocs, GetUserFollowingsDocs, GetUserProfileDocs, GetUserProfileHeaderDocs,AddProjectDocs, UpdateProjectDocs, DeleteProjectDocs } from './docs/user.docs'; +import {ApiBearerAuth} from '@nestjs/swagger'; @ApiBearerAuth() @UseGuards(JwtAuthGuard) @@ -73,12 +67,19 @@ export class UserController { } @Post('projects') + @AddProjectDocs.ApiOperation + @AddProjectDocs.ApiBody + @AddProjectDocs.ApiResponse async addProject(@Req() req, @Body() projectData: any) { const userId = req.user?.user_id; return this.userService.addProject(userId, projectData); } @Put('projects/:projectId') + @UpdateProjectDocs.ApiOperation + @UpdateProjectDocs.ApiParam + @UpdateProjectDocs.ApiBody + @UpdateProjectDocs.ApiResponse async updateProject( @Req() req, @Param('projectId') projectId: string, @@ -90,6 +91,9 @@ export class UserController { } @Delete('projects/:projectId') + @DeleteProjectDocs.ApiOperation + @DeleteProjectDocs.ApiParam + @DeleteProjectDocs.ApiResponse async deleteProject(@Req() req, @Param('projectId') projectId: string) { const userId = req.user?.user_id; const numProjectId = parseInt(projectId, 10); From 71afc8373b700d95a093d8b9d54edd0dc19e21a5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 00:57:28 +0900 Subject: [PATCH 159/414] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=91=EC=97=85=EB=AC=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 0e423f0..ace6640 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -21,7 +21,18 @@ import { import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { FileInterceptor } from '@nestjs/platform-express'; -import { GetUserFollowersDocs, GetUserFollowingsDocs, GetUserProfileDocs, GetUserProfileHeaderDocs,AddProjectDocs, UpdateProjectDocs, DeleteProjectDocs } from './docs/user.docs'; +import { + GetUserFollowersDocs, + GetUserFollowingsDocs, + GetUserProfileDocs, + GetUserProfileHeaderDocs, + AddProjectDocs, + UpdateProjectDocs, + DeleteProjectDocs, + AddWorkDocs, + UpdateWorkDocs, + DeleteWorkDocs, +} from './docs/user.docs'; import {ApiBearerAuth} from '@nestjs/swagger'; @ApiBearerAuth() @@ -101,12 +112,19 @@ export class UserController { } @Post('artist/works') + @AddWorkDocs.ApiOperation + @AddWorkDocs.ApiBody + @AddWorkDocs.ApiResponse async addWork(@Req() req, @Body('musicUrl') musicUrl: string) { const userId = req.user?.user_id; return this.userService.addArtistWork(userId, musicUrl); } @Put('artist/works/:workId') + @UpdateWorkDocs.ApiOperation + @UpdateWorkDocs.ApiParam + @UpdateWorkDocs.ApiBody + @UpdateWorkDocs.ApiResponse async updateWork( @Req() req, @Param('workId') workId: string, @@ -118,6 +136,9 @@ export class UserController { } @Delete('artist/works/:workId') + @DeleteWorkDocs.ApiOperation + @DeleteWorkDocs.ApiParam + @DeleteWorkDocs.ApiResponse async deleteWork(@Req() req, @Param('workId') workId: string) { const userId = req.user?.user_id; const numWorkId = parseInt(workId, 10); From d5c4ae913dea731075216bf7c1f81388f61fa2ed Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 01:11:46 +0900 Subject: [PATCH 160/414] =?UTF-8?q?[Feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 12 ++++++++++++ src/feed/feed.module.ts | 3 ++- src/feed/feed.service.ts | 22 +++++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 6fd4684..01d8af4 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -8,7 +8,9 @@ import { Put, Query, Req, + UploadedFile, UseGuards, + UseInterceptors, } from '@nestjs/common'; import { FeedService } from './feed.service'; import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; @@ -16,6 +18,7 @@ import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { FeedDto } from './dto/feed.dto'; import { CommentDto } from './dto/comment.dto'; import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; @Controller('feed') export class FeedController { @@ -117,4 +120,13 @@ export class FeedController { const userId = req.user.user_id; return this.feedService.deleteComment(userId, feedId, commentId); } + + // 이미지 업로드 + @Post('image') + @UseInterceptors(FileInterceptor('file')) + @UseGuards(JwtAuthGuard) + async func(@Req() req, @UploadedFile() file: Express.Multer.File) { + const userId = req.user.user_id; + return await this.feedService.uploadFeedImage(userId, file); + } } diff --git a/src/feed/feed.module.ts b/src/feed/feed.module.ts index a63c411..0b3be44 100644 --- a/src/feed/feed.module.ts +++ b/src/feed/feed.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { FeedService } from './feed.service'; import { FeedController } from './feed.controller'; import { PrismaModule } from '@src/prisma/prisma.module'; +import { S3Module } from '@src/s3/s3.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, S3Module], controllers: [FeedController], providers: [FeedService], }) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index fba905e..edece03 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -4,10 +4,14 @@ import { FeedDto } from './dto/feed.dto'; import * as cheerio from 'cheerio'; import { CommentDto } from './dto/comment.dto'; import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; +import { S3Service } from '@src/s3/s3.service'; @Injectable() export class FeedService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly s3: S3Service + ) {} // 피드 전체 조회 async getAllFeeds(user, queryDto: GetFeedsQueryDto) { @@ -516,4 +520,20 @@ export class FeedService { return auth.user_id == userId; } + + // 이미지 업로드 + async uploadFeedImage(userId: number, file: Express.Multer.File) { + const fileType = file.mimetype.split('/')[1]; + const imageUrl = await this.s3.uploadImage( + 8, + file.buffer, + fileType, + 'pad_feed' + ); + + return { + imageUrl, + message: { code: 200, message: '이미지 업로드가 완료되었습니다.' }, + }; + } } From 5bda5f576c6d9c7531efe6ba279202ca96a612f5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:23:04 +0900 Subject: [PATCH 161/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=8D=B0=EC=BD=94?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index ace6640..7ec49de 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -32,6 +32,13 @@ import { AddWorkDocs, UpdateWorkDocs, DeleteWorkDocs, + PatchUserNicknameDocs, + GetUserSettingDocs, + UpdateGithubUsernameDocs, + UpdateUserJobDetailDocs, + PatchUserStatusDocs, + PatchUserIntroduceDocs, + PatchUserSkillsDocs } from './docs/user.docs'; import {ApiBearerAuth} from '@nestjs/swagger'; @@ -146,6 +153,9 @@ export class UserController { } @Patch('githubNickname') + @UpdateGithubUsernameDocs.ApiOperation + @UpdateGithubUsernameDocs.ApiBody + @UpdateGithubUsernameDocs.ApiResponse async updateGithubUsername( @Req() req, @Body('githubUsername') githubUsername: string @@ -155,30 +165,44 @@ export class UserController { } @Get('profile/settings') + @GetUserSettingDocs.ApiOperation + @GetUserSettingDocs.ApiResponse async getUserSetting(@Req() req) { const userId = req.user?.user_id; return this.userService.getUserSetting(userId); } @Patch('profile/nickname') + @PatchUserNicknameDocs.ApiOperation + @PatchUserNicknameDocs.ApiBody + @PatchUserNicknameDocs.ApiResponse async patchUserNickname(@Req() req, @Body('nickname') nickname: string) { const userId = req.user?.user_id; return this.userService.patchUserNickname(userId, nickname); } @Patch('profile/introduce') + @PatchUserIntroduceDocs.ApiOperation + @PatchUserIntroduceDocs.ApiBody + @PatchUserIntroduceDocs.ApiResponse async patchUserIntroduce(@Req() req, @Body('introduce') introduce: string) { const userId = req.user?.user_id; return this.userService.patchUserIntroduce(userId, introduce); } @Patch('profile/status') + @PatchUserStatusDocs.ApiOperation + @PatchUserStatusDocs.ApiBody + @PatchUserStatusDocs.ApiResponse async patchUserStatus(@Req() req, @Body('statusId') statusId: number) { const userId = req.user?.user_id; return this.userService.patchUserStatus(userId, statusId); } @Patch('profile/job') + @UpdateUserJobDetailDocs.ApiOperation + @UpdateUserJobDetailDocs.ApiBody + @UpdateUserJobDetailDocs.ApiResponse async updateUserJobDetail( @Req() req, @Body('category') category: string, @@ -189,6 +213,9 @@ export class UserController { } @Post('profile/skills') + @PatchUserSkillsDocs.ApiOperation + @PatchUserSkillsDocs.ApiBody + @PatchUserSkillsDocs.ApiResponse async patchUserSkills(@Req() req, @Body('skills') skills: string[]) { const userId = req.user?.user_id; return this.userService.addUserSkills(userId, skills); @@ -219,9 +246,9 @@ export class UserController { @Req() req, @Body('notification') notifications: { - pushAlert: boolean; - followingAlert: boolean; - projectAlert: boolean; + pushAlert?: boolean; + followingAlert?: boolean; + projectAlert?: boolean; } ) { const userId = req.user?.user_id; From c4ca56ab80dfb5731eb71d68f2c789635a9a6c89 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:23:32 +0900 Subject: [PATCH 162/414] =?UTF-8?q?[Fix]=20patchUserNotification=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index d6cac83..f40b553 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -349,6 +349,7 @@ export class UserService { url: link.link, // 링크 정보만 반환 })), skills: user.UserSkills.map(skill => skill.skill.name), // 기술 스택 + jobDeatil: user.job_detail, notifications: { pushAlert: user.push_alert, followingAlert: user.following_alert, @@ -473,9 +474,9 @@ export class UserService { async patchUserNotification( userId: number, notifications: { - pushAlert: boolean; - followingAlert: boolean; - projectAlert: boolean; + pushAlert?: boolean; + followingAlert?: boolean; + projectAlert?: boolean; } ) { const updatedUser = await this.prisma.user.update({ @@ -976,7 +977,7 @@ export class UserService { return { message: { - code: 200, + code: 201, text: '아티스트 작업물 추가에 성공했습니다.', }, musicId: newWork.id, From 016d0229915cd2f27cd904b0d3bec3d55705b8f8 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:29:24 +0900 Subject: [PATCH 163/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=8D=B0=EC=BD=94?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 7ec49de..d0facfe 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -38,7 +38,10 @@ import { UpdateUserJobDetailDocs, PatchUserStatusDocs, PatchUserIntroduceDocs, - PatchUserSkillsDocs + PatchUserSkillsDocs, + DeleteUserSkillsDocs, + PatchProfileImageDocs, + PatchUserNotificationDocs } from './docs/user.docs'; import {ApiBearerAuth} from '@nestjs/swagger'; @@ -222,12 +225,19 @@ export class UserController { } @Delete('profile/skills') + @DeleteUserSkillsDocs.ApiOperation + @DeleteUserSkillsDocs.ApiBody + @DeleteUserSkillsDocs.ApiResponse async deleteUserSkills(@Req() req, @Body('skills') skills: string[]) { const userId = req.user?.user_id; return this.userService.deleteUserSkills(userId, skills); } @Patch('profile/image') + @PatchProfileImageDocs.ApiOperation + @PatchProfileImageDocs.ApiConsumes + @PatchProfileImageDocs.ApiBody + @PatchProfileImageDocs.ApiResponse @UseInterceptors(FileInterceptor('file')) async patchProfileImage( @Req() req, @@ -242,6 +252,9 @@ export class UserController { } @Patch('profile/notification') + @PatchUserNotificationDocs.ApiOperation + @PatchUserNotificationDocs.ApiBody + @PatchUserNotificationDocs.ApiResponse async patchUserNotification( @Req() req, @Body('notification') @@ -313,7 +326,7 @@ export class UserController { return this.userService.updateUserResume(userId, numResumeId, body); } - @Delete(':resumeId') + @Delete('profile/resume/:resumeId') async deleteUserResume(@Req() req, @Param('resumeId') resumeId: string) { const userId = req.user?.user_id; const numResumeId = parseInt(resumeId, 10); From 574f48c8ce5af8dba24a2cbc2ea2008674470c49 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:49:02 +0900 Subject: [PATCH 164/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=8D=B0=EC=BD=94?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index d0facfe..0c54d3b 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -41,7 +41,13 @@ import { PatchUserSkillsDocs, DeleteUserSkillsDocs, PatchProfileImageDocs, - PatchUserNotificationDocs + PatchUserNotificationDocs, + AddUserLinksDocs, + DeleteUserLinksDocs, + GetUserResumeDocs, + CreateUserResumeDocs, + UpdateUserResumeDocs, + DeleteUserResumeDocs } from './docs/user.docs'; import {ApiBearerAuth} from '@nestjs/swagger'; @@ -269,6 +275,9 @@ export class UserController { } @Post('profile/links') + @AddUserLinksDocs.ApiOperation + @AddUserLinksDocs.ApiBody + @AddUserLinksDocs.ApiResponse async addUserLinks(@Req() req, @Body('links') links: string[]) { const userId = req.user?.user_id; const formattedLinks = links.map(url => ({ url })); @@ -276,6 +285,9 @@ export class UserController { } @Delete('profile/links') + @DeleteUserLinksDocs.ApiOperation + @DeleteUserLinksDocs.ApiBody + @DeleteUserLinksDocs.ApiResponse async deleteUserLinks(@Req() req, @Body('linkIds') linkIds: number[]) { const userId = req.user?.user_id; return this.userService.deleteUserLinks(userId, linkIds); @@ -299,6 +311,9 @@ export class UserController { } @Get('profile/resume/:userId') + @GetUserResumeDocs.ApiOperation + @GetUserResumeDocs.ApiParam + @GetUserResumeDocs.ApiResponse async getUserResume(@Req() req, @Param('userId') targetUserId: string) { const loggedInUserId = req.user?.user_id; const numUserId = parseInt(targetUserId, 10); @@ -306,6 +321,9 @@ export class UserController { } @Post('profile/resume') + @CreateUserResumeDocs.ApiOperation + @CreateUserResumeDocs.ApiBody + @CreateUserResumeDocs.ApiResponse async createUserResume( @Req() req, @Body() body: { title: string; portfolioUrl?: string; detail: string } @@ -316,6 +334,10 @@ export class UserController { // 지원서 수정 @Patch('profile/resume/:resumeId') + @UpdateUserResumeDocs.ApiOperation + @UpdateUserResumeDocs.ApiParam + @UpdateUserResumeDocs.ApiBody + @UpdateUserResumeDocs.ApiResponse async updateUserResume( @Req() req, @Param('resumeId') resumeId: string, @@ -327,6 +349,9 @@ export class UserController { } @Delete('profile/resume/:resumeId') + @DeleteUserResumeDocs.ApiOperation + @DeleteUserResumeDocs.ApiParam + @DeleteUserResumeDocs.ApiResponse async deleteUserResume(@Req() req, @Param('resumeId') resumeId: string) { const userId = req.user?.user_id; const numResumeId = parseInt(resumeId, 10); From 3b729e0ad0f80b01eff2ddb2490326683bd859d9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:49:41 +0900 Subject: [PATCH 165/414] =?UTF-8?q?[Fix]=20Response=20=EA=B0=92=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 118 ++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index f40b553..dc9ad47 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -573,51 +573,69 @@ export class UserService { }; } - async addUserLinks(userId: number, links: { url: string }[]) { - // 넘어온 URL 목록 - const urls = links.map(link => link.url); - - // 이미 존재하는 URL 조회 - const existingLinks = await this.prisma.myPageUserLink.findMany({ - where: { - user_id: userId, - link: { in: urls }, - }, - select: { link: true }, - }); - - // 이미 존재하는 URL 필터링 - const existingUrls = existingLinks.map(link => link.link); - const newLinks = links.filter(link => !existingUrls.includes(link.url)); - - // 추가할 URL이 없으면 바로 반환 - if (newLinks.length === 0) { - return { - message: { - code: 200, - text: '추가할 링크가 없습니다.', - }, - count: 0, - }; - } - - // 새 URL만 추가 - const createdLinks = await this.prisma.myPageUserLink.createMany({ - data: newLinks.map(link => ({ - user_id: userId, - link: link.url, - })), + async addUserLinks(userId: number, links: { url: string }[]) { + // 넘어온 URL 목록 + const urls = links.map(link => link.url); + + // 이미 존재하는 URL 조회 + const existingLinks = await this.prisma.myPageUserLink.findMany({ + where: { + user_id: userId, + link: { in: urls }, + }, + select: { link: true }, + }); + + // 이미 존재하는 URL 필터링 + const existingUrls = existingLinks.map(link => link.link); + const newLinks = links.filter(link => !existingUrls.includes(link.url)); + + // 추가할 URL이 없으면 바로 반환 + if (newLinks.length === 0) { + // 유저의 현재 링크 조회 + const currentLinks = await this.prisma.myPageUserLink.findMany({ + where: { user_id: userId }, + select: { id: true, link: true }, }); return { message: { code: 200, - text: '링크가 성공적으로 추가되었습니다.', + text: '추가할 링크가 없습니다.', }, - count: createdLinks.count, + links: currentLinks.map(link => ({ + id: link.id, + url: link.link, + })), }; } + // 새 URL만 추가 + await this.prisma.myPageUserLink.createMany({ + data: newLinks.map(link => ({ + user_id: userId, + link: link.url, + })), + }); + + // 유저의 모든 링크 조회 + const updatedLinks = await this.prisma.myPageUserLink.findMany({ + where: { user_id: userId }, + select: { id: true, link: true }, + }); + + return { + message: { + code: 200, + text: '링크가 성공적으로 추가되었습니다.', + }, + links: updatedLinks.map(link => ({ + linkId: link.id, + url: link.link, + })), + }; +} + async deleteUserLinks(userId: number, linkIds: number[]) { const deletedLinks = await this.prisma.myPageUserLink.deleteMany({ where: { @@ -625,13 +643,19 @@ export class UserService { user_id: userId, }, }); - + const updatedLinks = await this.prisma.myPageUserLink.findMany({ + where: { user_id: userId }, + select: { id: true, link: true }, + }); return { message: { code: 200, text: '링크가 성공적으로 삭제되었습니다.', }, - count: deletedLinks.count, + links: updatedLinks.map(link => ({ + linkId: link.id, + url: link.link, + })), }; } @@ -743,10 +767,16 @@ export class UserService { return { message: { - code: 200, + code: 201, text: '사용자 이력서 작성에 성공했습니다.', }, - newResume, + resume: { + userId: newResume.user_id, + resumeId: newResume.id, + title: newResume.title, + portfolioUrl: newResume.portfolio_url, + detail:newResume.detail + } }; } @@ -784,7 +814,13 @@ export class UserService { code: 200, text: '사용자 이력서 수정에 성공했습니다.', }, - updatedResume, + resume: { + userId: updatedResume.user_id, + resumeId: updatedResume.id, + title: updatedResume.title, + portfolioUrl: updatedResume.portfolio_url, + detail:updatedResume.detail + } }; } From 13fb35a9011ae47c7ab668c7ecb1f1760e4b3cb3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 01:59:36 +0900 Subject: [PATCH 166/414] =?UTF-8?q?[Feat]=20user=20Swagger=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/SwaggerDocs/user.swagger.ts | 969 +++++++++++++++++++ 1 file changed, 969 insertions(+) create mode 100644 src/modules/user/SwaggerDocs/user.swagger.ts diff --git a/src/modules/user/SwaggerDocs/user.swagger.ts b/src/modules/user/SwaggerDocs/user.swagger.ts new file mode 100644 index 0000000..0a5dc8a --- /dev/null +++ b/src/modules/user/SwaggerDocs/user.swagger.ts @@ -0,0 +1,969 @@ +import { + ApiOperation, + ApiParam, + ApiResponse, + ApiBody, + ApiConsumes, + ApiQuery, +} from '@nestjs/swagger'; + +export const GetUserFollowersDocs = { + ApiOperation: ApiOperation({ + summary: '사용자를 팔로우하는 사용자 목록 조회', + description: '특정 사용자를 팔로우하는 사용자들의 목록을 반환합니다.', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '팔로워 목록 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '팔로워 목록 조회에 성공했습니다', + }, + followerUsers: [ + { + id: 1, + nickname: 'Alice', + profileUrl: 'https://example.com/profiles/alice.jpg', + }, + ], + }, + }, + }), +}; + +export const GetUserFollowingsDocs = { + ApiOperation: ApiOperation({ + summary: '사용자가 팔로우한 사용자 목록 조회', + description: '특정 사용자가 팔로우한 사용자들의 목록을 반환합니다.', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '팔로잉 목록 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '팔로잉 목록 조회에 성공했습니다', + }, + followingUsers: [ + { + id: 3, + nickname: 'Charlie', + profileUrl: 'https://example.com/profiles/charlie.jpg', + }, + ], + }, + }, + }), +}; + +export const GetUserProfileDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 프로필 정보 조회', + description: '특정 사용자의 프로필 정보를 반환합니다.', + }), + ApiParam: ApiParam({ + name: 'userId', + required: true, + description: '조회할 사용자의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '사용자 프로필 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '유저 프로필 조회에 성공했습니다', + }, + userId: 1, + nickname: 'test', + role: 'Programmer', + introduce: 'I am a software engineer.', + specificData: { + githubUsername: 'JohnGit', + myPageProjects: [ + { + title: 'My Project', + description: 'Project description here.', + links: [ + { + type: 'Github', + url: 'https://github.com/johndoe/myproject', + }, + ], + }, + ], + }, + followerCount: 12, + followingCount: 34, + isOwnProfile: true, + }, + }, + }), +}; + +export const GetUserProfileHeaderDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 프로필 헤더 정보 조회', + description: '특정 사용자의 프로필 헤더 정보를 반환합니다. (간략한 정보)', + }), + ApiParam: ApiParam({ + name: 'userId', + required: true, + description: '조회할 사용자의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '사용자 프로필 헤더 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '유저 프로필(헤더 부분) 조회에 성공했습니다', + }, + userId: 1, + nickname: 'testForChangeNickName', + profileUrl: + '"https://user-profile-icons.s3.ap-northeast-2.amazonaws.com/pad_users/profile_7d52f324-8694-4789-a4f9-ab4dfc40e482.jpeg', + role: 'Programmer', + introduce: 'I am a software engineer.', + userLinks: [ + 'https://github.com/Ss0Mae', + 'https://www.linkedin.com/in/Ss0Mae', + ], + isOwnProfile: true, + isFollowing: false, + }, + }, + }), +}; + +export const AddProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 추가', + description: '사용자의 마이페이지에 새 프로젝트를 추가합니다.', + }), + ApiBody: ApiBody({ + description: + '추가할 프로젝트 데이터 typeId : 1 = Github, 2 = Web, 3 = IOS, 4 = Android', + schema: { + example: { + title: 'My Project', + description: 'This is a description of my project.', + links: [ + { url: 'https://github.com/myproject', typeId: 1 }, + { url: 'https://myproject.com', typeId: 2 }, + ], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 201, + description: '프로젝트 추가 성공', + schema: { + example: { + message: { + code: 201, + text: '마이페이지에 프로젝트 추가에 성공했습니다', + }, + myPageProjectId: 1, + title: 'My Project', + description: 'This is a description of my project.', + links: [ + { id: 1, url: 'https://github.com/myproject', type: 'Github' }, + { id: 2, url: 'https://myproject.com', type: 'Website' }, + ], + }, + }, + }), +}; + +export const UpdateProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 수정', + description: '사용자의 특정 프로젝트를 수정합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + required: true, + description: '수정할 프로젝트의 ID', + type: 'string', + }), + ApiBody: ApiBody({ + description: '수정할 프로젝트 데이터', + schema: { + example: { + title: 'Updated Project', + description: 'This is an updated description.', + links: [ + { url: 'https://github.com/updatedproject', typeId: 1 }, + { url: 'https://updatedproject.com', typeId: 2 }, + ], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 수정 성공', + schema: { + example: { + message: { + code: 200, + text: '프로젝트 수정에 성공했습니다', + }, + myPageProjectId: 1, + title: 'Updated Project', + description: 'This is an updated description.', + links: [ + { id: 1, url: 'https://github.com/updatedproject', type: 'Github' }, + { id: 2, url: 'https://updatedproject.com', type: 'Website' }, + ], + }, + }, + }), +}; + +export const DeleteProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 삭제', + description: '사용자의 특정 프로젝트를 삭제합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + required: true, + description: '삭제할 프로젝트의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 삭제 성공', + schema: { + example: { + message: { + code: 200, + text: '프로젝트 삭제에 성공했습니다', + }, + }, + }, + }), +}; + +export const AddWorkDocs = { + ApiOperation: ApiOperation({ + summary: '아티스트 작업물 추가', + description: '아티스트의 새 작업물(musicUrl)을 추가합니다.', + }), + ApiBody: ApiBody({ + description: '추가할 작업물의 musicUrl', + schema: { + example: { + musicUrl: 'https://www.youtube.com/watch?v=example', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 201, + description: '작업물 추가 성공', + schema: { + example: { + message: { + code: 201, + text: '작업물이 성공적으로 추가되었습니다.', + }, + musicId: 1, + musicUrl: 'https://www.youtube.com/watch?v=example', + }, + }, + }), +}; + +export const UpdateWorkDocs = { + ApiOperation: ApiOperation({ + summary: '아티스트 작업물 수정', + description: '특정 작업물의 musicUrl을 수정합니다.', + }), + ApiParam: ApiParam({ + name: 'workId', + required: true, + description: '수정할 작업물의 ID', + type: 'string', + }), + ApiBody: ApiBody({ + description: '수정할 작업물의 musicUrl', + schema: { + example: { + musicUrl: 'https://www.youtube.com/watch?v=updated_example', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '작업물 수정 성공', + schema: { + example: { + message: { + code: 200, + text: '작업물이 성공적으로 수정되었습니다.', + }, + musicId: 1, + musicUrl: 'https://www.youtube.com/watch?v=updated_example', + }, + }, + }), +}; + +export const DeleteWorkDocs = { + ApiOperation: ApiOperation({ + summary: '아티스트 작업물 삭제', + description: '특정 작업물을 삭제합니다.', + }), + ApiParam: ApiParam({ + name: 'workId', + required: true, + description: '삭제할 작업물의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '작업물 삭제 성공', + schema: { + example: { + message: { + code: 200, + text: '작업물이 성공적으로 삭제되었습니다.', + }, + }, + }, + }), +}; + +export const UpdateGithubUsernameDocs = { + ApiOperation: ApiOperation({ + summary: 'GitHub 닉네임 업데이트', + description: '사용자의 GitHub 닉네임을 업데이트합니다.', + }), + ApiBody: ApiBody({ + description: '업데이트할 GitHub 닉네임', + schema: { + example: { + githubUsername: 'NewGithubUsername', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: 'GitHub 닉네임 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '깃허브 유저네임 등록에 성공했습니다.', + }, + }, + }, + }), +}; + +export const GetUserSettingDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 설정 정보 조회', + description: '사용자의 설정 정보를 조회합니다.', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '유저 정보 세팅페이지 정보 조회에 성공했습니다', + schema: { + example: { + message: { + code: 200, + text: '유저 정보 세팅페이지 정보 조회에 성공했습니다.', + }, + nickname: 'UserNickname', + profileUrl: 'UserProfileURL', + introduce: 'User Introduce', + status: '구인 중', + links: [ + { id: 1, url: 'https://github.com/Ss0Mae' }, + { id: 2, url: 'https://www.google.com' }, + ], + skills: ['TypeScript', 'Nest.js'], + jobDetail: 'IT / 백엔드개발자', + notification: { + pushAlert: false, + followingAlert: false, + projectAlert: false, + }, + }, + }, + }), +}; + +export const PatchUserNicknameDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 닉네임 업데이트', + description: '사용자의 닉네임을 업데이트합니다.', + }), + ApiBody: ApiBody({ + description: '업데이트할 닉네임', + schema: { + example: { + nickname: 'NewNickname', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '닉네임 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '닉네임이 성공적으로 업데이트되었습니다.', + }, + nickname: 'NewNickName', + }, + }, + }), +}; + +export const PatchUserIntroduceDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 자기소개 업데이트', + description: '사용자의 자기소개 내용을 업데이트합니다.', + }), + ApiBody: ApiBody({ + description: '업데이트할 자기소개 내용', + schema: { + example: { + introduce: '안녕하세요. 저는 백엔드 개발자입니다.', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '자기소개 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자의 소개가 성공적으로 업데이트되었습니다.', + }, + introduce: '안녕하세요. 저는 백엔드 개발자입니다.', + }, + }, + }), +}; + +export const PatchUserStatusDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 상태 업데이트', + description: '사용자의 현재 상태를 업데이트합니다. (예: 활동 중, 휴식 중)', + }), + ApiBody: ApiBody({ + description: + '업데이트할 상태 ID 1 = 둘러보는중 2 = 외주 / 프로젝트 구하는 중 3 = 구인하는 중 4 = 작업 중', + schema: { + example: { + statusId: 1, // 예: 1=활동 중, 2=휴식 중 + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '상태 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자의 상태가 성공적으로 업데이트되었습니다.', + }, + status: '활동 중', + }, + }, + }), +}; + +export const UpdateUserJobDetailDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 직업 세부정보 업데이트', + description: '사용자의 직업 카테고리 및 세부정보를 업데이트합니다.', + }), + ApiBody: ApiBody({ + description: '업데이트할 직업 카테고리 및 직무 상세', + schema: { + example: { + category: '개발자', + jobDetail: '백엔드 엔지니어', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '직업 세부정보 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '직무 정보가 성공적으로 업데이트되었습니다.', + }, + jobDetail: '개발자 / 백엔드 엔지니어', + }, + }, + }), +}; + +export const PatchUserSkillsDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 기술 추가', + description: '사용자의 기술 스택에 새로운 기술을 추가합니다.', + }), + ApiBody: ApiBody({ + description: '추가할 기술 목록', + schema: { + example: { + skills: ['JavaScript', 'TypeScript', 'Node.js'], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '기술 추가 성공', + schema: { + example: { + message: { + code: 200, + text: '기술 스택이 성공적으로 추가되었습니다.', + }, + skills: ['JavaScript', 'TypeScript', 'Node.js'], + }, + }, + }), +}; + +export const DeleteUserSkillsDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 기술 삭제', + description: '사용자의 기술 스택에서 특정 기술들을 삭제합니다.', + }), + ApiBody: ApiBody({ + description: '삭제할 기술 목록', + schema: { + example: { + skills: ['JavaScript', 'TypeScript', 'Node.js'], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '기술 삭제 성공', + schema: { + example: { + message: { + code: 200, + text: '기술 스택이 성공적으로 삭제되었습니다.', + }, + skills: ['JavaScript', 'TypeScript', 'Node.js'], + }, + }, + }), +}; + +export const PatchProfileImageDocs = { + ApiOperation: ApiOperation({ + summary: '프로필 이미지 업데이트', + description: '사용자의 프로필 이미지를 업데이트합니다.', + }), + ApiConsumes: ApiConsumes('multipart/form-data'), + ApiBody: ApiBody({ + description: '업데이트할 이미지 파일', + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로필 이미지 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '프로필 이미지가 성공적으로 업데이트되었습니다.', + }, + user: { + userId: 1, + nickname: 'ssomae', + profileUrl: 'https://example.com/profiles/ssomae.jpg', + }, + }, + }, + }), +}; + +export const PatchUserNotificationDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 알림 설정 업데이트', + description: + '사용자의 알림 설정(push, following, project)을 업데이트합니다.', + }), + ApiBody: ApiBody({ + description: '업데이트할 알림 설정', + schema: { + example: { + notification: { + pushAlert: true, + followingAlert: false, + projectAlert: true, + }, + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '알림 설정 업데이트 성공', + schema: { + example: { + message: { + code: 200, + text: '알림 설정이 성공적으로 업데이트되었습니다.', + }, + notifications: { + pushAlert: true, + followingAlert: false, + projectAlert: true, + }, + }, + }, + }), +}; + +export const AddUserLinksDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 링크 추가', + description: '사용자의 프로필에 새로운 링크를 추가합니다.', + }), + ApiBody: ApiBody({ + description: '추가할 링크 목록', + schema: { + example: { + links: [ + { url: 'https://github.com/user' }, + { url: 'https://linkedin.com/in/user' }, + ], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '링크 추가 성공', + schema: { + example: { + message: { + code: 200, + text: '링크가 성공적으로 추가되었습니다.', + }, + links: [ + { linkId: 1, url: 'https://github.com/user' }, + { linkId: 2, url: 'https://linkedin.com/in/user' }, + ], + }, + }, + }), +}; + +export const DeleteUserLinksDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 링크 삭제', + description: '사용자의 프로필에서 특정 링크를 삭제합니다.', + }), + ApiBody: ApiBody({ + description: '삭제할 링크 ID 목록', + schema: { + example: { + linkIds: [1, 2], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '링크 삭제 성공', + schema: { + example: { + message: { + code: 200, + text: '링크가 성공적으로 삭제되었습니다.', + }, + links: [ + { linkId: 3, url: 'https://twitter.com/user' }, + { linkId: 4, url: 'https://facebook.com/user' }, + ], + }, + }, + }), +}; + +export const GetUserResumeDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 이력서 조회', + description: '특정 사용자의 이력서를 조회합니다.', + }), + ApiParam: ApiParam({ + name: 'userId', + required: true, + description: '조회할 사용자의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '이력서 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자 이력서 조회에 성공했습니다.', + }, + userId: 1, + title: 'Backend Developer', + jobDetail: '개발자 / 백엔드 엔지니어', + skills: ['Node.js', 'TypeScript', 'GraphQL'], + portfolioUrl: 'https://portfolio.com/user', + detail: '경력 및 프로젝트 설명', + isOwnProfile: true, + }, + }, + }), +}; + +export const CreateUserResumeDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 이력서 생성', + description: '새로운 이력서를 생성합니다.', + }), + ApiBody: ApiBody({ + description: '생성할 이력서 정보', + schema: { + example: { + title: 'Frontend Developer', + portfolioUrl: 'https://github.com/user', + detail: '5년간 프론트엔드 개발 경력.', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 201, + description: '이력서 생성 성공', + schema: { + example: { + message: { + code: 201, + text: '사용자 이력서 생성에 성공했습니다.', + }, + resume: { + userId: 1, + resumeId: 1, + title: 'Resume Title', + portfolioUrl: 'portfolioURL', + detail: 'Resume Detail', + }, + }, + }, + }), +}; + +export const UpdateUserResumeDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 이력서 수정', + description: '기존 이력서를 수정합니다.', + }), + ApiParam: ApiParam({ + name: 'resumeId', + required: true, + description: '수정할 이력서의 ID', + type: 'string', + }), + ApiBody: ApiBody({ + description: '수정할 이력서 정보', + schema: { + example: { + title: 'Updated Developer Title', + portfolioUrl: 'https://updated.github.com/user', + detail: 'Updated details about the resume.', + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '이력서 수정 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자 이력서 수정에 성공했습니다.', + }, + updatedResume: { + userId: 1, + resumeId: 1, + title: 'Resume Title', + portfolioUrl: 'portfolioURL', + detail: 'Resume Detail', + }, + }, + }, + }), +}; + +export const DeleteUserResumeDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 이력서 삭제', + description: '기존 이력서를 삭제합니다.', + }), + ApiParam: ApiParam({ + name: 'resumeId', + required: true, + description: '삭제할 이력서의 ID', + type: 'string', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '이력서 삭제 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자 이력서 삭제에 성공했습니다.', + }, + }, + }, + }), +}; + +export const GetUserFeedPostsDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 피드 조회', + description: '특정 사용자가 작성한 피드 목록을 조회합니다.', + }), + ApiParam: ApiParam({ + name: 'userId', + required: true, + description: '피드를 조회할 사용자의 ID', + type: 'string', + }), + ApiQuery: [ + ApiQuery({ + name: 'page', + required: false, + description: '페이지 번호 (기본값: 1)', + type: 'number', + }), + ApiQuery({ + name: 'limit', + required: false, + description: '페이지 당 항목 수 (기본값: 10)', + type: 'number', + }), + ], + ApiResponse: ApiResponse({ + status: 200, + description: '사용자 피드 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자 피드 조회에 성공했습니다.', + }, + feeds: [ + { + id: 1, + title: 'My First Feed', + content: 'This is the content of my feed.', + thumbnailUrl: 'https://example.com/thumbnail.jpg', + createdAt: '2023-01-01T00:00:00.000Z', + view: 100, + likeCount: 10, + commentCount: 5, + user: { + id: 1, + nickname: 'JohnDoe', + profileUrl: 'https://example.com/profile.jpg', + }, + tags: ['Tag1', 'Tag2'], + }, + ], + totalCount: 1, + currentPage: 1, + totalPages: 1, + }, + }, + }), +}; + +export const GetUserConnectionHubProjectsDocs = { + ApiOperation: ApiOperation({ + summary: '사용자 커넥션 허브 조회', + description: '특정 사용자가 생성하거나 지원한 프로젝트 목록을 조회합니다.', + }), + ApiParam: ApiParam({ + name: 'userId', + required: true, + description: '프로젝트를 조회할 사용자의 ID', + type: 'string', + }), + ApiQuery: [ + ApiQuery({ + name: 'type', + required: true, + description: "프로젝트 유형 ('applied' 또는 'created')", + enum: ['applied', 'created'], + }), + ApiQuery({ + name: 'page', + required: false, + description: '페이지 번호 (기본값: 1)', + type: 'number', + }), + ApiQuery({ + name: 'limit', + required: false, + description: '페이지 당 항목 수 (기본값: 10)', + type: 'number', + }), + ], + ApiResponse: ApiResponse({ + status: 200, + description: '커넥션 허브 프로젝트 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '사용자 커넥션허브 조회에 성공했습니다.', + }, + projects: [ + { + projectPostId: 1, + title: 'Project 1', + content: 'Description of project 1', + thumbnailUrl: 'https://example.com/project-thumbnail.jpg', + startDate: '2023-01-01', + duration: '6 months', + recruiting: true, + view: 100, + tags: ['Node.js', 'TypeScript'], + }, + ], + totalCount: 1, + currentPage: 1, + totalPages: 1, + }, + }, + }), +}; From 84aac012dea19cf70fb10e9575a5ec35ad27941b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 02:01:45 +0900 Subject: [PATCH 167/414] =?UTF-8?q?[Feat]=20user=20Swagger=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/{SwaggerDocs/user.swagger.ts => docs/user.docs.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/modules/user/{SwaggerDocs/user.swagger.ts => docs/user.docs.ts} (100%) diff --git a/src/modules/user/SwaggerDocs/user.swagger.ts b/src/modules/user/docs/user.docs.ts similarity index 100% rename from src/modules/user/SwaggerDocs/user.swagger.ts rename to src/modules/user/docs/user.docs.ts From bff05f6bd129441d55f7efaf2ccd5e931ec5a3d3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 02:01:49 +0900 Subject: [PATCH 168/414] =?UTF-8?q?[Feat]=20user=20Swagger=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 42 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 0c54d3b..9357f60 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -47,9 +47,11 @@ import { GetUserResumeDocs, CreateUserResumeDocs, UpdateUserResumeDocs, - DeleteUserResumeDocs + DeleteUserResumeDocs, + GetUserFeedPostsDocs, + GetUserConnectionHubProjectsDocs, } from './docs/user.docs'; -import {ApiBearerAuth} from '@nestjs/swagger'; +import { ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; @ApiBearerAuth() @UseGuards(JwtAuthGuard) @@ -359,6 +361,21 @@ export class UserController { } @Get(':userId/feeds') + @GetUserFeedPostsDocs.ApiOperation + @GetUserFeedPostsDocs.ApiParam + @ApiQuery({ + name: 'page', + required: false, + description: '페이지 번호 (기본값: 1)', + type: 'number', + }) + @ApiQuery({ + name: 'limit', + required: false, + description: '페이지 당 항목 수 (기본값: 10)', + type: 'number', + }) + @GetUserFeedPostsDocs.ApiResponse async getUserFeedPosts( @Param('userId') userId: string, @Query('page') page: number = 1, @@ -369,6 +386,27 @@ export class UserController { } @Get(':userId/connection-hub') + @GetUserConnectionHubProjectsDocs.ApiOperation + @GetUserConnectionHubProjectsDocs.ApiParam + @ApiQuery({ + name: 'type', + required: true, + description: "프로젝트 유형 ('applied' 또는 'created')", + enum: ['applied', 'created'], + }) + @ApiQuery({ + name: 'page', + required: false, + description: '페이지 번호 (기본값: 1)', + type: 'number', + }) + @ApiQuery({ + name: 'limit', + required: false, + description: '페이지 당 항목 수 (기본값: 10)', + type: 'number', + }) + @GetUserConnectionHubProjectsDocs.ApiResponse async getUserConnectionHubProjects( @Param('userId') userId: string, @Query('type') type: 'applied' | 'created', From 01648d53cd060a7239aae4cf374d24ec5b712e3c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 13:11:43 +0900 Subject: [PATCH 169/414] =?UTF-8?q?[Fix]=20=EB=A7=81=ED=81=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20API=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20Swagger=20Docs=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 9 +-- src/modules/user/user.service.ts | 110 ++++++++++++++--------------- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index 0a5dc8a..6c7aee3 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -656,20 +656,17 @@ export const AddUserLinksDocs = { description: '추가할 링크 목록', schema: { example: { - links: [ - { url: 'https://github.com/user' }, - { url: 'https://linkedin.com/in/user' }, - ], + links: ['https://github.com/user', 'https://linkedin.com/in/user'], }, }, }), ApiResponse: ApiResponse({ - status: 200, + status: 201, description: '링크 추가 성공', schema: { example: { message: { - code: 200, + code: 201, text: '링크가 성공적으로 추가되었습니다.', }, links: [ diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index dc9ad47..1fbec33 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -573,69 +573,69 @@ export class UserService { }; } - async addUserLinks(userId: number, links: { url: string }[]) { - // 넘어온 URL 목록 - const urls = links.map(link => link.url); - - // 이미 존재하는 URL 조회 - const existingLinks = await this.prisma.myPageUserLink.findMany({ - where: { - user_id: userId, - link: { in: urls }, - }, - select: { link: true }, - }); - - // 이미 존재하는 URL 필터링 - const existingUrls = existingLinks.map(link => link.link); - const newLinks = links.filter(link => !existingUrls.includes(link.url)); - - // 추가할 URL이 없으면 바로 반환 - if (newLinks.length === 0) { - // 유저의 현재 링크 조회 - const currentLinks = await this.prisma.myPageUserLink.findMany({ + async addUserLinks(userId: number, links: { url: string }[]) { + // 넘어온 URL 목록 + const urls = links.map(link => link.url); + + // 이미 존재하는 URL 조회 + const existingLinks = await this.prisma.myPageUserLink.findMany({ + where: { + user_id: userId, + link: { in: urls }, + }, + select: { link: true }, + }); + + // 이미 존재하는 URL 필터링 + const existingUrls = existingLinks.map(link => link.link); + const newLinks = links.filter(link => !existingUrls.includes(link.url)); + + // 추가할 URL이 없으면 바로 반환 + if (newLinks.length === 0) { + // 유저의 현재 링크 조회 + const currentLinks = await this.prisma.myPageUserLink.findMany({ + where: { user_id: userId }, + select: { id: true, link: true }, + }); + + return { + message: { + code: 201, + text: '추가할 링크가 없습니다.', + }, + links: currentLinks.map(link => ({ + linkId: link.id, + url: link.link, + })), + }; + } + + // 새 URL만 추가 + await this.prisma.myPageUserLink.createMany({ + data: newLinks.map(link => ({ + user_id: userId, + link: link.url, + })), + }); + + // 유저의 모든 링크 조회 + const updatedLinks = await this.prisma.myPageUserLink.findMany({ where: { user_id: userId }, select: { id: true, link: true }, }); return { message: { - code: 200, - text: '추가할 링크가 없습니다.', + code: 201, + text: '링크가 성공적으로 추가되었습니다.', }, - links: currentLinks.map(link => ({ - id: link.id, + links: updatedLinks.map(link => ({ + linkId: link.id, url: link.link, })), }; } - // 새 URL만 추가 - await this.prisma.myPageUserLink.createMany({ - data: newLinks.map(link => ({ - user_id: userId, - link: link.url, - })), - }); - - // 유저의 모든 링크 조회 - const updatedLinks = await this.prisma.myPageUserLink.findMany({ - where: { user_id: userId }, - select: { id: true, link: true }, - }); - - return { - message: { - code: 200, - text: '링크가 성공적으로 추가되었습니다.', - }, - links: updatedLinks.map(link => ({ - linkId: link.id, - url: link.link, - })), - }; -} - async deleteUserLinks(userId: number, linkIds: number[]) { const deletedLinks = await this.prisma.myPageUserLink.deleteMany({ where: { @@ -775,8 +775,8 @@ export class UserService { resumeId: newResume.id, title: newResume.title, portfolioUrl: newResume.portfolio_url, - detail:newResume.detail - } + detail: newResume.detail, + }, }; } @@ -819,8 +819,8 @@ export class UserService { resumeId: updatedResume.id, title: updatedResume.title, portfolioUrl: updatedResume.portfolio_url, - detail:updatedResume.detail - } + detail: updatedResume.detail, + }, }; } From 461b9039a4c33efb1b5f1877cde65131f083f14f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 14:24:48 +0900 Subject: [PATCH 170/414] =?UTF-8?q?[Fix]=20=ED=94=BC=EB=93=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index edece03..4a09243 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -178,10 +178,13 @@ export class FeedService { }); if (!result.length) { - throw new HttpException( - '게시글을 찾을 수 없습니다', - HttpStatus.NOT_FOUND - ); + return { + comments: [], + message: { + code: 200, + message: '개별 피드(댓글)를 정상적으로 조회했습니다.', + }, + }; } const comments = result.map(c => ({ From 268a10d3ce97e1e92ab0d04335d770a39c573271 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 14:42:41 +0900 Subject: [PATCH 171/414] =?UTF-8?q?[Fix]=20db=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 1 - 1 file changed, 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 567b4a5..09a1d34 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,7 +38,6 @@ model User { MyPageProject MyPageProject[] UserLinks MyPageUserLink[] ProgrammerData ProgrammerData? - MyPageProject MyPageProject[] ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] From d11ee776e0544a4b96a78f2498f6884693ce7c68 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 14:43:00 +0900 Subject: [PATCH 172/414] =?UTF-8?q?[Fix]=20Prisma=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 567b4a5..b3fdb29 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,7 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? + job_detail String? ArtistData ArtistData[] Channel_users Channel_users[] FeedComments FeedComment[] @@ -38,7 +39,6 @@ model User { MyPageProject MyPageProject[] UserLinks MyPageUserLink[] ProgrammerData ProgrammerData? - MyPageProject MyPageProject[] ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] @@ -58,7 +58,7 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) user_id Int title String description String @@ -67,7 +67,7 @@ model MyPageProject { user User @relation(fields: [user_id], references: [id]) ProjectLinks MyPageProjectLink[] - @@index([user_id], map: "MyPageProject_user_id_idx") // 인덱스 추가 + @@index([user_id]) } model MyPageProjectLink { @@ -96,10 +96,12 @@ model ProgrammerData { } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int - music_url String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + music_url String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "ArtistData_user_id_fkey") } model Status { From b1d34af5ec3fccdbc3a4059429f0a765dc726252 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 15:04:14 +0900 Subject: [PATCH 173/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=B6=94=EA=B0=80=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 7 +++++++ src/feed/feed.service.ts | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 01d8af4..a078447 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -121,6 +121,13 @@ export class FeedController { return this.feedService.deleteComment(userId, feedId, commentId); } + @Post('comment/:id') + @UseGuards(JwtAuthGuard) + async handleCommentLikes(@Req() req, @Param('id') commentId: number) { + const userId = req.user.user_id; + return await this.feedService.handleCommentLikes(userId, commentId); + } + // 이미지 업로드 @Post('image') @UseInterceptors(FileInterceptor('file')) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 4a09243..f8297a9 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -500,6 +500,29 @@ export class FeedService { return { message: { code: 200, message: '댓글 수정이 완료되었습니다.' } }; } + // 댓글 좋아요 추가/제거 + async handleCommentLikes(userId: number, commentId: number) { + // 좋아요 여부 확인 + const exist = await this.prisma.feedCommentLikes.findMany({ + where: { user_id: userId, comment_id: commentId }, + }); + + if (exist.length) { + // 있을 시 좋아요 제거 + await this.prisma.feedCommentLikes.deleteMany({ + where: { user_id: userId, comment_id: commentId }, + }); + + return { message: { code: 200, message: '좋아요가 취소되었습니다.' } }; + } else { + // 없을 시 좋아요 추가 + await this.prisma.feedCommentLikes.create({ + data: { user_id: userId, comment_id: commentId }, + }); + return { message: { code: 200, message: '좋아요가 추가되었습니다.' } }; + } + } + // 게시글 권한 확인 async feedAuth(userId: number, feedId: number) { const auth = await this.prisma.feedPost.findUnique({ From dbba9199b7131b06c7d481035eb771b8aa7264eb Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 15:06:30 +0900 Subject: [PATCH 174/414] =?UTF-8?q?[Feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EB=8C=93=EA=B8=80=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index f8297a9..db831f0 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -174,6 +174,7 @@ export class FeedService { profile_url: true, }, }, + FeedCommentLikes: true, }, }); @@ -195,6 +196,7 @@ export class FeedService { userProfileUrl: c.user.profile_url, comment: c.content, createdAt: c.created_at, + likes: c.FeedCommentLikes.length, })); return { From 4448c1c7443155d111856e35b09f5495da0e8be8 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 15:23:11 +0900 Subject: [PATCH 175/414] =?UTF-8?q?[Fix]=20redis=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/redis.config.ts | 2 +- src/modules/user/user.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts index 35615b9..32d6853 100644 --- a/src/config/redis.config.ts +++ b/src/config/redis.config.ts @@ -4,5 +4,5 @@ import { Injectable } from '@nestjs/common'; export class RedisConfig { public static readonly host = process.env.REDIS_HOST || 'localhost'; public static readonly port = parseInt(process.env.REDIS_PORT, 10) || 6379; - public static readonly password = process.env.REDIS_PASSWORD || undefined; + public static readonly password = 'Ksok484545!'; } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 1fbec33..206cd0a 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -80,8 +80,8 @@ export class UserService { code: 200, text: '유저 프로필 조회에 성공했습니다', }, - userId: user.id, - role: user.role.name, + //userId: user.id, + //role: user.role.name, status: user.status.name, applyCount: user.apply_count, postCount: user.post_count, From 09bfea7588af96222fec105ae15c5f957078feb3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 15:40:55 +0900 Subject: [PATCH 176/414] =?UTF-8?q?[Fix]=20redis=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 09a1d34..b3fdb29 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,7 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? + job_detail String? ArtistData ArtistData[] Channel_users Channel_users[] FeedComments FeedComment[] @@ -57,7 +58,7 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) user_id Int title String description String @@ -66,7 +67,7 @@ model MyPageProject { user User @relation(fields: [user_id], references: [id]) ProjectLinks MyPageProjectLink[] - @@index([user_id], map: "MyPageProject_user_id_idx") // 인덱스 추가 + @@index([user_id]) } model MyPageProjectLink { @@ -95,10 +96,12 @@ model ProgrammerData { } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int - music_url String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + music_url String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "ArtistData_user_id_fkey") } model Status { From 51099f8f751ccee1a9546a98beaafa3a0e7a48bb Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 15:48:13 +0900 Subject: [PATCH 177/414] [Feat] Prisma db pull --- prisma/schema.prisma | 62 +++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 149095f..b3fdb29 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,6 @@ model User { role_id Int introduce String? status_id Int - job_detail String? // "카테고리 / 직무명" 형식으로 저장 apply_count Int? @default(0) post_count Int? @default(0) push_alert Boolean @default(false) @@ -26,7 +25,8 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt password String? - ArtistData ArtistData? + job_detail String? + ArtistData ArtistData[] Channel_users Channel_users[] FeedComments FeedComment[] FeedCommentLikes FeedCommentLikes[] @@ -36,18 +36,17 @@ model User { Follows Follows[] @relation("UserFollows") Message Message[] Message_status Message_status[] + MyPageProject MyPageProject[] + UserLinks MyPageUserLink[] ProgrammerData ProgrammerData? - MyPageProject MyPageProject? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] role Role @relation(fields: [role_id], references: [id]) status Status @relation(fields: [status_id], references: [id]) UserApplyProject UserApplyProject[] - UserLinks MyPageUserLink[] UserSkills UserSkill[] - @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") } @@ -59,46 +58,50 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) - user_id Int @unique + id Int @id @default(autoincrement()) + user_id Int title String description String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + user User @relation(fields: [user_id], references: [id]) ProjectLinks MyPageProjectLink[] + + @@index([user_id]) } model MyPageProjectLink { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) project_id Int type_id Int url String - project MyPageProject @relation(fields: [project_id], references: [id]) - type LinkType @relation(fields: [type_id], references: [id]) + project MyPageProject @relation(fields: [project_id], references: [id]) + type LinkType @relation(fields: [type_id], references: [id]) @@index([project_id], map: "ProjectLink_project_id_fkey") @@index([type_id], map: "ProjectLink_type_id_fkey") } model LinkType { - id Int @id @default(autoincrement()) - name String @unique + id Int @id @default(autoincrement()) + name String @unique Links MyPageProjectLink[] } model ProgrammerData { - id Int @id @default(autoincrement()) - user_id Int @unique - github_username String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int @unique + github_username String + user User @relation(fields: [user_id], references: [id]) } model ArtistData { - id Int @id @default(autoincrement()) - user_id Int @unique - music_url String - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + music_url String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "ArtistData_user_id_fkey") } model Status { @@ -108,12 +111,12 @@ model Status { } model Resume { - id Int @id @default(autoincrement()) - user_id Int - title String + id Int @id @default(autoincrement()) + user_id Int + title String + detail String @db.Text portfolio_url String? - detail String @db.Text - user User @relation(fields: [user_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([user_id], map: "Resume_user_id_fkey") } @@ -234,13 +237,14 @@ model ProjectPost { applicant_count Int view Int saved_count Int + created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) work_type WorkType @relation(fields: [work_type_id], references: [id]) Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] - created_at DateTime @default(now()) + @@index([user_id], map: "ProjectPost_user_id_fkey") @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") } From 7b11b9dbbc58559f953de3e47141e0a726c72ca3 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 16:01:31 +0900 Subject: [PATCH 178/414] =?UTF-8?q?[Fix]=20getSocketId=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=A6=AC=ED=84=B4=EA=B0=92=20=EC=98=B5=EC=85=94?= =?UTF-8?q?=EB=84=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index fa7fac2..e263bcd 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -120,7 +120,7 @@ export class ChatService { client_id: true, }, }); - return socketId[0].client_id; + return socketId[0]?.client_id; } // 유저가 참여한 채널 전체 조회 From 48749e240a01dbdf4fba3982183ae3d5e940f7e4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 16:43:34 +0900 Subject: [PATCH 179/414] =?UTF-8?q?[Fix]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95,=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 35 +++++++++++------------ src/modules/user/user.service.ts | 46 ++++++++++++++---------------- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index 6c7aee3..f0efbe1 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -79,27 +79,24 @@ export const GetUserProfileDocs = { code: 200, text: '유저 프로필 조회에 성공했습니다', }, - userId: 1, - nickname: 'test', - role: 'Programmer', - introduce: 'I am a software engineer.', - specificData: { - githubUsername: 'JohnGit', - myPageProjects: [ - { - title: 'My Project', - description: 'Project description here.', - links: [ - { - type: 'Github', - url: 'https://github.com/johndoe/myproject', - }, - ], - }, - ], - }, + status: '둘러보는 중', + githubUsername: 'JohnGit', + works: [ + { + title: 'My Project', + description: 'Project description here.', + links: [ + { + type: 'Github', + url: 'https://github.com/johndoe/myproject', + }, + ], + }, + ], followerCount: 12, followingCount: 34, + applyCount: 12, + postCount: 17, isOwnProfile: true, }, }, diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 206cd0a..59cca06 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -49,19 +49,32 @@ export class UserService { where: { following_user_id: targetUserId }, }); - // 사용자 직업군에 따른 맞춤 데이터 생성 - let specificData = null; + // 반환 데이터 구성 + const response = { + message: { + code: 200, + text: '유저 프로필 조회에 성공했습니다', + }, + status: user.status.name, + applyCount: user.apply_count, + postCount: user.post_count, + followerCount, // 팔로워 수 + followingCount, // 팔로잉 수 + isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 + }; + + // 사용자 직업군에 따라 데이터를 동적으로 추가 if (user.role.name === 'Artist') { - specificData = { - works: user.ArtistData.map(works => works.music_url), // 단순 URL 배열로 변환 - }; + Object.assign(response, { + works: user.ArtistData.map(work => work.music_url), // 단순 URL 배열로 변환 + }); } else if ( user.role.name === 'Programmer' || user.role.name === 'Designer' ) { - specificData = { + Object.assign(response, { githubUsername: user.ProgrammerData?.github_username || null, - myPageProjects: user.MyPageProject + works: user.MyPageProject ? user.MyPageProject.map(project => ({ title: project.title, description: project.description, @@ -71,25 +84,10 @@ export class UserService { })), })) : [], - }; + }); } - // 반환 데이터 구성 - return { - message: { - code: 200, - text: '유저 프로필 조회에 성공했습니다', - }, - //userId: user.id, - //role: user.role.name, - status: user.status.name, - applyCount: user.apply_count, - postCount: user.post_count, - followerCount, // 팔로워 수 - followingCount, // 팔로잉 수 - specificData, // 직업군 맞춤 데이터 - isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 - }; + return response; } async getUserProfileHeader(loggedInUserId: number, targetUserId: number) { From 1c2b7b8bcbb9597b9cbba7b8a5936d6f51eae340 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 17:09:49 +0900 Subject: [PATCH 180/414] =?UTF-8?q?[Feat]=20=EA=B7=B8=EB=A3=B9=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 47 ++++++++++++++++++++++++++++++++++++++++ src/chat/chat.service.ts | 25 +++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index e14ae46..ffd0b03 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -87,6 +87,53 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { client.emit('channelCreated', channel); } + @SubscribeMessage('createGroup') + async handleCreateGroup( + @MessageBody() data: { userIds: number[] }, + @ConnectedSocket() client: Socket + ) { + const { userIds } = data; + // userIds[0] => 클라이언트(그룹 채팅 마스터) + const userId = userIds[0]; + + // 채널 id 조회 + const channelId = await this.chatService.getGroupChannelId(userIds); + + // 채널 객체 조회 + const channelData = await this.chatService.getChannel(userId, channelId); + const channel = channelData.channel; + + // 채널에 마스터 유저 참여 + client.join(channelId.toString()); + console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); + + // 나머지 유저 온라인 여부 확인 + const groupMemberIds = userIds.filter(id => id !== userId); + const targetSockets = await this.chatService.getSocketIds(groupMemberIds); + + // 온라인인 유저가 있을 때 + if (targetSockets.length) { + // 모든 소켓 확인 + const sockets = await this.server.fetchSockets(); + + const userSockets = sockets.filter(socket => + targetSockets.includes(socket.id) + ); + + // 유저2의 채널 리스트에 해당 채널 추가 + client.emit('channelAdded', channel); + userSockets.forEach(socket => { + socket.emit('channelAdded', channel); + }); + } else { + // 오프라인 일때 + console.log('오프라인인 유저가 있습니다.'); + } + + // 클라이언트에 채널id 전달 + client.emit('groupCreated', channel); + } + // 채널 참여 @SubscribeMessage('joinChannel') async handleJoinChannel( diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e263bcd..4c4ecfc 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -49,6 +49,22 @@ export class ChatService { return newChannel.id; } + async getGroupChannelId(userIds: number[]) { + // 새로운 채널 생성 + const channel = await this.prisma.channel.create({}); + const channelId = channel.id; + + const data = userIds.map(userId => ({ + channel_id: channelId, + user_id: userId, + })); + + // 매핑 테이블에 저장 + await this.prisma.channel_users.createMany({ data }); + + return channelId; + } + // 메세지 저장 async createMessage(type, channelId, userId, content) { return await this.prisma.message.create({ @@ -123,6 +139,15 @@ export class ChatService { return socketId[0]?.client_id; } + async getSocketIds(Ids: number[]) { + const socketIds = await this.prisma.online_users.findMany({ + where: { user_id: { in: Ids } }, + select: { client_id: true }, + }); + + return socketIds.map(id => id.client_id); + } + // 유저가 참여한 채널 전체 조회 async getAllChannels(id: number) { const result = await this.prisma.channel_users.findMany({ From c2c975e042ef741c747f4a6118bbd40bc0d71116 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 17:21:59 +0900 Subject: [PATCH 181/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=EC=8B=9C=20=EC=B1=84=EB=84=90=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index ffd0b03..c2278b7 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -146,7 +146,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { client.join(channelId.toString()); console.log(`유저 ${userId} 채널 ${channelId} 참여`); // 채널 객체 - const channel = { channelId }; + const channel = await this.chatService.getChannel(userId, channelId); // 클라이언트에 채널id 전달 client.emit('channelJoined', channel); From 283de3a200448239b5d538f2f16628f7d5ea9637 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 17:34:55 +0900 Subject: [PATCH 182/414] =?UTF-8?q?[Fix]=20channelJoined=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index c2278b7..47ef05b 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -146,7 +146,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { client.join(channelId.toString()); console.log(`유저 ${userId} 채널 ${channelId} 참여`); // 채널 객체 - const channel = await this.chatService.getChannel(userId, channelId); + const channelData = await this.chatService.getChannel(userId, channelId); + const { channel } = channelData; // 클라이언트에 채널id 전달 client.emit('channelJoined', channel); From 91a09eec85aad321db98f4dec5c0d7fce410664a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 17:36:57 +0900 Subject: [PATCH 183/414] =?UTF-8?q?[Feat]=20Prisma=20=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b3fdb29..4c10814 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -314,7 +314,8 @@ model UserApplyProject { model Channel { id Int @id @default(autoincrement()) - name String @default("default_channel_name") + title String @default("default_channel_title") + thumbnail_url String? active Boolean @default(true) created_at DateTime @default(now()) Channel_users Channel_users[] From 504dac63df44955a8967e25b3e5a4cae2dfa0a5f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 17:43:56 +0900 Subject: [PATCH 184/414] =?UTF-8?q?[Feat]=20=EA=B7=B8=EB=A3=B9=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=ED=8B=80,=20=EC=8D=B8=EB=84=A4=EC=9D=BCurl=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 14 ++++++++++---- src/chat/chat.service.ts | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 47ef05b..87e891c 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -46,7 +46,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 1대1 새 채팅방 생성 (userId1(클라이언트/본인), userId2(상대방)) @SubscribeMessage('createChannel') async handleCreateChannel( - @MessageBody() data: { userId1: number; userId2: number }, + @MessageBody() + data: { userId1: number; userId2: number }, @ConnectedSocket() client: Socket ) { const { userId1, userId2 } = data; @@ -89,15 +90,20 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @SubscribeMessage('createGroup') async handleCreateGroup( - @MessageBody() data: { userIds: number[] }, + @MessageBody() + data: { userIds: number[]; title: string; thumnailUrl: string }, @ConnectedSocket() client: Socket ) { - const { userIds } = data; + const { userIds, title, thumnailUrl } = data; // userIds[0] => 클라이언트(그룹 채팅 마스터) const userId = userIds[0]; // 채널 id 조회 - const channelId = await this.chatService.getGroupChannelId(userIds); + const channelId = await this.chatService.getGroupChannelId( + userIds, + title, + thumnailUrl + ); // 채널 객체 조회 const channelData = await this.chatService.getChannel(userId, channelId); diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 4c4ecfc..607956c 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -49,9 +49,18 @@ export class ChatService { return newChannel.id; } - async getGroupChannelId(userIds: number[]) { + async getGroupChannelId( + userIds: number[], + title: string, + thumnailUrl: string + ) { // 새로운 채널 생성 - const channel = await this.prisma.channel.create({}); + const channel = await this.prisma.channel.create({ + data: { + title, + thumbnail_url: thumnailUrl, + }, + }); const channelId = channel.id; const data = userIds.map(userId => ({ @@ -240,8 +249,9 @@ export class ChatService { // 채널 데이터 양식화 const channel = { channelId: result.id, - title: result.name, + title: result.title, type: result.Channel_users.length > 2 ? 'group' : 'private', + thumnailUrl: result.thumbnail_url, users: result.Channel_users.map(res => ({ userId: res.user.id, email: res.user.email, From 33dd7d030277f72dd282ebb36822eedaf79551f9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 19 Jan 2025 18:35:03 +0900 Subject: [PATCH 185/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EA=B9=83?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EB=84=A4=EC=9E=84=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 35 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 59cca06..e6b9531 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1083,27 +1083,24 @@ export class UserService { throw new NotFoundException('깃허브 닉네임이 필요합니다.'); } - // 기존 ProgrammerData 확인 - const programmerData = await this.prisma.programmerData.findFirst({ + // upsert를 사용하여 데이터 생성 또는 업데이트 + const updatedData = await this.prisma.programmerData.upsert({ where: { user_id: userId }, + update: { + github_username: githubUsername, // 이미 존재하는 경우 업데이트 + }, + create: { + user_id: userId, + github_username: githubUsername, // 존재하지 않는 경우 새로 생성 + }, }); - if (!programmerData) { - // 프로그래머 데이터가 없는 경우 새로 생성 - const newData = await this.prisma.programmerData.create({ - data: { - user_id: userId, - github_username: githubUsername, - }, - }); - - return { - message: { - code: 200, - text: '깃허브 유저네임 등록에 성공했습니다.', - }, - githubUsername: newData.github_username, - }; - } + return { + message: { + code: 200, + text: '깃허브 유저네임이 성공적으로 저장되었습니다.', + }, + githubUsername: updatedData.github_username, + }; } } From e15190b317698ee777e797a9e2ab256467584f1a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 21:31:23 +0900 Subject: [PATCH 186/414] [Feat] Prisma db pull --- prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b3fdb29..f8cf835 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -314,9 +314,10 @@ model UserApplyProject { model Channel { id Int @id @default(autoincrement()) - name String @default("default_channel_name") active Boolean @default(true) created_at DateTime @default(now()) + thumbnail_url String? + title String @default("default_channel_title") Channel_users Channel_users[] Message Message[] } From 77dcf76cec4910a19b69dba7cffe25f48390de6f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 19 Jan 2025 21:43:55 +0900 Subject: [PATCH 187/414] =?UTF-8?q?[Feat]=20=EC=A1=B0=ED=9A=8C=EC=88=98=20?= =?UTF-8?q?=EC=A6=9D=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index db831f0..9df501a 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -23,6 +23,7 @@ export class FeedService { const result = await this.prisma.feedPost.findMany({ where: cursor ? { id: { gt: cursor } } : {}, + include: { Likes: { where: { user_id: userId }, @@ -116,6 +117,12 @@ export class FeedService { const post = await this.getPostObj(result); + // 조회수 증가 + await this.prisma.feedPost.update({ + where: { id: feedId }, + data: { view: { increment: 1 } }, + }); + return { post, message: { code: 200, message: '개별 피드를 정상적으로 조회했습니다.' }, From 1932596de88b073bfc0a9eb91fcfd0cce8edb395 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 00:40:46 +0900 Subject: [PATCH 188/414] =?UTF-8?q?[Refactor]=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 8 ++++---- src/chat/chat.service.ts | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index cba02c0..c0f9331 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -25,15 +25,15 @@ export class ChatController { ) { return await this.chatService.getMessages( req.user.user_id, - +channelId, - +limit, - +currentPage + channelId, + limit, + currentPage ); } @Get('channels/:id') @UseGuards(JwtAuthGuard) async getChannel(@Req() req: any, @Param('id') channelId: number) { - return await this.chatService.getChannel(req.user.user_id, +channelId); + return await this.chatService.getChannel(req.user.user_id, channelId); } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 607956c..5fc3a29 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -274,7 +274,12 @@ export class ChatService { } // 채널 메세지 조회 - async getMessages(userId, channelId, limit, currentPage) { + async getMessages( + userId: number, + channelId: number, + limit: number, + currentPage: number + ) { try { // 유저 아이디가 채널에 속해있는지 확인 const auth = await this.prisma.channel_users.findMany({ From 88fb7ba3622f446d8b252159cc072a0666e291d6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 01:05:02 +0900 Subject: [PATCH 189/414] =?UTF-8?q?[Refactor]=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 87e891c..27a72fa 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -56,12 +56,14 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 채널 id 조회 const channelId = await this.chatService.getChannelId(userId1, userId2); - // 채널 객체 + // 채널 객체 조회 const channelData = await this.chatService.getChannel(userId1, channelId); const channel = channelData.channel; - // 채널에 유저 참여 + // 채널에 유저 참여, 채널리스트에 해당 채널 추가 client.join(channelId.toString()); + client.emit('channelAdded', channel); + console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); // B 유저 온라인 여부 확인 @@ -76,7 +78,6 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { ); // 유저2의 채널 리스트에 해당 채널 추가 - client.emit('channelAdded', channel); user2Socket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); } else { @@ -88,6 +89,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { client.emit('channelCreated', channel); } + // 그룹 채팅방 생성 @SubscribeMessage('createGroup') async handleCreateGroup( @MessageBody() @@ -119,21 +121,22 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 온라인인 유저가 있을 때 if (targetSockets.length) { - // 모든 소켓 확인 + // 접속중인 모든 소켓 확인 const sockets = await this.server.fetchSockets(); + // 채널 멤버들의 소켓만 조회하게 필터링 const userSockets = sockets.filter(socket => targetSockets.includes(socket.id) ); - // 유저2의 채널 리스트에 해당 채널 추가 + // 각 멤버들의 채널 리스트에 해당 채널 추가 client.emit('channelAdded', channel); userSockets.forEach(socket => { socket.emit('channelAdded', channel); }); } else { // 오프라인 일때 - console.log('오프라인인 유저가 있습니다.'); + console.log('모든 유저가 오프라인 상태입니다.'); } // 클라이언트에 채널id 전달 @@ -155,7 +158,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channelData = await this.chatService.getChannel(userId, channelId); const { channel } = channelData; - // 클라이언트에 채널id 전달 + // 클라이언트에 채널 객체 전달 client.emit('channelJoined', channel); } From 0f1751366de0bcc02e231e94c2b507c3fe37bb2d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 01:48:05 +0900 Subject: [PATCH 190/414] [Feat] Prisma db push --- prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4c10814..0a7f276 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -314,7 +314,8 @@ model UserApplyProject { model Channel { id Int @id @default(autoincrement()) - title String @default("default_channel_title") + title String @default("default_channel_title") + type String thumbnail_url String? active Boolean @default(true) created_at DateTime @default(now()) From 4cf147df2dc03c8889684ecb89aa129bbc6f1da7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 01:48:23 +0900 Subject: [PATCH 191/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20type=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5fc3a29..6ed6515 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -29,7 +29,11 @@ export class ChatService { if (channel) return channel.channel_id; // 없다면 새로운 채널 생성 후 - const newChannel = await this.prisma.channel.create({}); + const newChannel = await this.prisma.channel.create({ + data: { + type: 'private', + }, + }); // 매핑 테이블에 데이터 저장 await this.prisma.channel_users.createMany({ @@ -59,6 +63,7 @@ export class ChatService { data: { title, thumbnail_url: thumnailUrl, + type: 'group', }, }); const channelId = channel.id; @@ -250,7 +255,7 @@ export class ChatService { const channel = { channelId: result.id, title: result.title, - type: result.Channel_users.length > 2 ? 'group' : 'private', + type: result.type, thumnailUrl: result.thumbnail_url, users: result.Channel_users.map(res => ({ userId: res.user.id, From 0ff1230eec48a0a55c4125d6528db1e8e62aca58 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 11:37:57 +0900 Subject: [PATCH 192/414] =?UTF-8?q?[Refactor]=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20=EC=B1=84=EB=84=90-?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 6ed6515..6a2de7c 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -5,8 +5,6 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class ChatService { constructor(private readonly prisma: PrismaService) {} - // ---- Socket ---- - // 채널 id를 리턴하는 로직 async getChannelId(userId1, userId2) { // 매핑 테이블에서 파라미터로 전달된 유저 아이디에 해당하는 데이터 찾기 @@ -35,22 +33,13 @@ export class ChatService { }, }); + const channelId = newChannel.id; + // 매핑 테이블에 데이터 저장 - await this.prisma.channel_users.createMany({ - data: [ - { - channel_id: newChannel.id, - user_id: userId1, - }, - { - channel_id: newChannel.id, - user_id: userId2, - }, - ], - }); + await this.createChannelUsers(channelId, [userId1, userId2]); // 채널 id 리턴 - return newChannel.id; + return channelId; } async getGroupChannelId( @@ -58,6 +47,16 @@ export class ChatService { title: string, thumnailUrl: string ) { + // 기존 채널 조회 + const exist = await this.prisma.channel.findMany({ + where: { title }, + select: { id: true }, + }); + + if (exist.length) { + return exist[0].id; + } + // 새로운 채널 생성 const channel = await this.prisma.channel.create({ data: { @@ -68,15 +67,20 @@ export class ChatService { }); const channelId = channel.id; + // 매핑 테이블에 저장 + await this.createChannelUsers(channelId, userIds); + + return channelId; + } + + // channle_users 테이블에 채널-유저 저장 + async createChannelUsers(channelId: number, userIds: number[]) { const data = userIds.map(userId => ({ channel_id: channelId, user_id: userId, })); - // 매핑 테이블에 저장 await this.prisma.channel_users.createMany({ data }); - - return channelId; } // 메세지 저장 @@ -178,10 +182,6 @@ export class ChatService { return channels; } - // 메세지 상태 업데이트 - - // ---- HTTP ---- - // 채널 개별 조회 async getChannel(userId: number, channelId: number) { try { From 55b944f8831a0288f28f71de0eaadb65818cbbea Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 11:46:32 +0900 Subject: [PATCH 193/414] =?UTF-8?q?[Refactor]=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 6a2de7c..d543a80 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -5,8 +5,9 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class ChatService { constructor(private readonly prisma: PrismaService) {} - // 채널 id를 리턴하는 로직 - async getChannelId(userId1, userId2) { + // 기존 채널 조회 or 새 채널 생성 + // 채널id 리턴 (개인 채팅방) + async getChannelId(userId1: number, userId2: number) { // 매핑 테이블에서 파라미터로 전달된 유저 아이디에 해당하는 데이터 찾기 const result = await this.prisma.channel_users.groupBy({ by: ['channel_id'], @@ -42,6 +43,8 @@ export class ChatService { return channelId; } + // 기존 채널 조회 or 새 채널 생성 + // 채널id 리턴 (그룹 채팅방) async getGroupChannelId( userIds: number[], title: string, @@ -84,7 +87,12 @@ export class ChatService { } // 메세지 저장 - async createMessage(type, channelId, userId, content) { + async createMessage( + type: string, + channelId: number, + userId: number, + content: string + ) { return await this.prisma.message.create({ data: { type, @@ -96,7 +104,7 @@ export class ChatService { } // 유저 정보 확인 - async getSenderProfile(userId) { + async getSenderProfile(userId: number) { const result = await this.prisma.user.findUnique({ where: { id: userId, @@ -126,7 +134,7 @@ export class ChatService { } // 온라인 유저 DB에 저장 - async addUserOnline(userId, clientId) { + async addUserOnline(userId: number, clientId: string) { await this.prisma.online_users.create({ data: { user_id: userId, @@ -211,7 +219,7 @@ export class ChatService { } // 채널 객체 리턴 로직 - async getChannleObj(channelId) { + async getChannleObj(channelId: number) { // 채널 데이터 조회 const result = await this.prisma.channel.findUnique({ where: { id: channelId }, From 62cfab6b67a88afb2a25c2fa9ea8f99225cee959 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 11:49:10 +0900 Subject: [PATCH 194/414] =?UTF-8?q?[Refactor]=20=EC=98=A8=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 15 ++++++++------- src/chat/chat.service.ts | 12 ------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 27a72fa..760fb42 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -67,18 +67,19 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); // B 유저 온라인 여부 확인 - const targetSocket = await this.chatService.getSocketId(userId2); + const targetSocket = await this.chatService.getSocketIds([userId2]); // 온라인 일때 - if (targetSocket) { + if (targetSocket.length) { // 유저2의 소켓 가져오기 + const target = targetSocket[0]; const sockets = await this.server.fetchSockets(); - const user2Socket = sockets.find( - socket => socket.id === targetSocket.toString() + const userSocket = sockets.find( + socket => socket.id === target.toString() ); // 유저2의 채널 리스트에 해당 채널 추가 - user2Socket.emit('channelAdded', channel); + userSocket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); } else { // 오프라인 일때 @@ -111,8 +112,9 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channelData = await this.chatService.getChannel(userId, channelId); const channel = channelData.channel; - // 채널에 마스터 유저 참여 + // 채널에 마스터 유저 참여 & 채널리스트에 추가 client.join(channelId.toString()); + client.emit('channelAdded', channel); console.log(`client ${client.data.userId} ${channelId}번 채팅방 입장`); // 나머지 유저 온라인 여부 확인 @@ -130,7 +132,6 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { ); // 각 멤버들의 채널 리스트에 해당 채널 추가 - client.emit('channelAdded', channel); userSockets.forEach(socket => { socket.emit('channelAdded', channel); }); diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index d543a80..ff3c516 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -153,18 +153,6 @@ export class ChatService { } // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 - async getSocketId(userId: number) { - const socketId = await this.prisma.online_users.findMany({ - where: { - user_id: userId, - }, - select: { - client_id: true, - }, - }); - return socketId[0]?.client_id; - } - async getSocketIds(Ids: number[]) { const socketIds = await this.prisma.online_users.findMany({ where: { user_id: { in: Ids } }, From 34dcd9c136987fe21693038986083ca4f92ee02f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 16:10:35 +0900 Subject: [PATCH 195/414] =?UTF-8?q?[Fix]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 9df501a..f169659 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -202,8 +202,8 @@ export class FeedService { userRole: c.user.role.name, userProfileUrl: c.user.profile_url, comment: c.content, + likeCount: c.FeedCommentLikes.length, createdAt: c.created_at, - likes: c.FeedCommentLikes.length, })); return { From 9ddfce1e34371b970f94124136842130e26c28ea Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 17:16:05 +0900 Subject: [PATCH 196/414] =?UTF-8?q?[Fix]=20DB=20Scheme=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a7f276..c865858 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -62,6 +62,7 @@ model MyPageProject { user_id Int title String description String + projectProfileUrl String? created_at DateTime @default(now()) updated_at DateTime @updatedAt user User @relation(fields: [user_id], references: [id]) From 0318df659324c5a2992ac45ab8377f6c12a17e91 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 17:16:46 +0900 Subject: [PATCH 197/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=EC=8B=9C=20formdata=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 9357f60..bf6e2aa 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -52,12 +52,16 @@ import { GetUserConnectionHubProjectsDocs, } from './docs/user.docs'; import { ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { S3Service } from '@src/s3/s3.service'; @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('users') export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private readonly s3Service: S3Service + ) {} @Get(':userId') @GetUserProfileDocs.ApiOperation @@ -96,12 +100,30 @@ export class UserController { } @Post('projects') + @UseInterceptors(FileInterceptor('file')) @AddProjectDocs.ApiOperation @AddProjectDocs.ApiBody @AddProjectDocs.ApiResponse - async addProject(@Req() req, @Body() projectData: any) { + async addProject( + @Req() req, + @UploadedFile() file: Express.Multer.File, + @Body() body: any + ) { const userId = req.user?.user_id; - return this.userService.addProject(userId, projectData); + let imageUrl = null; + if (file) { + imageUrl = await this.s3Service.uploadImage( + userId, + file.buffer, + file.mimetype.split('/')[1], // 파일 확장자 추출 + 'pad_projects/images' // S3 저장 경로 설정 + ); + } + const projectData = { + ...body, + links: JSON.parse(body.links), // 문자열을 객체로 변환 + }; + return this.userService.addProject(userId, projectData, imageUrl); } @Put('projects/:projectId') From f213e3bfd61fd51f8014b82643944520abd913a4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 17:33:13 +0900 Subject: [PATCH 198/414] =?UTF-8?q?[Feat]=20getFeedsQuery=20Dto=20?= =?UTF-8?q?=EC=97=90=20tags=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/getFeedsQuery.dto.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/feed/dto/getFeedsQuery.dto.ts b/src/feed/dto/getFeedsQuery.dto.ts index 7be8310..840d76f 100644 --- a/src/feed/dto/getFeedsQuery.dto.ts +++ b/src/feed/dto/getFeedsQuery.dto.ts @@ -19,4 +19,7 @@ export class GetFeedsQueryDto { @IsInt() @Type(() => Number) cursor?: number; + + @IsOptional() + tags?: string; } From 8fa686ef86f9ceb387f2cb53eccf3322d971a76f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 17:35:15 +0900 Subject: [PATCH 199/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=97=90=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=EB=B3=84=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index f169659..427c10f 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -17,12 +17,32 @@ export class FeedService { async getAllFeeds(user, queryDto: GetFeedsQueryDto) { try { const userId = user ? user.user_id : 0; - const { latest = false, limit = 10, cursor = 0 } = queryDto; + const { latest = false, limit = 10, cursor = 0, tags } = queryDto; + // 정렬 기준 const orderKey = latest ? 'created_at' : 'like_count'; + // 쿼리로 전달받은 태그 + const tagIds = tags ? tags.split(',').map(id => parseInt(id)) : []; + + // 태그를 포함하고 있는 피드 아이디 조회 + const feedTagIds = tagIds?.length + ? ( + await this.prisma.feedPostTag.groupBy({ + by: ['post_id'], + where: { tag_id: { in: tagIds } }, + having: { + post_id: { _count: { equals: tagIds.length } }, // 태그 개수 일치하는 게시글만 조회 + }, + }) + ).map(p => p.post_id) + : null; + const result = await this.prisma.feedPost.findMany({ - where: cursor ? { id: { gt: cursor } } : {}, + where: { + ...(cursor ? { id: { gt: cursor } } : {}), // cursor 조건 추가 (옵셔널) + ...(feedTagIds ? { id: { in: feedTagIds } } : {}), // 태그 조건 추가 (옵셔널) + }, include: { Likes: { From c97dbbbd447f313a5b39c6fcc3b2bab61174fca1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 20 Jan 2025 18:02:51 +0900 Subject: [PATCH 200/414] =?UTF-8?q?[Refactor]=20Dto=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/dto/comment.dto.ts | 4 ++-- src/feed/dto/feed.dto.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/feed/dto/comment.dto.ts b/src/feed/dto/comment.dto.ts index 5b5ca54..a6b1640 100644 --- a/src/feed/dto/comment.dto.ts +++ b/src/feed/dto/comment.dto.ts @@ -1,7 +1,7 @@ import { IsString, IsNotEmpty } from 'class-validator'; export class CommentDto { - @IsString() - @IsNotEmpty() + @IsString({ message: '내용은 문자열이어야 합니다.' }) + @IsNotEmpty({ message: '내용을 입력해주세요' }) content: string; } diff --git a/src/feed/dto/feed.dto.ts b/src/feed/dto/feed.dto.ts index 076bfd0..d4ce7bd 100644 --- a/src/feed/dto/feed.dto.ts +++ b/src/feed/dto/feed.dto.ts @@ -1,16 +1,16 @@ import { IsString, IsArray, ArrayNotEmpty, IsNotEmpty } from 'class-validator'; export class FeedDto { - @IsString() - @IsNotEmpty() + @IsString({ message: '제목은 문자열이어야 합니다.' }) + @IsNotEmpty({ message: '제목을 입력해주세요.' }) title: string; - @IsArray() - @ArrayNotEmpty() + @IsArray({ message: '태그는 배열이어야 합니다.' }) + @ArrayNotEmpty({ message: '태그를 하나 이상 입력해주세요' }) @IsString({ each: true }) tags: string[]; - @IsString() - @IsNotEmpty() + @IsString({ message: '내용은 문자열이어야 합니다.' }) + @IsNotEmpty({ message: '내용을 입력해주세요' }) content: string; } From 7102bb81f4b6e100465ba429bce93b3b79369deb Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 21:58:31 +0900 Subject: [PATCH 201/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=EC=8B=9C=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=ED=8F=BC=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 30 +++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index bf6e2aa..8925e31 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -6,7 +6,6 @@ import { Patch, Post, Req, - Res, UseGuards, UseInterceptors, UploadedFile, @@ -16,7 +15,6 @@ import { HttpException, HttpStatus, Query, - HttpCode, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; @@ -127,6 +125,7 @@ export class UserController { } @Put('projects/:projectId') + @UseInterceptors(FileInterceptor('file')) // 파일 처리 인터셉터 추가 @UpdateProjectDocs.ApiOperation @UpdateProjectDocs.ApiParam @UpdateProjectDocs.ApiBody @@ -134,11 +133,34 @@ export class UserController { async updateProject( @Req() req, @Param('projectId') projectId: string, - @Body() projectData: any + @UploadedFile() file: Express.Multer.File, // 업로드된 파일 처리 + @Body() body: any ) { const userId = req.user?.user_id; const numProjectId = parseInt(projectId, 10); - return this.userService.updateProject(userId, numProjectId, projectData); + + // 이미지 업로드 처리 + let imageUrl = null; + if (file) { + imageUrl = await this.s3Service.uploadImage( + userId, + file.buffer, + file.mimetype.split('/')[1], // 파일 확장자 추출 + 'pad_projects/images' // S3 저장 경로 설정 + ); + } + // body의 links 필드 처리 (JSON 문자열을 객체로 변환) + const projectData = { + ...body, + links: body.links ? JSON.parse(body.links) : [], // links가 있으면 파싱, 없으면 빈 배열 + }; + + return this.userService.updateProject( + userId, + numProjectId, + projectData, + imageUrl + ); } @Delete('projects/:projectId') From 582ed4e2a521222004722a2e65ac08ce1608dda6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 21:59:20 +0900 Subject: [PATCH 202/414] =?UTF-8?q?[Fix]=20Swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 107 ++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 24 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index f0efbe1..e3c8e33 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -84,6 +84,8 @@ export const GetUserProfileDocs = { works: [ { title: 'My Project', + myPageProjectId: 1, + projectProfileUrl: 'projectProfileUrl', description: 'Project description here.', links: [ { @@ -92,6 +94,18 @@ export const GetUserProfileDocs = { }, ], }, + { + title: 'My Project2', + myPageProjectId: 2, + projectProfileUrl: 'projectProfileUrl2', + description: 'Project description here2.', + links: [ + { + type: 'Github', + url: 'https://github.com/johndoe/myproject2', + }, + ], + }, ], followerCount: 12, followingCount: 34, @@ -143,20 +157,39 @@ export const GetUserProfileHeaderDocs = { export const AddProjectDocs = { ApiOperation: ApiOperation({ summary: '프로젝트 추가', - description: '사용자의 마이페이지에 새 프로젝트를 추가합니다.', + description: + '사용자의 마이페이지에 새 프로젝트를 추가합니다. 프로젝트 이미지 업로드가 가능합니다.', }), + ApiConsumes: ApiConsumes('multipart/form-data'), ApiBody: ApiBody({ description: - '추가할 프로젝트 데이터 typeId : 1 = Github, 2 = Web, 3 = IOS, 4 = Android', + '추가할 프로젝트 데이터. `typeId` : 1 = Github, 2 = Web, 3 = IOS, 4 = Android', schema: { - example: { - title: 'My Project', - description: 'This is a description of my project.', - links: [ - { url: 'https://github.com/myproject', typeId: 1 }, - { url: 'https://myproject.com', typeId: 2 }, - ], + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: '프로젝트 프로필 이미지 파일', + }, + title: { + type: 'string', + description: '프로젝트 제목', + }, + description: { + type: 'string', + description: '프로젝트 설명', + }, + links: { + type: 'string', + description: '프로젝트 링크 배열 (JSON 문자열)', + example: JSON.stringify([ + { url: 'https://github.com/myproject', typeId: 1 }, + { url: 'https://myproject.com', typeId: 2 }, + ]), + }, }, + required: ['title', 'description', 'links'], }, }), ApiResponse: ApiResponse({ @@ -171,9 +204,10 @@ export const AddProjectDocs = { myPageProjectId: 1, title: 'My Project', description: 'This is a description of my project.', + projectProfileUrl: 'https://s3.example.com/path/to/image.png', links: [ - { id: 1, url: 'https://github.com/myproject', type: 'Github' }, - { id: 2, url: 'https://myproject.com', type: 'Website' }, + { url: 'https://github.com/myproject', type: 'Github' }, + { url: 'https://myproject.com', type: 'Web' }, ], }, }, @@ -183,7 +217,8 @@ export const AddProjectDocs = { export const UpdateProjectDocs = { ApiOperation: ApiOperation({ summary: '프로젝트 수정', - description: '사용자의 특정 프로젝트를 수정합니다.', + description: + '사용자의 특정 프로젝트를 수정합니다. 프로젝트 이미지도 수정 가능합니다.', }), ApiParam: ApiParam({ name: 'projectId', @@ -191,17 +226,35 @@ export const UpdateProjectDocs = { description: '수정할 프로젝트의 ID', type: 'string', }), + ApiConsumes: ApiConsumes('multipart/form-data'), ApiBody: ApiBody({ description: '수정할 프로젝트 데이터', schema: { - example: { - title: 'Updated Project', - description: 'This is an updated description.', - links: [ - { url: 'https://github.com/updatedproject', typeId: 1 }, - { url: 'https://updatedproject.com', typeId: 2 }, - ], + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: '새로운 프로젝트 프로필 이미지 파일', + }, + title: { + type: 'string', + description: '수정된 프로젝트 제목', + }, + description: { + type: 'string', + description: '수정된 프로젝트 설명', + }, + links: { + type: 'string', + description: '수정된 프로젝트 링크 배열 (JSON 문자열)', + example: JSON.stringify([ + { url: 'https://github.com/updatedproject', typeId: 1 }, + { url: 'https://updatedproject.com', typeId: 2 }, + ]), + }, }, + required: ['title', 'description', 'links'], }, }), ApiResponse: ApiResponse({ @@ -211,14 +264,15 @@ export const UpdateProjectDocs = { example: { message: { code: 200, - text: '프로젝트 수정에 성공했습니다', + text: '마이페이지에 프로젝트 수정에 성공했습니다', }, myPageProjectId: 1, title: 'Updated Project', description: 'This is an updated description.', + projectProfileUrl: 'https://s3.example.com/path/to/updated_image.png', links: [ - { id: 1, url: 'https://github.com/updatedproject', type: 'Github' }, - { id: 2, url: 'https://updatedproject.com', type: 'Website' }, + { url: 'https://github.com/updatedproject', type: 'Github' }, + { url: 'https://updatedproject.com', type: 'Website' }, ], }, }, @@ -245,6 +299,7 @@ export const DeleteProjectDocs = { code: 200, text: '프로젝트 삭제에 성공했습니다', }, + projectId: 1, }, }, }), @@ -334,6 +389,7 @@ export const DeleteWorkDocs = { code: 200, text: '작업물이 성공적으로 삭제되었습니다.', }, + musicId: 1, }, }, }), @@ -361,6 +417,7 @@ export const UpdateGithubUsernameDocs = { code: 200, text: '깃허브 유저네임 등록에 성공했습니다.', }, + githubUsername: 'SSomae', }, }, }), @@ -385,8 +442,8 @@ export const GetUserSettingDocs = { introduce: 'User Introduce', status: '구인 중', links: [ - { id: 1, url: 'https://github.com/Ss0Mae' }, - { id: 2, url: 'https://www.google.com' }, + { linkId: 1, url: 'https://github.com/Ss0Mae' }, + { linkId: 2, url: 'https://www.google.com' }, ], skills: ['TypeScript', 'Nest.js'], jobDetail: 'IT / 백엔드개발자', @@ -727,6 +784,7 @@ export const GetUserResumeDocs = { text: '사용자 이력서 조회에 성공했습니다.', }, userId: 1, + resumeId: 2, title: 'Backend Developer', jobDetail: '개발자 / 백엔드 엔지니어', skills: ['Node.js', 'TypeScript', 'GraphQL'], @@ -836,6 +894,7 @@ export const DeleteUserResumeDocs = { code: 200, text: '사용자 이력서 삭제에 성공했습니다.', }, + resumeId: 1, }, }, }), From ea48a431167cccd496246d76dc6a86dd90937c63 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 21:59:57 +0900 Subject: [PATCH 203/414] =?UTF-8?q?[Feat]=20API=EB=93=A4=20=EC=82=AC?= =?UTF-8?q?=EC=86=8C=ED=95=9C=20=EC=9D=91=EB=8B=B5=EA=B0=92=EB=93=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 41 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index e6b9531..d95efd2 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -66,7 +66,10 @@ export class UserService { // 사용자 직업군에 따라 데이터를 동적으로 추가 if (user.role.name === 'Artist') { Object.assign(response, { - works: user.ArtistData.map(work => work.music_url), // 단순 URL 배열로 변환 + works: user.ArtistData.map(work => ({ + musicUrl: work.music_url, + musicId: work.id, + })), // 단순 URL 배열로 변환 }); } else if ( user.role.name === 'Programmer' || @@ -78,6 +81,8 @@ export class UserService { ? user.MyPageProject.map(project => ({ title: project.title, description: project.description, + myPageProjectId: project.id, + projectProfileUrl: project.projectProfileUrl, links: project.ProjectLinks.map(link => ({ type: link.type.name, url: link.url, @@ -189,7 +194,7 @@ export class UserService { }; } - async addProject(userId: number, projectData: any) { + async addProject(userId: number, projectData: any, imageUrl: string | null) { const { title, description, links } = projectData; // 작업물 추가 @@ -198,6 +203,7 @@ export class UserService { user_id: userId, title, description, + projectProfileUrl: imageUrl, ProjectLinks: { create: links.map(link => ({ url: link.url, @@ -220,15 +226,20 @@ export class UserService { myPageProjectId: newProject.id, title: newProject.title, description: newProject.description, + projectProfileUrl: newProject.projectProfileUrl, links: newProject.ProjectLinks.map(link => ({ - id: link.id, url: link.url, type: link.type.name, })), }; } - async updateProject(userId: number, projectId: number, projectData: any) { + async updateProject( + userId: number, + projectId: number, + projectData: any, + imageUrl: string | null + ) { const { title, description, links } = projectData; // 기존 프로젝트 확인 @@ -249,6 +260,7 @@ export class UserService { data: { title, description, + projectProfileUrl: imageUrl, ProjectLinks: { deleteMany: {}, // 기존 링크 삭제 create: links.map(link => ({ @@ -272,8 +284,8 @@ export class UserService { myPageProjectId: updatedProject.id, title: updatedProject.title, description: updatedProject.description, + projectProfileUrl: updatedProject.projectProfileUrl, links: updatedProject.ProjectLinks.map(link => ({ - id: link.id, url: link.url, type: link.type.name, })), @@ -343,7 +355,7 @@ export class UserService { introduce: user.introduce, status: user.status?.name, links: user.UserLinks.map(link => ({ - id: link.id, + linkId: link.id, url: link.link, // 링크 정보만 반환 })), skills: user.UserSkills.map(skill => skill.skill.name), // 기술 스택 @@ -710,6 +722,7 @@ export class UserService { title: true, portfolio_url: true, detail: true, + id: true, user: { select: { id: true, @@ -740,6 +753,7 @@ export class UserService { text: '사용자 이력서 조회에 성공했습니다.', }, userId: resume.user.id, + resumeId: resume.id, title: resume.title, jobDetail: resume.user.job_detail, // 직무 상세 skills: resume.user.UserSkills.map(userSkill => userSkill.skill.name), // 기술 스택 이름 리스트 @@ -754,12 +768,23 @@ export class UserService { userId: number, data: { title: string; portfolioUrl?: string; detail: string } ) { + // 유저가 이미 이력서를 가지고 있는지 확인 + const existingResume = await this.prisma.resume.findFirst({ + where: { user_id: userId }, + }); + + if (existingResume) { + // 이미 이력서가 있는 경우 예외를 던짐 + throw new Error('해당 유저는 이미 이력서를 가지고 있습니다.'); + } + + // 이력서 생성 const newResume = await this.prisma.resume.create({ data: { user_id: userId, title: data.title, portfolio_url: data.portfolioUrl, - detail: data.detail, // detail 정보를 introduce 필드에 저장 + detail: data.detail, }, }); @@ -847,6 +872,7 @@ export class UserService { code: 200, text: '사용자 이력서 삭제에 성공했습니다.', }, + resumeId, }; } @@ -1075,6 +1101,7 @@ export class UserService { code: 200, text: '아티스트 작업물 삭제에 성공했습니다.', }, + musicId: workId, }; } From 17cf75a4764aca9e8eda1da5d92c4a60f3747f56 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 20 Jan 2025 22:26:44 +0900 Subject: [PATCH 204/414] =?UTF-8?q?[Fix]=20DB=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c865858..5c218dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,15 +58,15 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) - user_id Int - title String - description String + id Int @id @default(autoincrement()) + user_id Int + title String + description String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt projectProfileUrl String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) - ProjectLinks MyPageProjectLink[] + user User @relation(fields: [user_id], references: [id]) + ProjectLinks MyPageProjectLink[] @@index([user_id]) } From e2c67e1350ef55a623c538a47ac5cbd82554ba72 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 13:13:22 +0900 Subject: [PATCH 205/414] [Feat] Prisma db pull --- prisma/schema.prisma | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a7f276..5c218dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,14 +58,15 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) - user_id Int - title String - description String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) - ProjectLinks MyPageProjectLink[] + id Int @id @default(autoincrement()) + user_id Int + title String + description String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + projectProfileUrl String? + user User @relation(fields: [user_id], references: [id]) + ProjectLinks MyPageProjectLink[] @@index([user_id]) } From 24827585cda6d40b0a8e70a91a2e374d4e39228d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 13:17:48 +0900 Subject: [PATCH 206/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a7f276..5c218dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,14 +58,15 @@ model Role { } model MyPageProject { - id Int @id @default(autoincrement()) - user_id Int - title String - description String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - user User @relation(fields: [user_id], references: [id]) - ProjectLinks MyPageProjectLink[] + id Int @id @default(autoincrement()) + user_id Int + title String + description String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + projectProfileUrl String? + user User @relation(fields: [user_id], references: [id]) + ProjectLinks MyPageProjectLink[] @@index([user_id]) } From d2ec8d638dc0fe44bafd2e57c8dc9ea1a95d705c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 21 Jan 2025 14:23:45 +0900 Subject: [PATCH 207/414] =?UTF-8?q?[Fix]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 60 +++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index d95efd2..62803ec 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -248,26 +248,56 @@ export class UserService { id: projectId, user_id: userId, }, + include: { + ProjectLinks: true, // 기존 링크를 가져옴 + }, }); if (!existingProject) { throw new NotFoundException('작업물을 찾을 수 없습니다.'); } - // 프로젝트 업데이트 + // 파일이 없는 경우 기존의 projectProfileUrl 유지 + const finalImageUrl = imageUrl || existingProject.projectProfileUrl; + + // 링크 업데이트 및 추가 + if (links) { + for (const link of links) { + const existingLink = await this.prisma.myPageProjectLink.findFirst({ + where: { + project_id: projectId, + type_id: link.typeId, + }, + }); + + if (existingLink) { + // 기존 링크가 있으면 업데이트 + await this.prisma.myPageProjectLink.update({ + where: { id: existingLink.id }, + data: { + url: link.url, + }, + }); + } else { + // 기존 링크가 없으면 새로 생성 + await this.prisma.myPageProjectLink.create({ + data: { + project_id: projectId, + url: link.url, + type_id: link.typeId, + }, + }); + } + } + } + + // 프로젝트 자체 업데이트 const updatedProject = await this.prisma.myPageProject.update({ where: { id: projectId }, data: { - title, - description, - projectProfileUrl: imageUrl, - ProjectLinks: { - deleteMany: {}, // 기존 링크 삭제 - create: links.map(link => ({ - url: link.url, - type_id: link.typeId, // LinkType의 ID를 사용 - })), - }, + title: title || existingProject.title, // title이 없으면 기존 값 유지 + description: description || existingProject.description, // description이 없으면 기존 값 유지 + projectProfileUrl: finalImageUrl, // 이미지 URL 업데이트 }, include: { ProjectLinks: { @@ -276,6 +306,12 @@ export class UserService { }, }); + // 최종적으로 모든 링크를 가져옴 + const allLinks = await this.prisma.myPageProjectLink.findMany({ + where: { project_id: projectId }, + include: { type: true }, // type 필드 포함 + }); + return { message: { code: 200, @@ -285,7 +321,7 @@ export class UserService { title: updatedProject.title, description: updatedProject.description, projectProfileUrl: updatedProject.projectProfileUrl, - links: updatedProject.ProjectLinks.map(link => ({ + links: allLinks.map(link => ({ url: link.url, type: link.type.name, })), From 6d75d2b3683921bc4566ceb720c252531230539c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 15:38:14 +0900 Subject: [PATCH 208/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EA=B8=B0=EC=B4=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 17 ++++++++++++++++- src/chat/chat.service.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index c0f9331..a385011 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,4 +1,13 @@ -import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @@ -36,4 +45,10 @@ export class ChatController { async getChannel(@Req() req: any, @Param('id') channelId: number) { return await this.chatService.getChannel(req.user.user_id, channelId); } + + // 메세지 검색 + @Post(':id/search') + async searchMessage(@Body() body, @Param('id') id: number) { + return await this.chatService.searchMessage(id, body.keyword); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index ff3c516..d857766 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -349,4 +349,36 @@ export class ChatService { return err.message; } } + + // 메세지 검색 + async searchMessage(channelId, keyword) { + // 키워드에 해당하는 메세지id 검색 + const keywordMessage = await this.prisma.message.findFirst({ + orderBy: { id: 'desc' }, + where: { channel_id: channelId, content: { contains: keyword } }, + select: { id: true }, + }); + + const messageId = keywordMessage.id; + + // 키워드 메세지 이전 15개 + const previous = await this.prisma.message.findMany({ + orderBy: { id: 'desc' }, + where: { channel_id: channelId, id: { lt: messageId } }, + select: { id: true }, + take: 15, + }); + const prevIds = previous.reverse().map(pre => pre.id); + + // 키워드 메세지 이후 15개 + const sub = await this.prisma.message.findMany({ + orderBy: { id: 'asc' }, + where: { channel_id: channelId, id: { gt: messageId } }, + select: { id: true }, + take: 15, + }); + const subIds = sub.map(sub => sub.id); + + return { prevIds, messageId, subIds }; + } } From ee3df1aff99c8177c60146787a3dc7aa1d6aea00 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 15:48:38 +0900 Subject: [PATCH 209/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=EB=A1=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index d857766..a264381 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -379,6 +379,35 @@ export class ChatService { }); const subIds = sub.map(sub => sub.id); - return { prevIds, messageId, subIds }; + const pm = await this.getMessageById(prevIds); + const m = await this.getMessageById([messageId]); + const sm = await this.getMessageById(subIds); + return { pm, m, sm }; + } + + // 메세지 아이디로 메세지 조회 + async getMessageById(ids) { + const result = await this.prisma.message.findMany({ + where: { id: { in: ids } }, + include: { + user: { + select: { + id: true, + email: true, + name: true, + nickname: true, + role: true, + profile_url: true, + auth_provider: true, + }, + }, + }, + }); + + return result; } } + +// 키워드 검색 -> 키워드 id, 이전/후 id 15개 -> 31개 +// 31개의 id들을 양식화 해서 전달 +// 데이터 조회 및 양식화에 필요한 데이터 (채널 아이디/ 유저 아이디/ 메세지 아이디) From 33b26770588d355316ff4d1049cced7341d00910 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 15:55:02 +0900 Subject: [PATCH 210/414] =?UTF-8?q?[Refactor]=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91=EC=8B=9D?= =?UTF-8?q?=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 41 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index a264381..57dfd91 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -321,22 +321,7 @@ export class ChatService { }); // 메세지 데이터 양식화 - const data = result.map(msg => ({ - messageId: msg.id, - type: msg.type, - content: msg.content, - channelId: msg.channel_id, - date: msg.created_at, - user: { - userId: msg.user.id, - email: msg.user.email, - name: msg.user.name, - nickname: msg.user.nickname, - profileUrl: msg.user.profile_url, - authProvider: msg.user.auth_provider, - roleId: msg.user.role.id, - }, - })); + const data = await this.getMessageObj(result); const message = { code: 200, @@ -404,7 +389,29 @@ export class ChatService { }, }); - return result; + const obj = await this.getMessageObj(result); + return obj; + } + + // 메세지 데이터 양식화 + async getMessageObj(messages) { + const data = messages.map(msg => ({ + messageId: msg.id, + type: msg.type, + content: msg.content, + channelId: msg.channel_id, + date: msg.created_at, + user: { + userId: msg.user.id, + email: msg.user.email, + name: msg.user.name, + nickname: msg.user.nickname, + profileUrl: msg.user.profile_url, + authProvider: msg.user.auth_provider, + roleId: msg.user.role.id, + }, + })); + return data; } } From 23bcf7c92171fca2064d4607982e36cbfd0de5fa Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 16:21:01 +0900 Subject: [PATCH 211/414] =?UTF-8?q?[Refactor]=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 82 +++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 57dfd91..80a79b5 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -337,37 +337,61 @@ export class ChatService { // 메세지 검색 async searchMessage(channelId, keyword) { - // 키워드에 해당하는 메세지id 검색 - const keywordMessage = await this.prisma.message.findFirst({ - orderBy: { id: 'desc' }, - where: { channel_id: channelId, content: { contains: keyword } }, - select: { id: true }, - }); + try { + // 키워드에 해당하는 메세지id 검색 + const keywordMessage = await this.prisma.message.findFirst({ + orderBy: { id: 'desc' }, + where: { channel_id: channelId, content: { contains: keyword } }, + select: { id: true }, + }); - const messageId = keywordMessage.id; + if (!keywordMessage) { + const message = { code: 404, text: '메세지를 찾을 수 없습니다' }; + return { message }; + } - // 키워드 메세지 이전 15개 - const previous = await this.prisma.message.findMany({ - orderBy: { id: 'desc' }, - where: { channel_id: channelId, id: { lt: messageId } }, - select: { id: true }, - take: 15, - }); - const prevIds = previous.reverse().map(pre => pre.id); + // 키워드 메세지 커서 설정 + const search = keywordMessage.id; - // 키워드 메세지 이후 15개 - const sub = await this.prisma.message.findMany({ - orderBy: { id: 'asc' }, - where: { channel_id: channelId, id: { gt: messageId } }, - select: { id: true }, - take: 15, - }); - const subIds = sub.map(sub => sub.id); + // 커서 기준 이전/후 메세지 15개 아이디 조회 + const [forward, backward] = await this.prisma.$transaction([ + this.prisma.message.findMany({ + orderBy: { id: 'desc' }, + where: { channel_id: channelId, id: { lt: search } }, + select: { id: true }, + take: 15, + }), + this.prisma.message.findMany({ + orderBy: { id: 'asc' }, + where: { channel_id: channelId, id: { gt: search } }, + select: { id: true }, + take: 15, + }), + ]); + + const forwardIds = forward.reverse().map(pre => pre.id); + const backwordIds = backward.map(sub => sub.id); + const ids = [...forwardIds, search, ...backwordIds]; + + const messages = await this.getMessageById(ids); + // 무한 스크롤용 커서 데이터 + const cursor = { + // 이후의 데이터 요청 + prev: forwardIds[0], + next: backwordIds[backwordIds.length - 1], + search, + }; - const pm = await this.getMessageById(prevIds); - const m = await this.getMessageById([messageId]); - const sm = await this.getMessageById(subIds); - return { pm, m, sm }; + const message = { + code: 200, + message: '데이터 패칭 성공', + }; + + return { messages, cursor, message }; + } catch (err) { + console.log(err); + return err; + } } // 메세지 아이디로 메세지 조회 @@ -414,7 +438,3 @@ export class ChatService { return data; } } - -// 키워드 검색 -> 키워드 id, 이전/후 id 15개 -> 31개 -// 31개의 id들을 양식화 해서 전달 -// 데이터 조회 및 양식화에 필요한 데이터 (채널 아이디/ 유저 아이디/ 메세지 아이디) From ebd1f38313652d4f3a4b78fe314cd6e408eb299c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 21 Jan 2025 18:47:29 +0900 Subject: [PATCH 212/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EC=97=90=20cursor,=20direction=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 80a79b5..5bcb39e 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -275,12 +275,7 @@ export class ChatService { } // 채널 메세지 조회 - async getMessages( - userId: number, - channelId: number, - limit: number, - currentPage: number - ) { + async getMessages(userId: number, channelId: number, limit: number) { try { // 유저 아이디가 채널에 속해있는지 확인 const auth = await this.prisma.channel_users.findMany({ @@ -317,7 +312,6 @@ export class ChatService { id: 'desc', }, take: limit, - skip: (currentPage - 1) * limit, }); // 메세지 데이터 양식화 @@ -336,12 +330,23 @@ export class ChatService { } // 메세지 검색 - async searchMessage(channelId, keyword) { + async searchMessage( + channelId: number, + keyword: string, + cursor: number, + direction: string + ) { try { // 키워드에 해당하는 메세지id 검색 const keywordMessage = await this.prisma.message.findFirst({ orderBy: { id: 'desc' }, - where: { channel_id: channelId, content: { contains: keyword } }, + where: { + channel_id: channelId, + // forward(스크롤 다운)일 시 cursor보다 id 높은 값(최신) + // backward(스크롤 업)일 시 cursor보다 id 낮은 값(오래된) + id: direction == 'forward' ? { gt: cursor } : { lt: cursor }, + content: { contains: keyword }, + }, select: { id: true }, }); @@ -375,10 +380,12 @@ export class ChatService { const messages = await this.getMessageById(ids); // 무한 스크롤용 커서 데이터 - const cursor = { - // 이후의 데이터 요청 + const cursors = { + // backward 무한스크롤 요청 커서 prev: forwardIds[0], + // forward 무한스크롤 요청 커서 next: backwordIds[backwordIds.length - 1], + // 검색 메세지 아이디 커서 search, }; @@ -387,9 +394,8 @@ export class ChatService { message: '데이터 패칭 성공', }; - return { messages, cursor, message }; + return { messages, cursors, message }; } catch (err) { - console.log(err); return err; } } From c7604f4cc8271b4822b19aa57b11e002a1bfba48 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 15:49:12 +0900 Subject: [PATCH 213/414] =?UTF-8?q?[Fix]=20=EC=9C=A0=EC=A0=80=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=9E=91=EC=84=B1=ED=95=9C=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 62803ec..31ad45f 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -916,11 +916,12 @@ export class UserService { // Offset 계산 const offset = (page - 1) * limit; - // 피드 조회 + // 특정 유저의 피드 조회 const feeds = await this.prisma.feedPost.findMany({ + where: { user_id: userId }, // 특정 유저의 글만 가져옴 skip: offset, take: limit, - orderBy: { created_at: 'desc' }, // 최신 순 정렬 + orderBy: { created_at: 'desc' }, include: { user: { select: { @@ -931,14 +932,16 @@ export class UserService { }, Tags: { include: { - tag: true, // 태그 이름 가져오기 + tag: true, }, }, }, }); // 총 피드 개수 (페이지네이션 용) - const totalCount = await this.prisma.feedPost.count(); + const totalCount = await this.prisma.feedPost.count({ + where: { user_id: userId }, // 특정 유저의 글만 카운트 + }); // 반환 데이터 구성 return { @@ -960,7 +963,7 @@ export class UserService { nickname: feed.user.nickname, profileUrl: feed.user.profile_url, }, - tags: feed.Tags.map(tag => tag.tag.name), // 태그 리스트 + tags: feed.Tags.map(tag => tag.tag.name), })), totalCount, currentPage: page, From 021020ef785a58237fa1dddde2cabb75e1bcfb6c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 15:49:23 +0900 Subject: [PATCH 214/414] =?UTF-8?q?[Fix]=20db=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5c218dd..c98bfa9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -227,8 +227,8 @@ model ProjectPost { id Int @id @default(autoincrement()) user_id Int title String - content String - thumbnail_url String + content String @db.Text + thumbnail_url String? role Int unit String start_date DateTime @@ -236,8 +236,8 @@ model ProjectPost { work_type_id Int recruiting Boolean applicant_count Int - view Int - saved_count Int + view Int @default(0) + saved_count Int @default(0) created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) @@ -306,9 +306,11 @@ model UserApplyProject { id Int @id @default(autoincrement()) user_id Int post_id Int + status String @default("Pending") // "Accepted", "Rejected", "Pending" post ProjectPost @relation(fields: [post_id], references: [id]) user User @relation(fields: [user_id], references: [id]) + @@unique([user_id, post_id]) // 중복 지원 방지 @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From daf57df25704143993dfbbe04fce8dbc31da5703 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 16:02:26 +0900 Subject: [PATCH 215/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 25 +++++++--------------- src/chat/chat.service.ts | 42 +++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index a385011..67a37e0 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,13 +1,4 @@ -import { - Body, - Controller, - Get, - Param, - Post, - Query, - Req, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; @@ -30,13 +21,17 @@ export class ChatController { @Req() req: any, @Param('id') channelId: number, @Query('limit') limit: number, - @Query('currentPage') currentPage: number + @Query('cursor') cursor: number, + @Query('keyword') keyword: string, + @Query('direction') direction: string ) { return await this.chatService.getMessages( req.user.user_id, channelId, limit, - currentPage + cursor ? cursor : 0, + keyword, + direction ); } @@ -45,10 +40,4 @@ export class ChatController { async getChannel(@Req() req: any, @Param('id') channelId: number) { return await this.chatService.getChannel(req.user.user_id, channelId); } - - // 메세지 검색 - @Post(':id/search') - async searchMessage(@Body() body, @Param('id') id: number) { - return await this.chatService.searchMessage(id, body.keyword); - } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5bcb39e..0addac3 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -275,7 +275,14 @@ export class ChatService { } // 채널 메세지 조회 - async getMessages(userId: number, channelId: number, limit: number) { + async getMessages( + userId: number, + channelId: number, + limit: number, + cursor: number, + keyword: string, + direction: string + ) { try { // 유저 아이디가 채널에 속해있는지 확인 const auth = await this.prisma.channel_users.findMany({ @@ -290,10 +297,17 @@ export class ChatService { throw new Error('권한 X'); } + if (keyword) { + return this.searchMessage(channelId, limit, cursor, keyword, direction); + } // 메세지 데이터 조회 const result = await this.prisma.message.findMany({ + orderBy: { + id: direction == 'forward' ? 'asc' : 'desc', + }, where: { channel_id: channelId, + id: direction == 'forward' ? { gt: cursor } : { lt: cursor }, }, include: { user: { @@ -308,12 +322,9 @@ export class ChatService { }, }, }, - orderBy: { - id: 'desc', - }, + take: limit, }); - // 메세지 데이터 양식화 const data = await this.getMessageObj(result); @@ -321,9 +332,14 @@ export class ChatService { code: 200, text: '데이터 패칭 성공', }; + const cursors = { + prev: data[data.length - 1] ? data[data.length - 1].messageId : null, + next: data[0] ? data[0].messageId : null, + search: cursor, + }; // 응답데이터 {메세지데이터, 페이지네이션} - return { messages: data, message }; + return { messages: data, cursors, message }; } catch (err) { return err.message; } @@ -332,8 +348,9 @@ export class ChatService { // 메세지 검색 async searchMessage( channelId: number, - keyword: string, + limit: number, cursor: number, + keyword: string, direction: string ) { try { @@ -364,13 +381,13 @@ export class ChatService { orderBy: { id: 'desc' }, where: { channel_id: channelId, id: { lt: search } }, select: { id: true }, - take: 15, + take: limit, }), this.prisma.message.findMany({ orderBy: { id: 'asc' }, where: { channel_id: channelId, id: { gt: search } }, select: { id: true }, - take: 15, + take: limit, }), ]); @@ -382,9 +399,9 @@ export class ChatService { // 무한 스크롤용 커서 데이터 const cursors = { // backward 무한스크롤 요청 커서 - prev: forwardIds[0], + prev: forwardIds.length ? forwardIds[0] : null, // forward 무한스크롤 요청 커서 - next: backwordIds[backwordIds.length - 1], + next: backwordIds.length ? backwordIds[backwordIds.length - 1] : null, // 검색 메세지 아이디 커서 search, }; @@ -419,8 +436,7 @@ export class ChatService { }, }); - const obj = await this.getMessageObj(result); - return obj; + return result; } // 메세지 데이터 양식화 From 36077dfb0f9ec4af7a7b9b4388f41a410745aebb Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:05:37 +0900 Subject: [PATCH 216/414] =?UTF-8?q?[Fix]=20db=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c98bfa9..9f56330 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -229,7 +229,7 @@ model ProjectPost { title String content String @db.Text thumbnail_url String? - role Int + role String unit String start_date DateTime end_date DateTime From 0d14556eb21d685cbb38798b559f38e8fb3feedc Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:06:24 +0900 Subject: [PATCH 217/414] =?UTF-8?q?[Feat]=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 66980ae..ca7de06 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -1,4 +1,17 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; +import { ProjectService } from '@modules/project/project.service'; -@Controller('project') -export class ProjectController {} +@UseGuards(JwtAuthGuard) +@Controller('projects') +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @Get() + async getProjects( + @Query('skip') skip: number = 0, + @Query('limit') limit: number = 10 + ) { + return this.projectService.getProjects(skip, limit); + } +} From 33b308f4b7552f9f083fd49f966c4fe728f41f85 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:06:40 +0900 Subject: [PATCH 218/414] =?UTF-8?q?[Feat]=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 76 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 3274dd0..276ed9d 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -1,4 +1,78 @@ import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@prisma/prisma.service'; @Injectable() -export class ProjectService {} +export class ProjectService { + constructor(private prisma: PrismaService) {} + + async getProjects(skip: number, limit: number) { + const projects = await this.prisma.projectPost.findMany({ + skip, + take: limit, + include: { + Tags: { + select: { + tag: { + select: { name: true }, // 태그 이름만 선택 + }, + }, + }, + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + introduce: true, + role: { + select: { + name: true, + }, + }, + }, + }, + work_type: { + select: { name: true }, + }, + Applications: true, + }, + }); + + const totalCount = await this.prisma.projectPost.count(); + + const formattedProjects = projects.map(project => ({ + projectId: project.id, + title: project.title, + content: project.content, + thumbnail_url: project.thumbnail_url, + recruiting: project.recruiting, + start_date: project.start_date.toISOString(), + end_date: project.end_date.toISOString(), + tags: project.Tags.map(tag => tag.tag.name), + role: project.role, + work_type: project.work_type.name, + views: project.view, + applyCount: project.Applications.length, + bookMarkCount: project.saved_count, + user: { + id: project.user.id, + name: project.user.name, + nickname: project.user.nickname, + profile_url: project.user.profile_url, + introduce: project.user.introduce, + role: project.user.role.name, + }, + })); + + return { + message: { + code: 200, + text: '커넥션허브 목록 조회에 성공했습니다', + }, + formattedProjects, + totalCount: totalCount, + skip, + limit, + }; + } +} From 665a6de35a76308e8468193d553c7f465c4fd12b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:06:56 +0900 Subject: [PATCH 219/414] =?UTF-8?q?[Feat]=20project=20Module=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.module.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts index 4e9329e..66d6b9c 100644 --- a/src/modules/project/project.module.ts +++ b/src/modules/project/project.module.ts @@ -1,9 +1,14 @@ import { Module } from '@nestjs/common'; import { ProjectController } from './project.controller'; import { ProjectService } from './project.service'; +import { AuthModule } from '@modules/auth/auth.module'; +import { UserService } from '@modules/user/user.service'; +import { PrismaService } from '@prisma/prisma.service'; @Module({ + imports: [AuthModule], controllers: [ProjectController], - providers: [ProjectService], + providers: [ProjectService, PrismaService], + exports: [UserService], }) export class ProjectModule {} From a43dd1adff3494e2009c3ccfd52bc18050773691 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 17:35:18 +0900 Subject: [PATCH 220/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C/=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 22 ++++++++++++++- src/chat/chat.service.ts | 56 ++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index 67a37e0..bd697cf 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -30,7 +30,6 @@ export class ChatController { channelId, limit, cursor ? cursor : 0, - keyword, direction ); } @@ -40,4 +39,25 @@ export class ChatController { async getChannel(@Req() req: any, @Param('id') channelId: number) { return await this.chatService.getChannel(req.user.user_id, channelId); } + + // 채널 메세지 검색 + @Get('chnnales/:id/messages/search') + @UseGuards(JwtAuthGuard) + async searchChannelMessages( + @Req() req: any, + @Param('id') channelId: number, + @Query('limit') limit: number, + @Query('cursor') cursor: number, + @Query('keyword') keyword: string, + @Query('direction') direction: string + ) { + return await this.chatService.searchMessage( + req.user.user_id, + channelId, + limit, + cursor ? cursor : 0, + keyword, + direction + ); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 0addac3..be1a52d 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -280,7 +280,6 @@ export class ChatService { channelId: number, limit: number, cursor: number, - keyword: string, direction: string ) { try { @@ -297,18 +296,21 @@ export class ChatService { throw new Error('권한 X'); } - if (keyword) { - return this.searchMessage(channelId, limit, cursor, keyword, direction); - } // 메세지 데이터 조회 const result = await this.prisma.message.findMany({ orderBy: { - id: direction == 'forward' ? 'asc' : 'desc', - }, - where: { - channel_id: channelId, - id: direction == 'forward' ? { gt: cursor } : { lt: cursor }, + // 커서값이 없다면(초기요청) direction 상관없이 desc 정렬 + id: cursor ? (direction == 'forward' ? 'asc' : 'desc') : 'desc', }, + where: cursor + ? { + // forward(스크롤 다운)일 시 cursor보다 id 높은 값(최신) + // backward(스크롤 업)일 시 cursor보다 id 낮은 값(오래된) + channel_id: channelId, + id: direction === 'forward' ? { gt: cursor } : { lt: cursor }, + } + : { channel_id: channelId }, // 커서가 없으면 id 조건 없이 가져오기 + include: { user: { select: { @@ -325,21 +327,28 @@ export class ChatService { take: limit, }); + // 메세지 데이터 양식화 const data = await this.getMessageObj(result); - const message = { - code: 200, - text: '데이터 패칭 성공', - }; + // 메세지 데이터, 메세지 id순 오름차순 정렬 + const messages = + !cursor || direction == 'backward' ? data.reverse() : data; + + // 커서 const cursors = { prev: data[data.length - 1] ? data[data.length - 1].messageId : null, next: data[0] ? data[0].messageId : null, - search: cursor, }; - // 응답데이터 {메세지데이터, 페이지네이션} - return { messages: data, cursors, message }; + // 응답 메세지 + const message = { + code: 200, + text: '데이터 패칭 성공', + }; + + // 응답데이터 {메세지데이터, 커서, 응답메세지} + return { messages, cursors, message }; } catch (err) { return err.message; } @@ -347,6 +356,7 @@ export class ChatService { // 메세지 검색 async searchMessage( + userId: number, channelId: number, limit: number, cursor: number, @@ -354,9 +364,21 @@ export class ChatService { direction: string ) { try { + const auth = await this.prisma.channel_users.findMany({ + where: { + user_id: userId, + channel_id: channelId, + }, + }); + + // 아닐 시 예외처리 + if (!auth.length) { + throw new Error('권한 X'); + } + // 키워드에 해당하는 메세지id 검색 const keywordMessage = await this.prisma.message.findFirst({ - orderBy: { id: 'desc' }, + orderBy: { id: direction == 'forward' ? 'asc' : 'desc' }, where: { channel_id: channelId, // forward(스크롤 다운)일 시 cursor보다 id 높은 값(최신) From a872eddbdd7b4071895c20294b2801e6ca98fc2e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:48:35 +0900 Subject: [PATCH 221/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=9D=B8=20=EC=BB=A4?= =?UTF-8?q?=EB=84=A5=EC=85=98=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index ca7de06..c94bf1d 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -14,4 +14,9 @@ export class ProjectController { ) { return this.projectService.getProjects(skip, limit); } + + @Get('rankings') + async getWeeklyTopProjects() { + return this.projectService.getWeeklyTopProjects(); + } } From 0f90ff3672b7e6f33193368cb81e5358f3ab62cc Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 22 Jan 2025 17:48:42 +0900 Subject: [PATCH 222/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=9D=B8=20=EC=BB=A4?= =?UTF-8?q?=EB=84=A5=EC=85=98=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 276ed9d..7d37054 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -75,4 +75,53 @@ export class ProjectService { limit, }; } + + async getWeeklyTopProjects() { + // 이번 주 시작과 끝 날짜 계산 + const now = new Date(); + const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); // 주의 첫 번째 날(일요일) + const endOfWeek = new Date(now.setDate(now.getDate() - now.getDay() + 6)); // 주의 마지막 날(토요일) + + // 상위 5개 프로젝트 조회 + const topProjects = await this.prisma.projectPost.findMany({ + where: { + Applications: { + some: { + created_at: { + gte: startOfWeek, + lte: endOfWeek, + }, + }, + }, + }, + include: { + user: { + select: { + id: true, + nickname: true, + profile_url: true, + }, + }, + Applications: true, + }, + orderBy: { + Applications: { + _count: 'desc', + }, + }, + take: 5, // 상위 5개만 가져오기 + }); + + // 데이터 가공 + return topProjects.map(project => ({ + id: project.id, + title: project.title, + author: { + userId: project.user.id, + nickname: project.user.nickname, + profileUrl: project.user.profile_url, + }, + applications_count: project.Applications.length, + })); + } } From 0605e94c68ecda56ea566dd0a3a4e7cdcba6484c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 19:03:45 +0900 Subject: [PATCH 223/414] =?UTF-8?q?[Fix]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BB=A4=EC=84=9C=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index be1a52d..d82b156 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -337,8 +337,8 @@ export class ChatService { // 커서 const cursors = { - prev: data[data.length - 1] ? data[data.length - 1].messageId : null, - next: data[0] ? data[0].messageId : null, + prev: data[0] ? data[0].messageId : null, + next: data[data.length - 1] ? data[data.length - 1].messageId : null, }; // 응답 메세지 From 5d212164af2302dd06985c45d85742a87c4cf4e8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 19:25:54 +0900 Subject: [PATCH 224/414] =?UTF-8?q?[Feat]=20next=20prev=20null=20=EA=B0=92?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index d82b156..5bfacee 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -336,10 +336,21 @@ export class ChatService { !cursor || direction == 'backward' ? data.reverse() : data; // 커서 - const cursors = { - prev: data[0] ? data[0].messageId : null, - next: data[data.length - 1] ? data[data.length - 1].messageId : null, - }; + + const prev = data[0] ? data[0].messageId : null; + const next = data[data.length - 1] + ? data[data.length - 1].messageId + : null; + + let cursors; + + if (direction == 'backward' && !next) { + cursors = { prev }; + } else if (direction == 'forward' && !prev) { + cursors = { next }; + } else { + cursors = { next, prev }; + } // 응답 메세지 const message = { From 03185eefb1e333af4312bcb451c63b79e7eaf87f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 19:45:31 +0900 Subject: [PATCH 225/414] =?UTF-8?q?[Feat]=20cursor=20=EA=B0=92=20null=20?= =?UTF-8?q?=EC=9D=BC=EB=95=8C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 5bfacee..0ac82f6 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -387,6 +387,16 @@ export class ChatService { throw new Error('권한 X'); } + if (!cursor) { + const res = await this.prisma.message.findFirst({ + orderBy: { id: 'desc' }, + where: { channel_id: channelId }, + select: { id: true }, + }); + + cursor = res.id; + } + // 키워드에 해당하는 메세지id 검색 const keywordMessage = await this.prisma.message.findFirst({ orderBy: { id: direction == 'forward' ? 'asc' : 'desc' }, From 7e8e19424316181cc2140022dcb61f75294f7c2b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 19:53:14 +0900 Subject: [PATCH 226/414] =?UTF-8?q?[Fix]=20url=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index bd697cf..b6b09ad 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -41,7 +41,7 @@ export class ChatController { } // 채널 메세지 검색 - @Get('chnnales/:id/messages/search') + @Get('channels/:id/messages/search') @UseGuards(JwtAuthGuard) async searchChannelMessages( @Req() req: any, From 9c236655324e582358ee01c44674af7a9a765d2d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 20:09:23 +0900 Subject: [PATCH 227/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=BB=A4=EC=84=9C=EA=B0=92=20?= =?UTF-8?q?direction=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 0ac82f6..996be65 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -336,21 +336,14 @@ export class ChatService { !cursor || direction == 'backward' ? data.reverse() : data; // 커서 - - const prev = data[0] ? data[0].messageId : null; - const next = data[data.length - 1] - ? data[data.length - 1].messageId - : null; - - let cursors; - - if (direction == 'backward' && !next) { - cursors = { prev }; - } else if (direction == 'forward' && !prev) { - cursors = { next }; - } else { - cursors = { next, prev }; - } + const cursors = + direction == 'backward' + ? { prev: data[0] ? data[0].messageId : null } + : { + next: data[data.length - 1] + ? data[data.length - 1].messageId + : null, + }; // 응답 메세지 const message = { From 3119c1e036e5c79ded805550d8ffc1431309dbde Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 21:33:20 +0900 Subject: [PATCH 228/414] =?UTF-8?q?[Fix]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 996be65..7200e17 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -472,7 +472,7 @@ export class ChatService { }, }); - return result; + return this.getMessageObj(result); } // 메세지 데이터 양식화 From 7ddc12eedfcfc2f190a354edde5a79da71c9d882 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 21:57:06 +0900 Subject: [PATCH 229/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=97=90=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 5 +++-- src/feed/feed.service.ts | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index a078447..7fff78e 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -39,8 +39,9 @@ export class FeedController { // 피드 조회 (댓글) @Get(':id/comments') - async getFeedComments(@Param('id') feedId: number) { - return await this.feedService.getFeedComments(feedId); + @UseGuards(OptionalAuthGuard) + async getFeedComments(@Param('id') feedId: number, @Req() req) { + return await this.feedService.getFeedComments(feedId, req.user); } // 좋아요 추가/ 제거 diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 427c10f..d1b68ce 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -183,8 +183,10 @@ export class FeedService { } // 피드 개별 조회 (댓글) - async getFeedComments(feedId: number) { + async getFeedComments(feedId: number, user) { try { + const userId = user ? user.user_id : 0; + const result = await this.prisma.feedComment.findMany({ where: { post_id: feedId, @@ -224,6 +226,9 @@ export class FeedService { comment: c.content, likeCount: c.FeedCommentLikes.length, createdAt: c.created_at, + isLiked: userId + ? !!c.FeedCommentLikes.filter(v => v.user_id == userId).length + : false, })); return { From 00d8b356014408cc21267debd7a5f126f4f048ba Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 22:01:19 +0900 Subject: [PATCH 230/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20?= =?UTF-8?q?S3=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index d1b68ce..949466a 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -588,7 +588,7 @@ export class FeedService { 8, file.buffer, fileType, - 'pad_feed' + 'pad_feed/images' ); return { From 1cb6e7764dec58b4db9c1f43b39b5757c2e43e71 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 22:21:30 +0900 Subject: [PATCH 231/414] =?UTF-8?q?[Feat]=20=ED=83=9C=EA=B7=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 6 ++++++ src/feed/feed.service.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 7fff78e..67d2552 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -30,6 +30,12 @@ export class FeedController { return this.feedService.getAllFeeds(req.user, queryDto); } + // 태그 데이터 조회 + @Get('/tags') + async getTags() { + return this.feedService.getTags(); + } + // 피드 조회 (게시글) @Get(':id') @UseGuards(OptionalAuthGuard) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 949466a..324081b 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -596,4 +596,8 @@ export class FeedService { message: { code: 200, message: '이미지 업로드가 완료되었습니다.' }, }; } + + async getTags() { + return await this.prisma.feedTag.findMany(); + } } From 330e30f0a930da27ec061063c04657d1336a63f3 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 22:26:41 +0900 Subject: [PATCH 232/414] =?UTF-8?q?[Chore]=20dayjs=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 91eff12..d6255a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", + "dayjs": "^1.11.13", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -6126,6 +6127,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 31cb79b..373435c 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.7", + "dayjs": "^1.11.13", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", From 0cf32cc21ba22384721b71cb453b610eea3afc16 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 23:01:05 +0900 Subject: [PATCH 233/414] =?UTF-8?q?[Feat]=20weekly=20best=20contents=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 6 +++++ src/feed/feed.service.ts | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 67d2552..1812a29 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -36,6 +36,12 @@ export class FeedController { return this.feedService.getTags(); } + // 위클리 베스트 컨텐츠 + @Get('/weekly') + async getWeeklyBest() { + return this.feedService.getWeeklyBest(); + } + // 피드 조회 (게시글) @Get(':id') @UseGuards(OptionalAuthGuard) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 324081b..f9329fb 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -5,6 +5,7 @@ import * as cheerio from 'cheerio'; import { CommentDto } from './dto/comment.dto'; import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; import { S3Service } from '@src/s3/s3.service'; +import * as dayjs from 'dayjs'; @Injectable() export class FeedService { @@ -600,4 +601,49 @@ export class FeedService { async getTags() { return await this.prisma.feedTag.findMany(); } + + async getWeeklyBest() { + // 현재 날짜 + const now = dayjs(); + // 이번주 시작(일요일) + const start = now.startOf('week').toDate(); + // 이번주 끝(토요일) + const end = now.endOf('week').toDate(); + + const result = await this.prisma.feedPost.findMany({ + where: { + created_at: { + gte: start, + lte: end, + }, + }, + // 1순위 좋아요 수, 2순위 조회수 + orderBy: [{ like_count: 'desc' }, { view: 'desc' }], + select: { + id: true, + title: true, + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + role: { select: { name: true } }, + }, + }, + }, + take: 5, + }); + + const contents = result.map(res => ({ + title: res.title, + userId: res.user.id, + userName: res.user.name, + userNickname: res.user.nickname, + userProfileUrl: res.user.profile_url, + userRole: res.user.role.name, + })); + + return { contents }; + } } From 8500b9694a2d4e7b1879b2741999e535cd014955 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 23:38:04 +0900 Subject: [PATCH 234/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5c218dd..37c8ef9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -227,17 +227,17 @@ model ProjectPost { id Int @id @default(autoincrement()) user_id Int title String - content String - thumbnail_url String - role Int + content String @db.Text + thumbnail_url String? + role String unit String start_date DateTime end_date DateTime work_type_id Int recruiting Boolean applicant_count Int - view Int - saved_count Int + view Int @default(0) + saved_count Int @default(0) created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) @@ -303,12 +303,15 @@ model ProjectPostTag { } model UserApplyProject { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + status String @default("Pending") + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + @@unique([user_id, post_id]) @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From f9bc722731303c9b8cf10efe726b9f3b39d5c8ee Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 23:41:45 +0900 Subject: [PATCH 235/414] =?UTF-8?q?[Feat]=20=ED=83=9C=EA=B7=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index f9329fb..b27eb71 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -599,7 +599,12 @@ export class FeedService { } async getTags() { - return await this.prisma.feedTag.findMany(); + const tags = await this.prisma.feedTag.findMany(); + + return { + tags, + message: { code: 200, message: '태그가 성공적으로 조회되었습니다.' }, + }; } async getWeeklyBest() { From 96a67068d2737f8fb0f517569fb1d1cdbcae8810 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Wed, 22 Jan 2025 23:56:15 +0900 Subject: [PATCH 236/414] =?UTF-8?q?[Feat]=20=EC=A3=BC=EA=B0=84=20=EC=9D=B8?= =?UTF-8?q?=EA=B8=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index b27eb71..04998df 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -641,6 +641,7 @@ export class FeedService { }); const contents = result.map(res => ({ + postId: res.id, title: res.title, userId: res.user.id, userName: res.user.name, @@ -649,6 +650,9 @@ export class FeedService { userRole: res.user.role.name, })); - return { contents }; + return { + contents, + message: { code: 200, message: '성공적으로 조회되었습니다.' }, + }; } } From 84520bd7776a396e0df05bbb61cee3818249af2f Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 17:32:43 +0900 Subject: [PATCH 237/414] =?UTF-8?q?[Fix]=20DB=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 52 ++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9f56330..65132aa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -227,33 +227,25 @@ model ProjectPost { id Int @id @default(autoincrement()) user_id Int title String - content String @db.Text + content String @db.Text thumbnail_url String? - role String - unit String + role String //[Programmer, Artist, Designer] + hub_type String //[Project, 아웃소싱] start_date DateTime - end_date DateTime - work_type_id Int + duration String + work_type String //[Online, Offline] recruiting Boolean - applicant_count Int - view Int @default(0) - saved_count Int @default(0) + applicant_count Int @default(0) + view Int @default(0) + saved_count Int @default(0) created_at DateTime @default(now()) Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) - work_type WorkType @relation(fields: [work_type_id], references: [id]) Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] @@index([user_id], map: "ProjectPost_user_id_fkey") - @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") -} - -model WorkType { - id Int @id @default(autoincrement()) - name String - ProjectPosts ProjectPost[] } model ProjectSave { @@ -281,34 +273,36 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String + name String @unique Tags ProjectPostTag[] } model ProjectPostTag { - id Int @id @default(autoincrement()) - post_id Int - tag_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - tag ProjectTag @relation(fields: [tag_id], references: [id]) + id Int @id @default(autoincrement()) + post_id Int + tag_id Int + post ProjectPost @relation(fields: [post_id], references: [id]) + tag ProjectTag @relation(fields: [tag_id], references: [id]) + created_at DateTime @default(now()) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") } model UserApplyProject { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - status String @default("Pending") // "Accepted", "Rejected", "Pending" - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + status String @default("Pending") // "Accepted", "Rejected", "Pending" + created_at DateTime @default(now()) // 지원 생성 시간 + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@unique([user_id, post_id]) // 중복 지원 방지 @@index([post_id], map: "UserApplyProject_post_id_fkey") From de05ddf667b95519b1d76c50a18969cbb1a08955 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 17:33:12 +0900 Subject: [PATCH 238/414] =?UTF-8?q?[Feat]=20App=20Module=EC=97=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index a6a799f..6b1541a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; import { FeedModule } from './feed/feed.module'; +import { ProjectModule } from './modules/project/project.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -14,6 +15,7 @@ import { FeedModule } from './feed/feed.module'; UserModule, ChatModule, FeedModule, + ProjectModule ], providers: [ChatGateway], }) From 3424996cde223f2acf0883b0cd151fcdbd88c669 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 17:33:38 +0900 Subject: [PATCH 239/414] =?UTF-8?q?[Feat]=20CreateProject=20DTO=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/dto/CreateProject.dto.ts | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/modules/project/dto/CreateProject.dto.ts diff --git a/src/modules/project/dto/CreateProject.dto.ts b/src/modules/project/dto/CreateProject.dto.ts new file mode 100644 index 0000000..1f7682f --- /dev/null +++ b/src/modules/project/dto/CreateProject.dto.ts @@ -0,0 +1,50 @@ +import { + IsString, + IsBoolean, + IsDateString, + IsArray, + ArrayNotEmpty, + IsIn, + Length, +} from 'class-validator'; + +export class CreateProjectDto { + @IsString() + @Length(1, 100) + title: string; + + @IsString() + @Length(1, 500) + content: string; + + @IsString() + @IsIn(['Programmer', 'Designer', 'Artist']) + role: string; + + @IsString() + @IsIn(['PROJECT', 'OUTSOURCING']) + hub_type: string; + + @IsDateString() + start_date: string; + + @IsString() + duration: string; + + @IsString() + @IsIn(['ONLINE', 'OFFLINE']) + work_type: string; + + @IsBoolean() + recruiting: boolean; + + @IsArray() + @ArrayNotEmpty() + @IsString({ each: true }) + skills: string[]; + + @IsArray() + @ArrayNotEmpty() + @IsString({ each: true }) + detail_roles: string[]; +} From c76e407a15d86707b01c3b158ba8139e7571a4fb Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 17:34:01 +0900 Subject: [PATCH 240/414] =?UTF-8?q?[Feat]=20Project=20Module=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts index 66d6b9c..ff40c7b 100644 --- a/src/modules/project/project.module.ts +++ b/src/modules/project/project.module.ts @@ -9,6 +9,6 @@ import { PrismaService } from '@prisma/prisma.service'; imports: [AuthModule], controllers: [ProjectController], providers: [ProjectService, PrismaService], - exports: [UserService], + exports: [ProjectService], }) export class ProjectModule {} From f061a03929c4583a1e5d2eeac9d3ae0db0bed042 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 17:34:27 +0900 Subject: [PATCH 241/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C,=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index c94bf1d..4f81642 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -1,7 +1,15 @@ -import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { ProjectService } from '@modules/project/project.service'; - +import { CreateProjectDto } from './dto/CreateProject.dto'; @UseGuards(JwtAuthGuard) @Controller('projects') export class ProjectController { @@ -10,13 +18,17 @@ export class ProjectController { @Get() async getProjects( @Query('skip') skip: number = 0, - @Query('limit') limit: number = 10 + @Query('limit') limit: number = 10, + @Query('role') role?: string, + @Query('unit') unit?: string, + @Query('sort') sort: string = 'latest' ) { - return this.projectService.getProjects(skip, limit); + return this.projectService.getProjects({ skip, limit, role, unit, sort }); } - @Get('rankings') - async getWeeklyTopProjects() { - return this.projectService.getWeeklyTopProjects(); + @Post() + async createProject(@Body() createProjectDto: CreateProjectDto, @Req() req) { + const userId = req.user.user_id; + return this.projectService.createProject(createProjectDto, userId); } } From 855bac9e57be4ea57f921de30e267a72b6007b60 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Thu, 23 Jan 2025 18:24:11 +0900 Subject: [PATCH 242/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20API=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AC=B8=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/docs/feed.docs.ts | 555 +++++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 src/feed/docs/feed.docs.ts diff --git a/src/feed/docs/feed.docs.ts b/src/feed/docs/feed.docs.ts new file mode 100644 index 0000000..76f05df --- /dev/null +++ b/src/feed/docs/feed.docs.ts @@ -0,0 +1,555 @@ +import { + ApiOperation, + ApiParam, + ApiResponse, + ApiBody, + ApiConsumes, + ApiQuery, +} from '@nestjs/swagger'; + +export const getMainPageDocs = { + ApiOperation: ApiOperation({ + summary: '메인 페이지 피드 조회', + description: '작성된 피드 목록을 조회합니다', + }), + ApiQuery: ApiQuery({ + name: 'latest', + required: false, + example: true, + description: '최신순 정렬 여부 (기본값: 인기순)', + }), + ApiQuery2: ApiQuery({ + name: 'cursor', + required: false, + description: '무한 스크롤 커서', + }), + ApiQuery3: ApiQuery({ + name: 'tags', + required: false, + description: '태그 아이디(태그별 게시글 조회할 때 사용)', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '메인 페이지 피드 목록 조회 성공', + schema: { + example: { + posts: [ + { + userId: '작성자 아이디', + userName: '작성자 이름', + userNickname: '작성자 닉네임', + userRole: '작성자 role', + userProfileUrl: '작성자 프로필 사진 주소', + postId: '게시글 아이디', + title: '게시글 제목', + thumbnailUrl: '게시글 썸네일 사진 주소', + content: '게시글 내용', + tags: ['게시글 태그1', '게시글 태그2'], + commentCount: '댓글 수', + likeCount: '좋아요 수', + viewCount: '조회 수', + isLiked: '좋아요 여부', + createdAt: '작성 시간', + }, + ], + message: { + code: 200, + message: '전체 피드를 정상적으로 조회했습니다.', + }, + }, + }, + }), +}; + +export const getWeeklyBestDocs = { + ApiOperation: ApiOperation({ + summary: '주간 인기 게시글 조회', + description: + '좋아요 순으로 주간 인기 게시글을 조회합니다 좋아요 수가 같을 시 조회 수로 정렬됩니다', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '주간 인기 게시글 조회 성공', + schema: { + example: { + contents: [ + { + postId: '게시글 아이디', + title: '게시글 제목', + userId: '유저 아이디', + userName: '유저 이름', + userNickname: '유저 닉네임', + userProfileUrl: '유저 프로필 url', + userRole: '유저 roel', + }, + { + postId: '게시글 아이디', + title: '게시글 제목', + userId: '유저 아이디', + userName: '유저 이름', + userNickname: '유저 닉네임', + userProfileUrl: '유저 프로필 url', + userRole: '유저 roel', + }, + ], + + message: { + code: '200', + message: '성공적으로 조회되었습니다.', + }, + }, + }, + }), +}; + +export const getTagsDoc = { + ApiOperation: ApiOperation({ + summary: '태그 데이터 조회', + description: 'DB에 저장되어 있는 태그 데이터를 조회합니다 ', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '피드 데이터 조회 성공', + schema: { + example: { + tags: [ + { id: 1, name: '고민' }, + { id: 2, name: '회고' }, + { id: 3, name: '아이디어' }, + { id: 4, name: '계획' }, + { id: 5, name: '토론' }, + { id: 6, name: '정보공유' }, + { id: 7, name: '추천' }, + { id: 8, name: '질문' }, + ], + message: { + code: 200, + message: '태그가 성공적으로 조회되었습니다.', + }, + }, + }, + }), +}; + +export const getFeedDocs = { + ApiOperation: ApiOperation({ + summary: '피드 개별 조회 (게시글)', + description: '개별 피드를 조회합니다', + }), + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '조회할 피드 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '개별 피드 조회 성공', + schema: { + example: { + post: { + userId: '작성자 아이디', + userName: '작성자 이름', + userNickname: '작성자 닉네임', + userRole: '작성자 role', + userProfileUrl: '작성자 프로필 사진 주소', + postId: '게시글 아이디', + title: '게시글 제목', + thumbnailUrl: '게시글 썸네일 사진 주소', + content: '게시글 내용', + tags: ['게시글 태그1', '게시글 태그2'], + commentCount: '댓글 수', + likeCount: '좋아요 수', + viewCount: '조회 수', + isLiked: '좋아요 여부', + createdAt: '작성 시간', + }, + message: { + code: 200, + message: '개별 피드를 정상적으로 조회했습니다.', + }, + }, + }, + }), +}; + +export const getFeedCommentDocs = { + ApiOperation: ApiOperation({ + summary: '피드 개별 조회 (댓글)', + description: '개별 피드의 댓글을 조회합니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '조회할 피드의 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '댓글 조회 성공', + schema: { + example: { + comments: [ + { + commentId: '댓글 아이디', + userId: '댓글 작성자 아이디', + userName: '작성자 이름', + userRole: '작성자 role', + userProfileUrl: '작성자 프로필 사진 주소', + comment: '댓글 내용', + createdAt: '작성 시간', + likeCount: '좋아요 수', + isLiked: '좋아요 여부', + }, + { + commentId: '댓글 아이디', + userId: '댓글 작성자 아이디', + userName: '작성자 이름', + userRole: '작성자 role', + userProfileUrl: '작성자 프로필 사진 주소', + comment: '댓글 내용', + createdAt: '작성 시간', + likeCount: '좋아요 수', + isLiked: '좋아요 여부', + }, + ], + message: { + code: 200, + message: '개별 피드(댓글)를 정상적으로 조회했습니다.', + }, + }, + }, + }), +}; + +export const handleFeedLikesDocs = { + ApiOperation: ApiOperation({ + summary: '피드 좋아요 추가/제거', + description: '요청 시 좋아요 여부에 따라 좋아요가 추가/제거 됩니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '피드 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '좋아요 추가/제거 성공', + schema: { + examples: [ + { + message: { + code: 200, + message: '좋아요가 취소되었습니다.', + }, + }, + { + message: { + code: 200, + message: '좋아요가 추가되었습니다.', + }, + }, + ], + }, + }), +}; + +export const createFeedDocs = { + ApiOperation: ApiOperation({ + summary: '피드 등록', + description: '피드를 등록합니다', + }), + + ApiBody: ApiBody({ + description: '등록할 피드 데이터', + schema: { + type: 'object', + properties: { + title: { + type: 'string', + description: '피드 제목', + }, + tags: { + type: 'string[]', + description: '피드 태그 목록', + example: ['고민', '회고'], + }, + content: { + type: 'string', + description: '피드 내용', + }, + }, + required: ['title', 'tags', 'content'], + }, + }), + + ApiResponse: ApiResponse({ + status: 201, + description: '피드 등록 성공', + schema: { + example: { + message: { + code: 201, + message: '피드 작성이 완료되었습니다.', + }, + post: { + id: '게시글 아이디', + }, + }, + }, + }), +}; + +export const updateFeedDocs = { + ApiOperation: ApiOperation({ + summary: '피드 수정', + description: '피드를 수정합니다', + }), + + ApiBody: ApiBody({ + description: '수정할 피드 데이터', + schema: { + type: 'object', + properties: { + title: { + type: 'string', + description: '수정 or 기존 피드 제목', + }, + tags: { + type: 'string[]', + description: '수정 or 기존 피드 태그 목록', + example: ['고민', '회고'], + }, + content: { + type: 'string', + description: '수정 or 기존 피드 내용', + }, + }, + required: ['title', 'tags', 'content'], + }, + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '피드 수정 완료', + schema: { + example: { + message: { + code: 200, + message: '피드 수정이 완료되었습니다.', + }, + }, + }, + }), +}; + +export const deleteFeedDocs = { + ApiOperation: ApiOperation({ + summary: '피드 삭제', + description: '피드를 삭제합니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '삭제할 피드 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '피드 삭제 성공', + schema: { + example: { + message: { + code: 200, + message: '피드가 삭제되었습니다.', + }, + }, + }, + }), +}; + +export const createCommentDocs = { + ApiOperation: ApiOperation({ + summary: '댓글 등록', + description: '피드에 댓글을 등록합니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '댓글을 작성할 피드 아이디', + }), + + ApiBody: ApiBody({ + description: '등록할 댓글 데이터', + schema: { + type: 'object', + properties: { + content: { + type: 'string', + description: '댓글 내용', + }, + }, + }, + }), + + ApiResponse: ApiResponse({ + status: 201, + description: '댓글 등록 성공', + schema: { + example: { + message: { + code: 201, + message: '댓글이 등록이 완료되었습니다.', + }, + }, + }, + }), +}; + +export const updateCommentDocs = { + ApiOperation: ApiOperation({ + summary: '댓글 수정', + description: '기존 댓글을 수정합니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '댓글이 작성된 피드 아이디', + }), + + ApiParam2: ApiParam({ + name: 'commentId', + required: true, + description: '댓글 아이디', + }), + + ApiBody: ApiBody({ + description: '수정할 댓글 데이터', + schema: { + type: 'object', + properties: { + content: { + type: 'string', + description: '수정 댓글 내용', + }, + }, + }, + }), + + ApiResponse: ApiResponse({ + status: 201, + description: '댓글 수정 성공', + schema: { + example: { + message: { + code: 201, + message: '댓글 수정이 완료되었습니다.', + }, + }, + }, + }), +}; + +export const deleteCommentDocs = { + ApiOperation: ApiOperation({ + summary: '댓글 삭제', + description: '댓글을 삭제합니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '삭제할 댓글 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '댓글 삭제 성공', + schema: { + example: { + message: { + code: 200, + message: '댓글이 삭제되었습니다.', + }, + }, + }, + }), +}; + +export const handleCommentLikesDocs = { + ApiOperation: ApiOperation({ + summary: '댓글 좋아요 추가/제거', + description: '요청 시 좋아요 여부에 따라 좋아요가 추가/제거 됩니다', + }), + + ApiParam: ApiParam({ + name: 'id', + required: true, + description: '댓글 아이디', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '좋아요 추가/제거 성공', + schema: { + examples: [ + { + message: { + code: 200, + message: '좋아요가 취소되었습니다.', + }, + }, + { + message: { + code: 200, + message: '좋아요가 추가되었습니다.', + }, + }, + ], + }, + }), +}; + +export const uploadImageDocs = { + ApiOperation: ApiOperation({ + summary: '이미지 업로드', + description: '이미지 업로드 시 이미지 링크를 반환합니다', + }), + + ApiConsumes: ApiConsumes('multipart/form-data'), + ApiBody: ApiBody({ + description: '업로드할 이미지 파일', + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '이미지 업로드 성공', + schema: { + example: { + imageUrl: '이미지 url', + message: { + code: 200, + message: '이미지 업로드가 완료되었습니다.', + }, + }, + }, + }), +}; From 7bca6d44054292c67a33f51b97d1af5e8dc9a032 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 23:59:19 +0900 Subject: [PATCH 243/414] =?UTF-8?q?[Add]=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 91eff12..5a5d372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", + "date-fns": "^4.1.0", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -6126,6 +6127,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 31cb79b..2cb2a7b 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.7", + "date-fns": "^4.1.0", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", From 58c19936d7acf43b54cfe7c9fa9a7315f0c10975 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 23 Jan 2025 23:59:48 +0900 Subject: [PATCH 244/414] [Fix] DB update --- prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 65132aa..d78617c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -234,7 +234,7 @@ model ProjectPost { start_date DateTime duration String work_type String //[Online, Offline] - recruiting Boolean + recruiting Boolean @default(true) applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) @@ -252,6 +252,7 @@ model ProjectSave { id Int @id @default(autoincrement()) user_id Int post_id Int + created_at DateTime @default(now()) // 지원 생성 시간 post ProjectPost @relation(fields: [post_id], references: [id]) user User @relation(fields: [user_id], references: [id]) From 31395765b58933d721ff16fe3c3de22be4d76e56 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 01:13:39 +0900 Subject: [PATCH 245/414] =?UTF-8?q?[Feat]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=EC=97=90=20=ED=86=A0=ED=81=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/docs/feed.docs.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/feed/docs/feed.docs.ts b/src/feed/docs/feed.docs.ts index 76f05df..12b09dc 100644 --- a/src/feed/docs/feed.docs.ts +++ b/src/feed/docs/feed.docs.ts @@ -5,6 +5,7 @@ import { ApiBody, ApiConsumes, ApiQuery, + ApiBearerAuth, } from '@nestjs/swagger'; export const getMainPageDocs = { @@ -227,6 +228,8 @@ export const getFeedCommentDocs = { }; export const handleFeedLikesDocs = { + ApiBearerAuth: ApiBearerAuth(), + ApiOperation: ApiOperation({ summary: '피드 좋아요 추가/제거', description: '요청 시 좋아요 여부에 따라 좋아요가 추가/제거 됩니다', @@ -261,6 +264,7 @@ export const handleFeedLikesDocs = { }; export const createFeedDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '피드 등록', description: '피드를 등록합니다', @@ -307,6 +311,7 @@ export const createFeedDocs = { }; export const updateFeedDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '피드 수정', description: '피드를 수정합니다', @@ -350,6 +355,7 @@ export const updateFeedDocs = { }; export const deleteFeedDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '피드 삭제', description: '피드를 삭제합니다', @@ -376,6 +382,7 @@ export const deleteFeedDocs = { }; export const createCommentDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '댓글 등록', description: '피드에 댓글을 등록합니다', @@ -415,6 +422,7 @@ export const createCommentDocs = { }; export const updateCommentDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '댓글 수정', description: '기존 댓글을 수정합니다', @@ -460,6 +468,7 @@ export const updateCommentDocs = { }; export const deleteCommentDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '댓글 삭제', description: '댓글을 삭제합니다', @@ -486,6 +495,8 @@ export const deleteCommentDocs = { }; export const handleCommentLikesDocs = { + ApiBearerAuth: ApiBearerAuth(), + ApiOperation: ApiOperation({ summary: '댓글 좋아요 추가/제거', description: '요청 시 좋아요 여부에 따라 좋아요가 추가/제거 됩니다', @@ -520,6 +531,7 @@ export const handleCommentLikesDocs = { }; export const uploadImageDocs = { + ApiBearerAuth: ApiBearerAuth(), ApiOperation: ApiOperation({ summary: '이미지 업로드', description: '이미지 업로드 시 이미지 링크를 반환합니다', From d8692164b5d79f336445a2118719f21e4353bad0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 01:21:37 +0900 Subject: [PATCH 246/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.controller.ts | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/feed/feed.controller.ts b/src/feed/feed.controller.ts index 1812a29..2550fe4 100644 --- a/src/feed/feed.controller.ts +++ b/src/feed/feed.controller.ts @@ -19,12 +19,33 @@ import { FeedDto } from './dto/feed.dto'; import { CommentDto } from './dto/comment.dto'; import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; import { FileInterceptor } from '@nestjs/platform-express'; +import { + createCommentDocs, + createFeedDocs, + deleteCommentDocs, + deleteFeedDocs, + getFeedCommentDocs, + getFeedDocs, + getMainPageDocs, + getTagsDoc, + getWeeklyBestDocs, + handleCommentLikesDocs, + handleFeedLikesDocs, + updateCommentDocs, + updateFeedDocs, + uploadImageDocs, +} from './docs/feed.docs'; @Controller('feed') export class FeedController { constructor(private readonly feedService: FeedService) {} // 메인 페이지 조회 @Get() + @getMainPageDocs.ApiOperation + @getMainPageDocs.ApiQuery + @getMainPageDocs.ApiQuery2 + @getMainPageDocs.ApiQuery3 + @getMainPageDocs.ApiResponse @UseGuards(OptionalAuthGuard) async getAllFeed(@Req() req, @Query() queryDto: GetFeedsQueryDto) { return this.feedService.getAllFeeds(req.user, queryDto); @@ -32,12 +53,16 @@ export class FeedController { // 태그 데이터 조회 @Get('/tags') + @getTagsDoc.ApiOperation + @getTagsDoc.ApiResponse async getTags() { return this.feedService.getTags(); } // 위클리 베스트 컨텐츠 @Get('/weekly') + @getWeeklyBestDocs.ApiOperation + @getWeeklyBestDocs.ApiResponse async getWeeklyBest() { return this.feedService.getWeeklyBest(); } @@ -45,6 +70,9 @@ export class FeedController { // 피드 조회 (게시글) @Get(':id') @UseGuards(OptionalAuthGuard) + @getFeedDocs.ApiOperation + @getFeedDocs.ApiParam + @getFeedDocs.ApiResponse async getFeed(@Param('id') feedId: number, @Req() req) { return await this.feedService.getFeed(feedId, req.user); } @@ -52,6 +80,9 @@ export class FeedController { // 피드 조회 (댓글) @Get(':id/comments') @UseGuards(OptionalAuthGuard) + @getFeedCommentDocs.ApiOperation + @getFeedCommentDocs.ApiParam + @getFeedCommentDocs.ApiResponse async getFeedComments(@Param('id') feedId: number, @Req() req) { return await this.feedService.getFeedComments(feedId, req.user); } @@ -59,6 +90,10 @@ export class FeedController { // 좋아요 추가/ 제거 @Post(':id/likes') @UseGuards(JwtAuthGuard) + @handleFeedLikesDocs.ApiBearerAuth + @handleFeedLikesDocs.ApiOperation + @handleFeedLikesDocs.ApiParam + @handleFeedLikesDocs.ApiResponse async handleFeedLikes(@Req() req, @Param('id') feedId: number) { const userId = req.user.user_id; return await this.feedService.handleFeedLikes(feedId, userId); @@ -67,6 +102,10 @@ export class FeedController { // 피드 등록 @Post() @UseGuards(JwtAuthGuard) + @createFeedDocs.ApiBearerAuth + @createFeedDocs.ApiOperation + @createFeedDocs.ApiBody + @createFeedDocs.ApiResponse async createFeed(@Req() req, @Body() feedDto: FeedDto) { const userId = req.user.user_id; return this.feedService.createFeed(feedDto, userId); @@ -75,6 +114,10 @@ export class FeedController { // 피드 수정 @Put(':id') @UseGuards(JwtAuthGuard) + @updateFeedDocs.ApiBearerAuth + @updateFeedDocs.ApiOperation + @updateFeedDocs.ApiBody + @updateFeedDocs.ApiResponse async updateFeed( @Req() req, @Body() feedDto: FeedDto, @@ -87,6 +130,10 @@ export class FeedController { // 피드 삭제 @Delete(':id') @UseGuards(JwtAuthGuard) + @deleteFeedDocs.ApiBearerAuth + @deleteFeedDocs.ApiOperation + @deleteFeedDocs.ApiParam + @deleteFeedDocs.ApiResponse async deleteFeed(@Req() req, @Param('id') feedId: number) { const userId = req.user.user_id; return this.feedService.deleteFeed(userId, feedId); @@ -95,6 +142,11 @@ export class FeedController { // 댓글 등록 @Post(':id/comment') @UseGuards(JwtAuthGuard) + @createCommentDocs.ApiBearerAuth + @createCommentDocs.ApiOperation + @createCommentDocs.ApiParam + @createCommentDocs.ApiBody + @createCommentDocs.ApiResponse async createComment( @Req() req, @Param('id') feedId: number, @@ -107,6 +159,12 @@ export class FeedController { // 댓글 수정 @Put(':id/comment/:commentId') @UseGuards(JwtAuthGuard) + @updateCommentDocs.ApiBearerAuth + @updateCommentDocs.ApiOperation + @updateCommentDocs.ApiParam + @updateCommentDocs.ApiParam2 + @updateCommentDocs.ApiBody + @updateCommentDocs.ApiResponse async updateComment( @Req() req, @Param('id') feedId: number, @@ -125,6 +183,10 @@ export class FeedController { // 댓글 삭제 @Delete(':id/comment/:commentId') @UseGuards(JwtAuthGuard) + @deleteCommentDocs.ApiBearerAuth + @deleteCommentDocs.ApiOperation + @deleteCommentDocs.ApiParam + @deleteCommentDocs.ApiResponse async deleteComment( @Req() req, @Param('id') feedId: number, @@ -136,6 +198,10 @@ export class FeedController { @Post('comment/:id') @UseGuards(JwtAuthGuard) + @handleCommentLikesDocs.ApiBearerAuth + @handleCommentLikesDocs.ApiOperation + @handleCommentLikesDocs.ApiParam + @handleCommentLikesDocs.ApiResponse async handleCommentLikes(@Req() req, @Param('id') commentId: number) { const userId = req.user.user_id; return await this.feedService.handleCommentLikes(userId, commentId); @@ -145,6 +211,11 @@ export class FeedController { @Post('image') @UseInterceptors(FileInterceptor('file')) @UseGuards(JwtAuthGuard) + @uploadImageDocs.ApiBearerAuth + @uploadImageDocs.ApiOperation + @uploadImageDocs.ApiConsumes + @uploadImageDocs.ApiBody + @uploadImageDocs.ApiResponse async func(@Req() req, @UploadedFile() file: Express.Multer.File) { const userId = req.user.user_id; return await this.feedService.uploadFeedImage(userId, file); From cb9b672ca22c1f587037740dbd6f39befd886689 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 01:21:55 +0900 Subject: [PATCH 247/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 37c8ef9..75901e6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -230,38 +230,31 @@ model ProjectPost { content String @db.Text thumbnail_url String? role String - unit String start_date DateTime - end_date DateTime - work_type_id Int recruiting Boolean - applicant_count Int + applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) created_at DateTime @default(now()) + duration String + hub_type String + work_type String Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) - work_type WorkType @relation(fields: [work_type_id], references: [id]) Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] @@index([user_id], map: "ProjectPost_user_id_fkey") - @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") -} - -model WorkType { - id Int @id @default(autoincrement()) - name String - ProjectPosts ProjectPost[] } model ProjectSave { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([post_id], map: "ProjectSave_post_id_fkey") @@index([user_id], map: "ProjectSave_user_id_fkey") @@ -281,22 +274,23 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String + name String @unique Tags ProjectPostTag[] } model ProjectPostTag { - id Int @id @default(autoincrement()) - post_id Int - tag_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - tag ProjectTag @relation(fields: [tag_id], references: [id]) + id Int @id @default(autoincrement()) + post_id Int + tag_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + tag ProjectTag @relation(fields: [tag_id], references: [id]) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") From fa793fc240a510f6c1bcd95e8bffcd940ef88106 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 14:29:36 +0900 Subject: [PATCH 248/414] =?UTF-8?q?[Feat]=20=EC=B1=84=EB=84=90=20=EB=82=98?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 19 +++++++++++++++++++ src/chat/chat.service.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 760fb42..c91c300 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -203,4 +203,23 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { console.log(sendData); this.server.to(data.channelId.toString()).emit('message', sendData); } + + @SubscribeMessage('exitChannel') + async handleLeaveChannel( + @MessageBody() + data: { userId: number; channelId: number }, + @ConnectedSocket() client: Socket + ) { + const { userId, channelId } = data; + + // 채널 나가기 + client.leave(channelId.toString()); + // 채널 나간 후 클라이언트에게 채널 아이디 전달 + client.emit('channelExited', channelId); + + // DB에서 유저 삭제 + 채널 탈퇴 메세지 DB 저장 후 반환 + const leaveMessage = await this.chatService.deleteUser(userId, channelId); + + this.server.to(channelId.toString()).emit('message', leaveMessage); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7200e17..ae2b656 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -495,4 +495,34 @@ export class ChatService { })); return data; } + + async deleteUser(userId: number, channelId: number) { + await this.prisma.channel_users.deleteMany({ + where: { + channel_id: channelId, + user_id: userId, + }, + }); + const userData = this.getSenderProfile(userId); + const nickname = (await userData).nickname; + + const data = { + type: 'exit', + content: `${nickname} 님이 방을 나갔습니다`, + channel_id: channelId, + user_id: userId, + }; + + const msg = await this.prisma.message.create({ + data, + }); + + return { + type: msg.type, + content: msg.content, + channelId: msg.channel_id, + date: msg.created_at, + messageId: msg.id, + }; + } } From bd5e7c287c498c0e492739504383821826c91dc2 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 14:43:31 +0900 Subject: [PATCH 249/414] =?UTF-8?q?[Feat]=20=EB=82=98=EA=B0=80=EA=B8=B0=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=EC=97=90=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index ae2b656..2ab9b6e 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -518,6 +518,7 @@ export class ChatService { }); return { + userId, type: msg.type, content: msg.content, channelId: msg.channel_id, From 6d92e63550d5b914d8923606cd76bf658f3fde36 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:31:04 +0900 Subject: [PATCH 250/414] =?UTF-8?q?[Fix]=20User=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 59 +++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index e3c8e33..f044b86 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -710,7 +710,7 @@ export const AddUserLinksDocs = { description: '추가할 링크 목록', schema: { example: { - links: ['https://github.com/user', 'https://linkedin.com/in/user'], + url: 'https://github.com/user' }, }, }), @@ -741,7 +741,7 @@ export const DeleteUserLinksDocs = { description: '삭제할 링크 ID 목록', schema: { example: { - linkIds: [1, 2], + linkId: 1, }, }, }), @@ -763,6 +763,61 @@ export const DeleteUserLinksDocs = { }), }; +export const UpdateUserLinksDocs = { + ApiOperation: ApiOperation({ + summary: '링크 수정', + description: '사용자의 특정 링크를 수정합니다.', + }), + ApiBody: ApiBody({ + description: '수정할 링크의 ID와 새로운 URL 정보', + schema: { + type: 'object', + properties: { + linkId: { + type: 'number', + description: '수정할 링크의 ID', + example: 1, + }, + url: { + type: 'string', + description: '새로운 링크 URL', + example: 'https://example.com', + }, + }, + required: ['linkId', 'url'], + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '링크가 성공적으로 수정되었습니다.', + schema: { + type: 'object', + properties: { + message: { + type: 'object', + properties: { + code: { type: 'number', example: 200 }, + text: { + type: 'string', + example: '링크가 성공적으로 수정되었습니다.', + }, + }, + }, + links: { + type: 'array', + items: { + type: 'object', + properties: { + linkId: { type: 'number', example: 1 }, + url: { type: 'string', example: 'https://example.com' }, + }, + }, + }, + }, + }, + }), +}; + export const GetUserResumeDocs = { ApiOperation: ApiOperation({ summary: '사용자 이력서 조회', From 06c57cca77a323b1cf1418f04de3e9cb0d50ceca Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:32:09 +0900 Subject: [PATCH 251/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=B6=94=EA=B0=80/=EC=82=AD=EC=A0=9C/?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 8925e31..dbdfccd 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -48,6 +48,7 @@ import { DeleteUserResumeDocs, GetUserFeedPostsDocs, GetUserConnectionHubProjectsDocs, + UpdateUserLinksDocs, } from './docs/user.docs'; import { ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; import { S3Service } from '@src/s3/s3.service'; @@ -324,19 +325,34 @@ export class UserController { @AddUserLinksDocs.ApiOperation @AddUserLinksDocs.ApiBody @AddUserLinksDocs.ApiResponse - async addUserLinks(@Req() req, @Body('links') links: string[]) { + async addUserLinks(@Req() req, @Body('url') url: string) { const userId = req.user?.user_id; - const formattedLinks = links.map(url => ({ url })); - return this.userService.addUserLinks(userId, formattedLinks); + return this.userService.addUserLink(userId, url); } @Delete('profile/links') @DeleteUserLinksDocs.ApiOperation @DeleteUserLinksDocs.ApiBody @DeleteUserLinksDocs.ApiResponse - async deleteUserLinks(@Req() req, @Body('linkIds') linkIds: number[]) { + async deleteUserLinks(@Req() req, @Body('linkId') linkId: number) { const userId = req.user?.user_id; - return this.userService.deleteUserLinks(userId, linkIds); + return this.userService.deleteUserLink(userId, linkId); + } + + @Patch('profile/links') + @UpdateUserLinksDocs.ApiOperation + @UpdateUserLinksDocs.ApiBody + @UpdateUserLinksDocs.ApiResponse + async updateUserLink( + @Req() req, + @Body() updateData: { linkId: number; url: string } + ) { + const userId = req.user?.user_id; + return this.userService.updateUserLink( + userId, + updateData.linkId, + updateData.url + ); } @Delete('account') From a9bc0f75815c76b767b96c8e708e469cef55102a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:33:02 +0900 Subject: [PATCH 252/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=B6=94=EA=B0=80/=EC=82=AD=EC=A0=9C/?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 88 +++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 31ad45f..67bf4f0 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -395,7 +395,7 @@ export class UserService { url: link.link, // 링크 정보만 반환 })), skills: user.UserSkills.map(skill => skill.skill.name), // 기술 스택 - jobDeatil: user.job_detail, + jobDetail: user.job_detail, notifications: { pushAlert: user.push_alert, followingAlert: user.following_alert, @@ -619,27 +619,15 @@ export class UserService { }; } - async addUserLinks(userId: number, links: { url: string }[]) { - // 넘어온 URL 목록 - const urls = links.map(link => link.url); - - // 이미 존재하는 URL 조회 - const existingLinks = await this.prisma.myPageUserLink.findMany({ - where: { - user_id: userId, - link: { in: urls }, - }, - select: { link: true }, + async addUserLink(userId: number, url: string) { + // URL이 이미 존재하는지 확인 + const existingLink = await this.prisma.myPageUserLink.findFirst({ + where: { user_id: userId, link: url }, }); - // 이미 존재하는 URL 필터링 - const existingUrls = existingLinks.map(link => link.link); - const newLinks = links.filter(link => !existingUrls.includes(link.url)); - - // 추가할 URL이 없으면 바로 반환 - if (newLinks.length === 0) { - // 유저의 현재 링크 조회 - const currentLinks = await this.prisma.myPageUserLink.findMany({ + if (existingLink) { + // 이미 존재하는 경우 바로 반환 + const userLinks = await this.prisma.myPageUserLink.findMany({ where: { user_id: userId }, select: { id: true, link: true }, }); @@ -647,21 +635,21 @@ export class UserService { return { message: { code: 201, - text: '추가할 링크가 없습니다.', + text: '이미 존재하는 링크입니다.', }, - links: currentLinks.map(link => ({ + links: userLinks.map(link => ({ linkId: link.id, url: link.link, })), }; } - // 새 URL만 추가 - await this.prisma.myPageUserLink.createMany({ - data: newLinks.map(link => ({ + // 새 링크 추가 + await this.prisma.myPageUserLink.create({ + data: { user_id: userId, - link: link.url, - })), + link: url, + }, }); // 유저의 모든 링크 조회 @@ -682,17 +670,23 @@ export class UserService { }; } - async deleteUserLinks(userId: number, linkIds: number[]) { - const deletedLinks = await this.prisma.myPageUserLink.deleteMany({ + async deleteUserLink(userId: number, id: number) { + const deletedLink = await this.prisma.myPageUserLink.deleteMany({ where: { - id: { in: linkIds }, + id, user_id: userId, }, }); + + if (deletedLink.count === 0) { + throw new NotFoundException('삭제할 링크를 찾을 수 없습니다.'); + } + const updatedLinks = await this.prisma.myPageUserLink.findMany({ where: { user_id: userId }, select: { id: true, link: true }, }); + return { message: { code: 200, @@ -705,6 +699,40 @@ export class UserService { }; } + async updateUserLink(userId: number, id: number, url: string) { + // 수정할 링크가 존재하는지 확인 + const existingLink = await this.prisma.myPageUserLink.findFirst({ + where: { id, user_id: userId }, + }); + + if (!existingLink) { + throw new NotFoundException('수정할 링크를 찾을 수 없습니다.'); + } + + // URL 업데이트 + await this.prisma.myPageUserLink.update({ + where: { id }, + data: { link: url }, + }); + + // 유저의 모든 링크 조회 + const updatedLinks = await this.prisma.myPageUserLink.findMany({ + where: { user_id: userId }, + select: { id: true, link: true }, + }); + + return { + message: { + code: 200, + text: '링크가 성공적으로 수정되었습니다.', + }, + links: updatedLinks.map(link => ({ + linkId: link.id, + url: link.link, + })), + }; + } + async deleteAccount(userId: number) { // 유저가 존재하지 않을 경우 처리 const user = await this.prisma.user.findUnique({ From e209ff106bac84d939eb3a2bc480da9f22d0782e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:33:21 +0900 Subject: [PATCH 253/414] =?UTF-8?q?[Feat]=20Project=20Module=20Import=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts index ff40c7b..1042561 100644 --- a/src/modules/project/project.module.ts +++ b/src/modules/project/project.module.ts @@ -4,9 +4,10 @@ import { ProjectService } from './project.service'; import { AuthModule } from '@modules/auth/auth.module'; import { UserService } from '@modules/user/user.service'; import { PrismaService } from '@prisma/prisma.service'; +import { S3Module } from '@src/s3/s3.module'; @Module({ - imports: [AuthModule], + imports: [AuthModule, S3Module], controllers: [ProjectController], providers: [ProjectService, PrismaService], exports: [ProjectService], From 20b59090d1c8ad63b36d6cb18847fda4d4b04986 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:38:28 +0900 Subject: [PATCH 254/414] =?UTF-8?q?[Feat]=20Project=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 128 ++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 4f81642..dbb843d 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -1,15 +1,23 @@ import { Body, Controller, + Delete, Get, + Param, + ParseIntPipe, + Patch, Post, + Put, Query, Req, + UploadedFile, UseGuards, + UseInterceptors, } from '@nestjs/common'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { ProjectService } from '@modules/project/project.service'; import { CreateProjectDto } from './dto/CreateProject.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; @UseGuards(JwtAuthGuard) @Controller('projects') export class ProjectController { @@ -31,4 +39,124 @@ export class ProjectController { const userId = req.user.user_id; return this.projectService.createProject(createProjectDto, userId); } + + @Put(':projectId') + async updateProject( + @Param('projectId', ParseIntPipe) projectId: number, + @Body() updateProjectDto: CreateProjectDto, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.updateProject( + userId, + projectId, + updateProjectDto + ); + } + + @Delete(':projectId') + async deleteProject(@Req() req, @Param('projectId') projectId: string) { + const userId = req.user.user_id; + const numProjectId = parseInt(projectId, 10); + return this.projectService.deleteProject(userId, numProjectId); + } + + @Get('popular-this-week') + async getPopularProjectsThisWeek() { + return this.projectService.getPopularProjectsThisWeek(); + } + + @Post('image') + @UseInterceptors(FileInterceptor('file')) + async func(@Req() req, @UploadedFile() file: Express.Multer.File) { + const userId = req.user.user_id; + return await this.projectService.uploadFeedImage(userId, file); + } + + @Get(':projectId') + async getProjectDetail(@Req() req, @Param('projectId') projectId: string) { + const userId = req.user.user_id; + const numProjectId = parseInt(projectId, 10); + return this.projectService.getProjectDetail(userId, numProjectId); + } + + @Post(':projectId/apply') + async applyToProject( + @Param('projectId', ParseIntPipe) projectId: number, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.applyToProject(userId, projectId); + } + + @Get(':projectId/applicants') + async getApplicants(@Param('projectId', ParseIntPipe) projectId: number) { + return this.projectService.getApplicants(projectId); + } + + @Get(':projectId/apply-status') + async checkApplyStatus( + @Param('projectId', ParseIntPipe) projectId: number, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.checkApplyStatus(userId, projectId); + } + + @Delete(':projectId/cancel-apply') + async cancelApplication( + @Param('projectId', ParseIntPipe) projectId: number, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.cancelApplication(userId, projectId); + } + + @Patch(':projectId/applications/:applicationId/status') + async updateApplicationStatus( + @Param('projectId', ParseIntPipe) projectId: number, + @Param('applicationId', ParseIntPipe) applicationId: number, + @Body('status') status: 'Accepted' | 'Rejected' | 'Pending', + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.updateApplicationStatus( + userId, + projectId, + applicationId, + status + ); + } + + @Patch(':projectId/status') + async updateProjectStatus( + @Param('projectId', ParseIntPipe) projectId: number, + @Body('recruiting') recruiting: boolean, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.updateProjectStatus( + userId, + projectId, + recruiting + ); + } + + @Post(':projectId/bookmark') + async toggleBookmark( + @Param('projectId', ParseIntPipe) projectId: number, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.toggleBookmark(userId, projectId); + } + + @Get(':projectId/bookmark') + async checkBookmark( + @Param('projectId', ParseIntPipe) projectId: number, + @Req() req + ) { + const userId = req.user.id; // 인증된 사용자 ID + return this.projectService.checkBookmark(userId, projectId); + } } From cbc1cc9a3e8601e8b5a9ea6ff404d11cc70e1b66 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 15:38:38 +0900 Subject: [PATCH 255/414] =?UTF-8?q?[Feat]=20Project=20API=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 699 +++++++++++++++++++++++-- 1 file changed, 645 insertions(+), 54 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 7d37054..8d3603b 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -1,22 +1,52 @@ -import { Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; +import { CreateProjectDto } from './dto/CreateProject.dto'; +import { startOfWeek, endOfWeek } from 'date-fns'; +import { S3Service } from '@src/s3/s3.service'; +import * as cheerio from 'cheerio'; @Injectable() export class ProjectService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly s3: S3Service + ) {} + + async getProjects(params: { + skip: number; + limit: number; + role?: string; + unit?: string; + sort: string; + }) { + const { skip, limit, role, unit, sort } = params; + + const where: any = {}; + if (role) where.role = role; + if (unit) where.Tags = { some: { tag: { name: unit } } }; + + const orderBy: any[] = []; + if (sort === 'latest') { + orderBy.push({ created_at: 'desc' }); + } else if (sort === 'popular') { + orderBy.push({ view: 'desc' }); + } - async getProjects(skip: number, limit: number) { const projects = await this.prisma.projectPost.findMany({ skip, take: limit, + where, + orderBy, include: { - Tags: { - select: { - tag: { - select: { name: true }, // 태그 이름만 선택 - }, - }, - }, + Tags: { select: { tag: { select: { name: true } } } }, + Applications: { select: { id: true } }, user: { select: { id: true, @@ -31,65 +61,156 @@ export class ProjectService { }, }, }, - work_type: { - select: { name: true }, - }, - Applications: true, }, }); - const totalCount = await this.prisma.projectPost.count(); + const totalCount = await this.prisma.projectPost.count({ where }); const formattedProjects = projects.map(project => ({ - projectId: project.id, + id: project.id, title: project.title, content: project.content, thumbnail_url: project.thumbnail_url, - recruiting: project.recruiting, - start_date: project.start_date.toISOString(), - end_date: project.end_date.toISOString(), - tags: project.Tags.map(tag => tag.tag.name), role: project.role, - work_type: project.work_type.name, - views: project.view, + tags: project.Tags.map(tag => `#${tag.tag.name}`), + hub_type: project.hub_type, + start_date: project.start_date.toISOString().split('T')[0], + duration: project.duration, + work_type: project.work_type, + //recruiting: project.recruiting, applyCount: project.Applications.length, bookMarkCount: project.saved_count, + view_count: project.view, + status: project.recruiting ? 'OPEN' : 'CLOSED', user: { - id: project.user.id, - name: project.user.name, + userId: project.user.id, nickname: project.user.nickname, - profile_url: project.user.profile_url, - introduce: project.user.introduce, + name: project.user.name, + profileUrl: project.user.profile_url, role: project.user.role.name, }, })); return { - message: { - code: 200, - text: '커넥션허브 목록 조회에 성공했습니다', + data: formattedProjects, + meta: { + total_count: totalCount, + page: Math.floor(skip / limit) + 1, + limit, }, - formattedProjects, - totalCount: totalCount, - skip, - limit, }; } - async getWeeklyTopProjects() { - // 이번 주 시작과 끝 날짜 계산 - const now = new Date(); - const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); // 주의 첫 번째 날(일요일) - const endOfWeek = new Date(now.setDate(now.getDate() - now.getDay() + 6)); // 주의 마지막 날(토요일) + async createProject(createProjectDto: CreateProjectDto, userId: number) { + const { + title, + content, + role, + hub_type, + start_date, + duration, + work_type, + recruiting, + skills, + detail_roles, + } = createProjectDto; + + const thumbnailUrl = await this.getThumbnailUrl(content); + // 프로젝트 생성 + const project = await this.prisma.projectPost.create({ + data: { + title, + content, + role, + hub_type, + start_date: new Date(start_date), + duration, + work_type, + recruiting, + thumbnail_url: thumbnailUrl, + user_id: userId, // userId를 사용하여 사용자 식별 + }, + }); + + // 태그 저장 (skills) + const tags = []; + for (const skill of skills) { + const tag = await this.prisma.projectTag.upsert({ + where: { name: skill }, + create: { name: skill }, + update: {}, + }); + + await this.prisma.projectPostTag.create({ + data: { + post_id: project.id, + tag_id: tag.id, + }, + }); + + tags.push(tag.name); // 생성된 태그 추가 + } + + const roleMapping: Record = { + Programmer: 1, + Artist: 2, + Designer: 3, + }; - // 상위 5개 프로젝트 조회 - const topProjects = await this.prisma.projectPost.findMany({ + // 매핑된 role_id 가져오기 + const saveRoleId = roleMapping[role]; + + // 모집단위 저장 (detail_roles) + const roles = []; + for (const detail_role of detail_roles) { + const role = await this.prisma.detailRole.upsert({ + where: { name: detail_role }, + create: { name: detail_role, role_id: saveRoleId }, + update: {}, + }); + + await this.prisma.projectDetailRole.create({ + data: { + post_id: project.id, + detail_role_id: role.id, + }, + }); + + roles.push(role.name); // 생성된 모집단위 추가 + } + + // 결과 반환 + return { + message: '프로젝트 생성 완료', + project: { + id: project.id, + title: project.title, + content: project.content, + role: project.role, + hub_type: project.hub_type, + start_date: project.start_date, + duration: project.duration, + work_type: project.work_type, + status: project.recruiting ? 'OPEN' : 'CLOSE', + tags, + detail_roles: roles, + }, + }; + } + + async getPopularProjectsThisWeek() { + const today = new Date(); + const startDate = startOfWeek(today, { weekStartsOn: 1 }); // 월요일 시작 + const endDate = endOfWeek(today, { weekStartsOn: 1 }); // 일요일 종료 + + // 이번 주 북마크가 많은 프로젝트를 조회 + const popularProjects = await this.prisma.projectPost.findMany({ where: { - Applications: { + Saves: { some: { created_at: { - gte: startOfWeek, - lte: endOfWeek, + gte: startDate, + lte: endDate, }, }, }, @@ -97,31 +218,501 @@ export class ProjectService { include: { user: { select: { - id: true, + name: true, nickname: true, profile_url: true, + role: true, }, }, - Applications: true, }, orderBy: { - Applications: { - _count: 'desc', + saved_count: 'desc', // 북마크 수 기준 정렬 + }, + take: 5, // 상위 5개 + }); + + // 반환 데이터 가공 + return popularProjects.map(project => ({ + projectId: project.id, + title: project.title, + user: { + name: project.user.name, + nickname: project.user.nickname, + profile_url: project.user.profile_url, + role: project.user.role, + }, + hub_type: project.hub_type, + saved_count: project.saved_count, + })); + } + + async uploadFeedImage(userId: number, file: Express.Multer.File) { + const fileType = file.mimetype.split('/')[1]; + const imageUrl = await this.s3.uploadImage( + 8, + file.buffer, + fileType, + 'pad_projects/image' + ); + + return { + imageUrl, + message: { code: 200, text: '이미지 업로드가 완료되었습니다.' }, + }; + } + + // 썸네일 추출 + async getThumbnailUrl(text: string) { + try { + const $ = cheerio.load(text); + const thumnailUrl = $('img').first().attr('src'); + return thumnailUrl; + } catch (err) { + throw err; + } + } + + async getProjectDetail(userId: number, numProjectId: number) { + // 프로젝트 상세 정보 조회 + const project = await this.prisma.projectPost.findUnique({ + where: { id: numProjectId }, + include: { + Tags: { + select: { + tag: { + select: { name: true }, + }, + }, + }, + Details: { + select: { + detail_role: { + select: { name: true }, + }, + }, + }, + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + introduce: true, + }, }, }, - take: 5, // 상위 5개만 가져오기 }); - // 데이터 가공 - return topProjects.map(project => ({ + if (!project) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + let isOwnConnectionHub = true; + project.user.id === userId ? true : false; + // 프로젝트 데이터 정리 + return { id: project.id, title: project.title, - author: { - userId: project.user.id, + content: project.content, + role: project.role, + hub_type: project.hub_type, + start_date: project.start_date, + duration: project.duration, + work_type: project.work_type, + status: project.recruiting ? 'OPEN' : 'CLOSE', + tags: project.Tags.map(t => t.tag.name), + detail_roles: project.Details.map(d => d.detail_role.name), + manager: { + id: project.user.id, + name: project.user.name, nickname: project.user.nickname, - profileUrl: project.user.profile_url, + profile_url: project.user.profile_url, + introduce: project.user.introduce, + }, + isOwnConnectionHub, + }; + } + + async applyToProject(userId: number, projectId: number) { + // 프로젝트 존재 여부 확인 + const project = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + }); + + if (!project) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + + // 이미 지원했는지 확인 + const existingApplication = await this.prisma.userApplyProject.findUnique({ + where: { + user_id_post_id: { user_id: userId, post_id: projectId }, + }, + }); + + if (existingApplication) { + throw new ConflictException('이미 지원한 프로젝트입니다.'); + } + + // 지원 생성 + await this.prisma.userApplyProject.create({ + data: { + user_id: userId, + post_id: projectId, + }, + }); + + return { message: '프로젝트에 지원되었습니다.' }; + } + + async getApplicants(projectId: number) { + // 프로젝트 존재 여부 확인 + const project = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + }); + + if (!project) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + + // 지원자 목록 조회 + const applicants = await this.prisma.userApplyProject.findMany({ + where: { post_id: projectId }, + select: { + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + introduce: true, + }, + }, }, - applications_count: project.Applications.length, + }); + + return applicants.map(applicant => ({ + id: applicant.user.id, + name: applicant.user.name, + nickname: applicant.user.nickname, + profile_url: applicant.user.profile_url, + introduce: applicant.user.introduce, })); } + + async updateProject( + userId: number, + projectId: number, + updateProjectDto: CreateProjectDto + ) { + const { + title, + content, + role, + hub_type, + start_date, + duration, + work_type, + recruiting, + skills, + detail_roles, + } = updateProjectDto; + + // 권한 확인 + const auth = await this.feedAuth(userId, projectId); + if (!auth) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + + // 썸네일 URL 업데이트 + const thumbnailUrl = await this.getThumbnailUrl(content); + + // 프로젝트 업데이트 + await this.prisma.projectPost.update({ + where: { id: projectId }, + data: { + title, + content, + role, + hub_type, + start_date: new Date(start_date), + duration, + work_type, + recruiting, + thumbnail_url: thumbnailUrl, + }, + }); + + // 모집단위 업데이트 + const roleMapping: Record = { + Programmer: 1, + Artist: 2, + Designer: 3, + }; + + const saveRoleId = roleMapping[role]; + + await this.prisma.$transaction(async prisma => { + // 기존 태그 삭제 + await prisma.projectPostTag.deleteMany({ + where: { post_id: projectId }, + }); + + // 새 태그 추가 + for (const skill of skills) { + const tag = await prisma.projectTag.upsert({ + where: { name: skill }, + create: { name: skill }, + update: {}, + }); + + await prisma.projectPostTag.create({ + data: { + post_id: projectId, + tag_id: tag.id, + }, + }); + } + + // 기존 모집단위 삭제 + await prisma.projectDetailRole.deleteMany({ + where: { post_id: projectId }, + }); + + // 새 모집단위 추가 + for (const detail_role of detail_roles) { + const role = await prisma.detailRole.upsert({ + where: { name: detail_role }, + create: { name: detail_role, role_id: saveRoleId }, + update: {}, + }); + + await prisma.projectDetailRole.create({ + data: { + post_id: projectId, + detail_role_id: role.id, + }, + }); + } + }); + + // 결과 반환 + const updatedProject = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + include: { + Tags: { + select: { + tag: { select: { name: true } }, + }, + }, + Details: { + select: { + detail_role: { select: { name: true } }, + }, + }, + }, + }); + + if (!updatedProject) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + + return { + message: '프로젝트가 성공적으로 수정되었습니다.', + project: { + id: updatedProject.id, + title: updatedProject.title, + content: updatedProject.content, + tags: updatedProject.Tags.map(t => t.tag.name), + detail_roles: updatedProject.Details.map(d => d.detail_role.name), + }, + }; + } + + // 게시글 권한 확인 + async feedAuth(userId: number, projectId: number) { + const auth = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + select: { user_id: true }, + }); + + return auth.user_id === userId; + } + + async checkApplyStatus(userId: number, projectId: number) { + const application = await this.prisma.userApplyProject.findUnique({ + where: { + user_id_post_id: { user_id: userId, post_id: projectId }, + }, + select: { + created_at: true, + }, + }); + + if (!application) { + return { status: 'not_applied' }; + } + + return { + status: 'applied', + applied_at: application.created_at, + }; + } + + async cancelApplication(userId: number, projectId: number) { + const application = await this.prisma.userApplyProject.findUnique({ + where: { + user_id_post_id: { user_id: userId, post_id: projectId }, + }, + }); + + if (!application) { + throw new NotFoundException('해당 프로젝트에 지원한 기록이 없습니다.'); + } + + await this.prisma.userApplyProject.delete({ + where: { + user_id_post_id: { user_id: userId, post_id: projectId }, + }, + }); + + return { message: '프로젝트 지원이 취소되었습니다.' }; + } + + async updateApplicationStatus( + userId: number, + projectId: number, + applicationId: number, + status: 'Accepted' | 'Rejected' | 'Pending' + ) { + // 프로젝트 작성자인지 확인 + const project = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + select: { user_id: true }, + }); + + if (!project) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + + if (project.user_id !== userId) { + throw new ForbiddenException('해당 프로젝트의 작성자가 아닙니다.'); + } + + // 지원 상태 업데이트 + const updatedApplication = await this.prisma.userApplyProject.update({ + where: { id: applicationId }, + data: { status }, + }); + + return { + message: '지원 상태가 변경되었습니다.', + application: { + id: updatedApplication.id, + status: updatedApplication.status, + }, + }; + } + + async updateProjectStatus( + userId: number, + projectId: number, + recruiting: boolean + ) { + // 프로젝트 작성자인지 확인 + const project = await this.prisma.projectPost.findUnique({ + where: { id: projectId }, + select: { user_id: true }, + }); + + if (!project) { + throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); + } + + if (project.user_id !== userId) { + throw new ForbiddenException('해당 프로젝트의 작성자가 아닙니다.'); + } + + // 프로젝트 상태 업데이트 + const updatedProject = await this.prisma.projectPost.update({ + where: { id: projectId }, + data: { recruiting }, + }); + + return { + message: '프로젝트 상태가 변경되었습니다.', + project: { + id: updatedProject.id, + recruiting: updatedProject.recruiting, + }, + }; + } + + async deleteProject(userId: number, projectId: number) { + const auth = await this.feedAuth(userId, projectId); + if (!auth) { + throw new HttpException('권한이 없습니다.', HttpStatus.FORBIDDEN); + } + await this.prisma.$transaction([ + this.prisma.projectDetailRole.deleteMany({ + where: { post_id: projectId }, + }), + + this.prisma.projectPostTag.deleteMany({ + where: { post_id: projectId }, + }), + + this.prisma.projectSave.deleteMany({ + where: { post_id: projectId }, + }), + + this.prisma.projectPost.delete({ + where: { id: projectId }, + }), + ]); + + return { message: { code: 200, text: '피드가 삭제되었습니다.' } }; + } + + async toggleBookmark(userId: number, projectId: number) { + // 북마크 존재 여부 확인 + const existingBookmark = await this.prisma.projectSave.findFirst({ + where: { user_id: userId, post_id: projectId }, + }); + + if (existingBookmark) { + // 북마크 삭제 + await this.prisma.projectSave.delete({ + where: { id: existingBookmark.id }, + }); + + return { + message: '북마크가 삭제되었습니다.', + bookmarked: false, + }; + } + + // 북마크 추가 + await this.prisma.projectSave.create({ + data: { + user_id: userId, + post_id: projectId, + }, + }); + + return { + message: '북마크가 추가되었습니다.', + bookmarked: true, + }; + } + + async checkBookmark(userId: number, projectId: number) { + // 북마크 여부 확인 + const bookmark = await this.prisma.projectSave.findFirst({ + where: { user_id: userId, post_id: projectId }, + }); + + return { + bookmarked: !!bookmark, + }; + } } From ecc7e88346bf21562e50c994e89b834a58a4fe6c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 15:41:32 +0900 Subject: [PATCH 256/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 67 +++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5c218dd..c3bc582 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -227,41 +227,34 @@ model ProjectPost { id Int @id @default(autoincrement()) user_id Int title String - content String - thumbnail_url String - role Int - unit String + content String @db.Text + thumbnail_url String? + role String start_date DateTime - end_date DateTime - work_type_id Int - recruiting Boolean - applicant_count Int - view Int - saved_count Int + recruiting Boolean @default(true) + applicant_count Int @default(0) + view Int @default(0) + saved_count Int @default(0) created_at DateTime @default(now()) + duration String + hub_type String + work_type String Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) - work_type WorkType @relation(fields: [work_type_id], references: [id]) Tags ProjectPostTag[] Saves ProjectSave[] Applications UserApplyProject[] @@index([user_id], map: "ProjectPost_user_id_fkey") - @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") -} - -model WorkType { - id Int @id @default(autoincrement()) - name String - ProjectPosts ProjectPost[] } model ProjectSave { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([post_id], map: "ProjectSave_post_id_fkey") @@index([user_id], map: "ProjectSave_user_id_fkey") @@ -281,34 +274,38 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String + name String @unique Tags ProjectPostTag[] } model ProjectPostTag { - id Int @id @default(autoincrement()) - post_id Int - tag_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - tag ProjectTag @relation(fields: [tag_id], references: [id]) + id Int @id @default(autoincrement()) + post_id Int + tag_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + tag ProjectTag @relation(fields: [tag_id], references: [id]) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") } model UserApplyProject { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + status String @default("Pending") + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + @@unique([user_id, post_id]) @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From 5f88e087238d731835773cb37c5c58ee7f7c63e6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Fri, 24 Jan 2025 15:53:00 +0900 Subject: [PATCH 257/414] =?UTF-8?q?[Feat]=20DB=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3bc582..d78617c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -229,16 +229,16 @@ model ProjectPost { title String content String @db.Text thumbnail_url String? - role String + role String //[Programmer, Artist, Designer] + hub_type String //[Project, 아웃소싱] start_date DateTime + duration String + work_type String //[Online, Offline] recruiting Boolean @default(true) applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) created_at DateTime @default(now()) - duration String - hub_type String - work_type String Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) Tags ProjectPostTag[] @@ -249,12 +249,12 @@ model ProjectPost { } model ProjectSave { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - created_at DateTime @default(now()) - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + created_at DateTime @default(now()) // 지원 생성 시간 + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([post_id], map: "ProjectSave_post_id_fkey") @@index([user_id], map: "ProjectSave_user_id_fkey") @@ -274,13 +274,13 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String @unique + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String @unique + name String @unique Tags ProjectPostTag[] } @@ -288,9 +288,9 @@ model ProjectPostTag { id Int @id @default(autoincrement()) post_id Int tag_id Int - created_at DateTime @default(now()) post ProjectPost @relation(fields: [post_id], references: [id]) tag ProjectTag @relation(fields: [tag_id], references: [id]) + created_at DateTime @default(now()) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") @@ -300,12 +300,12 @@ model UserApplyProject { id Int @id @default(autoincrement()) user_id Int post_id Int - status String @default("Pending") - created_at DateTime @default(now()) + status String @default("Pending") // "Accepted", "Rejected", "Pending" + created_at DateTime @default(now()) // 지원 생성 시간 post ProjectPost @relation(fields: [post_id], references: [id]) user User @relation(fields: [user_id], references: [id]) - @@unique([user_id, post_id]) + @@unique([user_id, post_id]) // 중복 지원 방지 @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From 9b4e80988bcf261e68a2504a09321ee3d15d222c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 19:16:51 +0900 Subject: [PATCH 258/414] =?UTF-8?q?[Feat]=20Project=20API=20Swagger=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 127 +++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/modules/project/docs/project.docs.ts diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts new file mode 100644 index 0000000..a6ef3ee --- /dev/null +++ b/src/modules/project/docs/project.docs.ts @@ -0,0 +1,127 @@ +import { ApiOperation, ApiQuery, ApiResponse, ApiBody } from '@nestjs/swagger'; + +export const GetProjectsDocs = { + ApiOperation: ApiOperation({ + summary: '전체 프로젝트 조회', + description: + '서비스에 존재하는 모든 프로젝트를 조회합니다. 다양한 쿼리 옵션을 제공합니다.', + }), + ApiQuerySkip: ApiQuery({ + name: 'skip', + required: false, + description: '건너뛸 데이터 수 (기본값: 0)', + type: Number, + }), + ApiQueryLimit: ApiQuery({ + name: 'limit', + required: false, + description: '가져올 데이터 수 (기본값: 10)', + type: Number, + }), + ApiQueryRole: ApiQuery({ + name: 'role', + required: false, + description: '필터링할 역할 (예: Programmer)', + type: String, + }), + ApiQueryUnit: ApiQuery({ + name: 'unit', + required: false, + description: '모집 단위 (예: React)', + type: String, + }), + ApiQuerySort: ApiQuery({ + name: 'sort', + required: false, + description: '정렬 기준 (latest 또는 popular)', + type: String, + }), + ApiResponseSuccess: ApiResponse({ + status: 200, + description: '프로젝트 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '전체 커넥션허브 조회에 성공했습니다', + }, + projects: [ + { + projectId: 1, + title: '프로젝트 제목', + content: '프로젝트 내용', + thumbnailUrl: 'https://example.com/thumbnail.png', + role: 'Programmer', + tags: ['#React', '#TypeScript'], + hubType: 'Project', + startDate: '2025-01-01', + duration: '3 months', + workType: 'Online', + applyCount: 5, + bookMarkCount: 3, + viewCount: 10, + status: 'OPEN', + user: { + userId: 101, + nickname: 'leechan_dev', + name: 'Lee Chan', + profileUrl: 'https://example.com/profile.png', + role: 'Developer', + }, + }, + ], + page: 1, + limit: 10, + }, + }, + }), +}; + +export const CreateProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 생성', + description: '새로운 프로젝트를 생성합니다.', + }), + ApiBody: ApiBody({ + description: '프로젝트 생성 요청 데이터', + schema: { + example: { + title: '프로젝트 제목', + content: '프로젝트 내용', + role: 'Programmer', + hub_type: 'Project', + start_date: '2025-01-01', + duration: '6 months', + work_type: 'Online', + recruiting: true, + skills: ['React', 'TypeScript'], + detail_roles: ['Frontend Developer', 'Fullstack Developer'], + }, + }, + }), + ApiResponseSuccess: ApiResponse({ + status: 201, + description: '프로젝트 생성 성공', + schema: { + example: { + message: { + code: 201, + text: '프로젝트 생성에 성공했습니다', + }, + project: { + projectId: 1, + title: '프로젝트 제목', + content: '프로젝트 내용', + role: 'Programmer', + hubType: 'Project', + startDate: '2025-01-01', + duration: '6 months', + workType: 'Online', + status: 'OPEN', + tags: ['React', 'TypeScript'], + detailRoles: ['Frontend Developer', 'Fullstack Developer'], + }, + }, + }, + }), +}; From ab54bf1db3aace9e12e891a42695d81f5966cfc6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 19:17:14 +0900 Subject: [PATCH 259/414] =?UTF-8?q?[Fix]=20=EC=9D=91=EB=8B=B5=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 89 +++++++++++++++----------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 8d3603b..e08da07 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -67,20 +67,19 @@ export class ProjectService { const totalCount = await this.prisma.projectPost.count({ where }); const formattedProjects = projects.map(project => ({ - id: project.id, + projectId: project.id, title: project.title, content: project.content, - thumbnail_url: project.thumbnail_url, + thumbnailUrl: project.thumbnail_url, role: project.role, tags: project.Tags.map(tag => `#${tag.tag.name}`), - hub_type: project.hub_type, - start_date: project.start_date.toISOString().split('T')[0], + hubType: project.hub_type, + startDate: project.start_date.toISOString().split('T')[0], duration: project.duration, - work_type: project.work_type, - //recruiting: project.recruiting, + workType: project.work_type, applyCount: project.Applications.length, bookMarkCount: project.saved_count, - view_count: project.view, + viewCount: project.view + 1, status: project.recruiting ? 'OPEN' : 'CLOSED', user: { userId: project.user.id, @@ -92,12 +91,13 @@ export class ProjectService { })); return { - data: formattedProjects, - meta: { - total_count: totalCount, - page: Math.floor(skip / limit) + 1, - limit, + message: { + code: 200, + text: '전체 커넥션허브 조회에 성공했습니다', }, + projects: formattedProjects, + page: Math.floor(skip / limit) + 1, + limit, }; } @@ -181,19 +181,22 @@ export class ProjectService { // 결과 반환 return { - message: '프로젝트 생성 완료', + message: { + code: 201, + text: '프로젝트 생성에 성공했습니다', + }, project: { - id: project.id, + projectId: project.id, title: project.title, content: project.content, role: project.role, - hub_type: project.hub_type, - start_date: project.start_date, + hubType: project.hub_type, + startDate: project.start_date, duration: project.duration, - work_type: project.work_type, + workType: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', tags, - detail_roles: roles, + detailRoles: roles, }, }; } @@ -238,11 +241,10 @@ export class ProjectService { user: { name: project.user.name, nickname: project.user.nickname, - profile_url: project.user.profile_url, + profileUrl: project.user.profile_url, role: project.user.role, }, - hub_type: project.hub_type, - saved_count: project.saved_count, + hubType: project.hub_type, })); } @@ -310,7 +312,7 @@ export class ProjectService { project.user.id === userId ? true : false; // 프로젝트 데이터 정리 return { - id: project.id, + projectId: project.id, title: project.title, content: project.content, role: project.role, @@ -319,14 +321,14 @@ export class ProjectService { duration: project.duration, work_type: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', - tags: project.Tags.map(t => t.tag.name), - detail_roles: project.Details.map(d => d.detail_role.name), + skills: project.Tags.map(t => t.tag.name), + detailRoles: project.Details.map(d => d.detail_role.name), manager: { - id: project.user.id, + userId: project.user.id, name: project.user.name, nickname: project.user.nickname, - profile_url: project.user.profile_url, - introduce: project.user.introduce, + profileUrl: project.user.profile_url, + introduce: project.user.introduce ? project.user.introduce : null, }, isOwnConnectionHub, }; @@ -361,7 +363,12 @@ export class ProjectService { }, }); - return { message: '프로젝트에 지원되었습니다.' }; + return { + message: { + text: '프로젝트에 지원되었습니다.', + code: 200, + }, + }; } async getApplicants(projectId: number) { @@ -389,14 +396,19 @@ export class ProjectService { }, }, }); - - return applicants.map(applicant => ({ - id: applicant.user.id, + const resultapplicants = applicants.map(applicant => ({ + userId: applicant.user.id, name: applicant.user.name, nickname: applicant.user.nickname, - profile_url: applicant.user.profile_url, - introduce: applicant.user.introduce, + profileUrl: applicant.user.profile_url, })); + return { + applicants: resultapplicants, + message: { + code: 200, + text: '프로젝트 지원자 목록 조회에 성공했습니다', + }, + }; } async updateProject( @@ -519,11 +531,11 @@ export class ProjectService { return { message: '프로젝트가 성공적으로 수정되었습니다.', project: { - id: updatedProject.id, + projectId: updatedProject.id, title: updatedProject.title, content: updatedProject.content, - tags: updatedProject.Tags.map(t => t.tag.name), - detail_roles: updatedProject.Details.map(d => d.detail_role.name), + skills: updatedProject.Tags.map(t => t.tag.name), + detailRoles: updatedProject.Details.map(d => d.detail_role.name), }, }; } @@ -641,8 +653,9 @@ export class ProjectService { return { message: '프로젝트 상태가 변경되었습니다.', project: { - id: updatedProject.id, + projectId: updatedProject.id, recruiting: updatedProject.recruiting, + status: recruiting ? 'OPEN' : 'CLOSE', }, }; } @@ -670,7 +683,7 @@ export class ProjectService { }), ]); - return { message: { code: 200, text: '피드가 삭제되었습니다.' } }; + return { message: { code: 200, text: '프로젝트가 삭제되었습니다.' } }; } async toggleBookmark(userId: number, projectId: number) { From 838513bbef118441347a3dfd4f7409a75c851cc0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 22:41:08 +0900 Subject: [PATCH 260/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index dbb843d..0ca5581 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -18,12 +18,20 @@ import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { ProjectService } from '@modules/project/project.service'; import { CreateProjectDto } from './dto/CreateProject.dto'; import { FileInterceptor } from '@nestjs/platform-express'; +import { CreateProjectDocs, DeleteProjectDocs, GetPopularProjectsThisWeekDocs, GetProjectsDocs, UpdateProjectDocs } from './docs/project.docs'; @UseGuards(JwtAuthGuard) @Controller('projects') export class ProjectController { constructor(private readonly projectService: ProjectService) {} @Get() + @GetProjectsDocs.ApiOperation + @GetProjectsDocs.ApiQuerySkip + @GetProjectsDocs.ApiQueryLimit + @GetProjectsDocs.ApiQueryRole + @GetProjectsDocs.ApiQueryUnit + @GetProjectsDocs.ApiQuerySort + @GetProjectsDocs.ApiResponseSuccess async getProjects( @Query('skip') skip: number = 0, @Query('limit') limit: number = 10, @@ -35,12 +43,19 @@ export class ProjectController { } @Post() + @CreateProjectDocs.ApiOperation + @CreateProjectDocs.ApiBody + @CreateProjectDocs.ApiResponseSuccess async createProject(@Body() createProjectDto: CreateProjectDto, @Req() req) { const userId = req.user.user_id; return this.projectService.createProject(createProjectDto, userId); } @Put(':projectId') + @UpdateProjectDocs.ApiOperation + @UpdateProjectDocs.ApiParam + @UpdateProjectDocs.ApiBody + @UpdateProjectDocs.ApiResponse async updateProject( @Param('projectId', ParseIntPipe) projectId: number, @Body() updateProjectDto: CreateProjectDto, @@ -55,6 +70,9 @@ export class ProjectController { } @Delete(':projectId') + @DeleteProjectDocs.ApiOperation + @DeleteProjectDocs.ApiParam + @DeleteProjectDocs.ApiResponse async deleteProject(@Req() req, @Param('projectId') projectId: string) { const userId = req.user.user_id; const numProjectId = parseInt(projectId, 10); @@ -62,6 +80,8 @@ export class ProjectController { } @Get('popular-this-week') + @GetPopularProjectsThisWeekDocs.ApiOperation + @GetPopularProjectsThisWeekDocs.ApiResponse async getPopularProjectsThisWeek() { return this.projectService.getPopularProjectsThisWeek(); } From 8ad2bceddf5f280f9b18f701e89c4a3a81711776 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 22:42:05 +0900 Subject: [PATCH 261/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 0ca5581..d7c512e 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -18,7 +18,7 @@ import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { ProjectService } from '@modules/project/project.service'; import { CreateProjectDto } from './dto/CreateProject.dto'; import { FileInterceptor } from '@nestjs/platform-express'; -import { CreateProjectDocs, DeleteProjectDocs, GetPopularProjectsThisWeekDocs, GetProjectsDocs, UpdateProjectDocs } from './docs/project.docs'; +import { CreateProjectDocs, DeleteProjectDocs, GetPopularProjectsThisWeekDocs, GetProjectDetailDocs, GetProjectsDocs, UpdateProjectDocs, UploadFeedImageDocs } from './docs/project.docs'; @UseGuards(JwtAuthGuard) @Controller('projects') export class ProjectController { @@ -88,12 +88,19 @@ export class ProjectController { @Post('image') @UseInterceptors(FileInterceptor('file')) + @UploadFeedImageDocs.ApiOperation + @UploadFeedImageDocs.ApiConsumes + @UploadFeedImageDocs.ApiBody + @UploadFeedImageDocs.ApiResponse async func(@Req() req, @UploadedFile() file: Express.Multer.File) { const userId = req.user.user_id; return await this.projectService.uploadFeedImage(userId, file); } @Get(':projectId') + @GetProjectDetailDocs.ApiOperation + @GetProjectDetailDocs.ApiParam + @GetProjectDetailDocs.ApiResponse async getProjectDetail(@Req() req, @Param('projectId') projectId: string) { const userId = req.user.user_id; const numProjectId = parseInt(projectId, 10); From 6fd5fce350b6b77056a5221998586512b4b68380 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 22:46:42 +0900 Subject: [PATCH 262/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index d7c512e..27e6c27 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -18,7 +18,19 @@ import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { ProjectService } from '@modules/project/project.service'; import { CreateProjectDto } from './dto/CreateProject.dto'; import { FileInterceptor } from '@nestjs/platform-express'; -import { CreateProjectDocs, DeleteProjectDocs, GetPopularProjectsThisWeekDocs, GetProjectDetailDocs, GetProjectsDocs, UpdateProjectDocs, UploadFeedImageDocs } from './docs/project.docs'; +import { + ApplyToProjectDocs, + CancelApplicationDocs, + CheckApplyStatusDocs, + CreateProjectDocs, + DeleteProjectDocs, + GetApplicantsDocs, + GetPopularProjectsThisWeekDocs, + GetProjectDetailDocs, + GetProjectsDocs, + UpdateProjectDocs, + UploadFeedImageDocs, +} from './docs/project.docs'; @UseGuards(JwtAuthGuard) @Controller('projects') export class ProjectController { @@ -108,6 +120,9 @@ export class ProjectController { } @Post(':projectId/apply') + @ApplyToProjectDocs.ApiOperation + @ApplyToProjectDocs.ApiParam + @ApplyToProjectDocs.ApiResponse async applyToProject( @Param('projectId', ParseIntPipe) projectId: number, @Req() req @@ -117,11 +132,17 @@ export class ProjectController { } @Get(':projectId/applicants') + @GetApplicantsDocs.ApiOperation + @GetApplicantsDocs.ApiParam + @GetApplicantsDocs.ApiResponse async getApplicants(@Param('projectId', ParseIntPipe) projectId: number) { return this.projectService.getApplicants(projectId); } @Get(':projectId/apply-status') + @CheckApplyStatusDocs.ApiOperation + @CheckApplyStatusDocs.ApiParam + @CheckApplyStatusDocs.ApiResponse async checkApplyStatus( @Param('projectId', ParseIntPipe) projectId: number, @Req() req @@ -131,6 +152,9 @@ export class ProjectController { } @Delete(':projectId/cancel-apply') + @CancelApplicationDocs.ApiOperation + @CancelApplicationDocs.ApiParam + @CancelApplicationDocs.ApiResponse async cancelApplication( @Param('projectId', ParseIntPipe) projectId: number, @Req() req From aec0b0c0dfff0f4bd91e7175acad371f03ba5b07 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 22:50:22 +0900 Subject: [PATCH 263/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 27e6c27..40e23e4 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -28,7 +28,9 @@ import { GetPopularProjectsThisWeekDocs, GetProjectDetailDocs, GetProjectsDocs, + UpdateApplicationStatusDocs, UpdateProjectDocs, + UpdateProjectStatusDocs, UploadFeedImageDocs, } from './docs/project.docs'; @UseGuards(JwtAuthGuard) @@ -164,6 +166,11 @@ export class ProjectController { } @Patch(':projectId/applications/:applicationId/status') + @UpdateApplicationStatusDocs.ApiOperation + @UpdateApplicationStatusDocs.ApiParamProject + @UpdateApplicationStatusDocs.ApiParamApplication + @UpdateApplicationStatusDocs.ApiBody + @UpdateApplicationStatusDocs.ApiResponse async updateApplicationStatus( @Param('projectId', ParseIntPipe) projectId: number, @Param('applicationId', ParseIntPipe) applicationId: number, @@ -180,6 +187,10 @@ export class ProjectController { } @Patch(':projectId/status') + @UpdateProjectStatusDocs.ApiOperation + @UpdateProjectStatusDocs.ApiParam + @UpdateProjectStatusDocs.ApiBody + @UpdateProjectStatusDocs.ApiResponse async updateProjectStatus( @Param('projectId', ParseIntPipe) projectId: number, @Body('recruiting') recruiting: boolean, From 4e48fc20c81c3d56d47123dbb53d16aca36709b5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 24 Jan 2025 22:52:41 +0900 Subject: [PATCH 264/414] =?UTF-8?q?[Feat]=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 40e23e4..5930914 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -22,12 +22,14 @@ import { ApplyToProjectDocs, CancelApplicationDocs, CheckApplyStatusDocs, + CheckBookmarkDocs, CreateProjectDocs, DeleteProjectDocs, GetApplicantsDocs, GetPopularProjectsThisWeekDocs, GetProjectDetailDocs, GetProjectsDocs, + ToggleBookmarkDocs, UpdateApplicationStatusDocs, UpdateProjectDocs, UpdateProjectStatusDocs, @@ -205,6 +207,9 @@ export class ProjectController { } @Post(':projectId/bookmark') + @ToggleBookmarkDocs.ApiOperation + @ToggleBookmarkDocs.ApiParam + @ToggleBookmarkDocs.ApiResponse async toggleBookmark( @Param('projectId', ParseIntPipe) projectId: number, @Req() req @@ -214,6 +219,9 @@ export class ProjectController { } @Get(':projectId/bookmark') + @CheckBookmarkDocs.ApiOperation + @CheckBookmarkDocs.ApiParam + @CheckBookmarkDocs.ApiResponse async checkBookmark( @Param('projectId', ParseIntPipe) projectId: number, @Req() req From f4d288dd9a5422633458187e5f6eacd73ab45a0f Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:10:11 +0900 Subject: [PATCH 265/414] =?UTF-8?q?[Feat]=20Project=20API=20Swagger=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 446 ++++++++++++++++++++++- 1 file changed, 437 insertions(+), 9 deletions(-) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index a6ef3ee..10139c2 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -1,4 +1,11 @@ -import { ApiOperation, ApiQuery, ApiResponse, ApiBody } from '@nestjs/swagger'; +import { + ApiOperation, + ApiQuery, + ApiResponse, + ApiBody, + ApiParam, + ApiConsumes, +} from '@nestjs/swagger'; export const GetProjectsDocs = { ApiOperation: ApiOperation({ @@ -52,11 +59,12 @@ export const GetProjectsDocs = { content: '프로젝트 내용', thumbnailUrl: 'https://example.com/thumbnail.png', role: 'Programmer', - tags: ['#React', '#TypeScript'], - hubType: 'Project', + skills: ['React', 'TypeScript'], + detailRoles: ['웹 프로그래밍 개발자', '백엔드 개발자'], + hubType: 'PROJECT', startDate: '2025-01-01', duration: '3 months', - workType: 'Online', + workType: 'ONLINE', applyCount: 5, bookMarkCount: 3, viewCount: 10, @@ -89,10 +97,10 @@ export const CreateProjectDocs = { title: '프로젝트 제목', content: '프로젝트 내용', role: 'Programmer', - hub_type: 'Project', + hub_type: 'PROJECT', start_date: '2025-01-01', duration: '6 months', - work_type: 'Online', + work_type: 'ONLINE', recruiting: true, skills: ['React', 'TypeScript'], detail_roles: ['Frontend Developer', 'Fullstack Developer'], @@ -112,16 +120,436 @@ export const CreateProjectDocs = { projectId: 1, title: '프로젝트 제목', content: '프로젝트 내용', + thumbnailUrl: 'thumbNail Photo url', role: 'Programmer', - hubType: 'Project', + hubType: 'PROJECT', startDate: '2025-01-01', duration: '6 months', - workType: 'Online', + workType: 'ONLINE', status: 'OPEN', - tags: ['React', 'TypeScript'], + viewCount: 0, + skills: ['React', 'TypeScript'], detailRoles: ['Frontend Developer', 'Fullstack Developer'], }, }, }, }), }; + +export const UpdateProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 수정', + description: '특정 프로젝트의 내용을 수정합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '수정하려는 프로젝트의 ID', + type: Number, + }), + ApiBody: ApiBody({ + description: '수정할 프로젝트 데이터', + schema: { + example: { + title: '수정된 프로젝트 제목', + content: '수정된 프로젝트 내용', + role: 'Programmer', + hub_type: 'PROJECT', + start_date: '2025-01-01', + duration: '6 months', + work_type: 'ONLINE', + recruiting: true, + skills: ['React', 'TypeScript'], + detail_roles: ['Frontend Developer', 'Fullstack Developer'], + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 수정 성공', + schema: { + example: { + message: { + code: 200, + text: '프로젝트가 성공적으로 수정되었습니다.', + }, + project: { + projectId: 1, + title: '수정된 프로젝트 제목', + content: '수정된 프로젝트 내용', + role: 'Programmer', + hubType: 'PROJECT', + thumbnailUrl: 'thumbnail URL', + startDate: '2025-01-23', + duration: '6 months', + workType: 'ONLINE', + skills: ['React', 'TypeScript'], + detailRoles: ['Frontend Developer', 'Fullstack Developer'], + }, + }, + }, + }), +}; + +export const DeleteProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 삭제', + description: '특정 프로젝트를 삭제합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '삭제하려는 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 삭제 성공', + schema: { + example: { + message: { code: 200, text: '프로젝트가 삭제되었습니다.' }, + }, + }, + }), +}; + +export const GetPopularProjectsThisWeekDocs = { + ApiOperation: ApiOperation({ + summary: '인기 프로젝트 조회 (이번 주)', + description: '이번 주 가장 인기 있는 프로젝트를 조회합니다.', + }), + ApiResponse: ApiResponse({ + status: 200, + description: '인기 프로젝트 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '인기 프로젝트 조회에 성공했습니다', + }, + popularProjects: [ + { + projectId: 1, + title: '프로젝트 제목', + user: { + userId: 1, + name: 'Lee Chan', + nickname: 'leechan_dev', + profileUrl: 'https://example.com/profile.png', + role: 'Developer', + }, + hubType: 'PROJECT', + }, + { + projectId: 2, + title: '프로젝트 제목 2', + user: { + userId: 2, + name: 'Kim Min', + nickname: 'min_kim', + profileUrl: 'https://example.com/profile2.png', + role: 'Designer', + }, + hubType: 'OUTSOURCING', + }, + ], + }, + }, + }), +}; + +export const UploadFeedImageDocs = { + ApiOperation: ApiOperation({ + summary: '이미지 업로드', + description: '프로젝트 관련 이미지를 업로드합니다.', + }), + ApiConsumes: ApiConsumes('multipart/form-data'), + ApiBody: ApiBody({ + description: '이미지 파일 업로드', + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '이미지 업로드 성공', + schema: { + example: { + imageUrl: 'https://example.com/uploads/image.png', + message: { + code: 200, + text: '이미지 업로드가 완료되었습니다.', + }, + }, + }, + }), +}; + +export const GetProjectDetailDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 상세 조회', + description: '특정 프로젝트의 상세 정보를 조회합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '조회하려는 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 상세 조회 성공', + schema: { + example: { + message: { + code: 200, + text: '프로젝트 상세 조회에 성공했습니다', + }, + projectId: 1, + title: '프로젝트 제목', + content: '프로젝트 상세 내용', + role: 'Programmer', + hubType: 'PROJECT', + startDate: '2025-01-01T00:00:00Z', + duration: '6 months', + workType: 'ONLINE', + status: 'OPEN', + skills: ['React', 'TypeScript'], + detailRoles: ['Frontend Developer', 'Backend Developer'], + manager: { + userId: 101, + name: 'Lee Chan', + nickname: 'leechan_dev', + profileUrl: 'https://example.com/profile.png', + introduce: '프론트엔드 개발자입니다.', + }, + isOwnConnectionHub: true, + }, + }, + }), +}; + +export const ApplyToProjectDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 지원', + description: '특정 프로젝트에 사용자가 지원합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '지원할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 지원 성공', + schema: { + example: { + message: { + text: '프로젝트에 지원되었습니다.', + code: 200, + }, + isApply: true + }, + }, + }), +}; + +export const GetApplicantsDocs = { + ApiOperation: ApiOperation({ + summary: '지원자 목록 조회', + description: '특정 프로젝트의 지원자 목록을 조회합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '지원자를 조회할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '지원자 목록 조회 성공', + schema: { + example: { + applicants: [ + { + userId: 1, + name: 'Lee Chan', + nickname: 'leechan_dev', + profileUrl: 'https://example.com/profile.png', + }, + ], + message: { + code: 200, + text: '프로젝트 지원자 목록 조회에 성공했습니다', + }, + }, + }, + }), +}; + +export const CheckApplyStatusDocs = { + ApiOperation: ApiOperation({ + summary: '지원 상태 확인', + description: '사용자가 특정 프로젝트에 지원했는지 확인합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '지원 상태를 확인할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '지원 상태 확인 성공', + schema: { + example: { + applied: true, + message: '해당 프로젝트에 이미 지원했습니다.', + }, + }, + }), +}; + +export const CancelApplicationDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 지원 취소', + description: '사용자가 특정 프로젝트에 대한 지원을 취소합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '지원 취소할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 지원 취소 성공', + schema: { + example: { + message: '프로젝트 지원이 취소되었습니다.', + }, + }, + }), +}; + +export const UpdateApplicationStatusDocs = { + ApiOperation: ApiOperation({ + summary: '지원 상태 변경', + description: '프로젝트 작성자가 특정 지원자의 지원 상태를 변경합니다.', + }), + ApiParamProject: ApiParam({ + name: 'projectId', + description: '변경할 지원 상태가 속한 프로젝트 ID', + type: Number, + }), + ApiParamApplication: ApiParam({ + name: 'userId', + description: '지원자의 userId', + type: Number, + }), + ApiBody: ApiBody({ + description: '지원 상태 변경 요청 데이터 (Accepted, Rejected, Pending)', + schema: { + example: { + status: 'Accepted', // 또는 'Rejected', 'Pending' + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '지원 상태 변경 성공', + schema: { + example: { + message: '지원 상태가 변경되었습니다.', + application: { + applicationId: 10, + status: 'Accepted', + }, + }, + }, + }), +}; + +export const UpdateProjectStatusDocs = { + ApiOperation: ApiOperation({ + summary: '프로젝트 상태 변경', + description: '프로젝트 작성자가 프로젝트 상태를 변경합니다 (OPEN / CLOSE).', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '상태를 변경할 프로젝트 ID', + type: Number, + }), + ApiBody: ApiBody({ + description: '프로젝트 상태 변경 요청 데이터 true: OPEN, false: CLOSE', + schema: { + example: { + recruiting: true, + }, + }, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '프로젝트 상태 변경 성공', + schema: { + example: { + message: '프로젝트 상태가 변경되었습니다.', + project: { + projectId: 1, + recruiting: true, + status: 'OPEN', + }, + }, + }, + }), +}; + +export const ToggleBookmarkDocs = { + ApiOperation: ApiOperation({ + summary: '북마크 추가/삭제', + description: '특정 프로젝트에 북마크를 추가하거나 삭제합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '북마크를 추가하거나 삭제할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '북마크 상태 변경 성공 or 실패 bookmarked: true => 북마크 추가됨 false => 북마크 삭제됨', + schema: { + example: { + message: { + code: 200, + text: '북마크가 추가되었습니다.', // 또는 '북마크가 삭제되었습니다.' + }, + bookmarked: true, // true: 북마크 추가됨, false: 북마크 삭제됨 + }, + }, + }), +}; + +export const CheckBookmarkDocs = { + ApiOperation: ApiOperation({ + summary: '북마크 여부 확인', + description: '특정 프로젝트에 북마크가 되어 있는지 여부를 확인합니다.', + }), + ApiParam: ApiParam({ + name: 'projectId', + description: '북마크 여부를 확인할 프로젝트의 ID', + type: Number, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '북마크 여부 확인 성공 or 실패', + schema: { + example: { + message: { + code: 200, + text: '북마크 여부 확인 성공.', + }, + bookmarked: true, // 또는 false + }, + }, + }), +}; From de8a229a4a8c36184aad4aa01c86822c6f80c83f Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:10:39 +0900 Subject: [PATCH 266/414] =?UTF-8?q?[Fix]=20Project=20API=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 84 +++++++++++++++++++++----- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index e08da07..bf63aae 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -34,9 +34,9 @@ export class ProjectService { const orderBy: any[] = []; if (sort === 'latest') { - orderBy.push({ created_at: 'desc' }); + orderBy.push({ created_at: 'desc' }); // 최신순 } else if (sort === 'popular') { - orderBy.push({ view: 'desc' }); + orderBy.push({ saved_count: 'desc' }); // 북마크 수 기준 정렬 } const projects = await this.prisma.projectPost.findMany({ @@ -47,6 +47,7 @@ export class ProjectService { include: { Tags: { select: { tag: { select: { name: true } } } }, Applications: { select: { id: true } }, + Details: { select: { detail_role: { select: { name: true } } } }, user: { select: { id: true, @@ -72,7 +73,8 @@ export class ProjectService { content: project.content, thumbnailUrl: project.thumbnail_url, role: project.role, - tags: project.Tags.map(tag => `#${tag.tag.name}`), + skills: project.Tags.map(tag => `${tag.tag.name}`), + detailRoles: project.Details.map(d => `${d.detail_role.name}`), hubType: project.hub_type, startDate: project.start_date.toISOString().split('T')[0], duration: project.duration, @@ -189,13 +191,15 @@ export class ProjectService { projectId: project.id, title: project.title, content: project.content, + thumbnailUrl: project.thumbnail_url, role: project.role, hubType: project.hub_type, startDate: project.start_date, duration: project.duration, workType: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', - tags, + viewCount : project.view, + skills: tags, detailRoles: roles, }, }; @@ -221,6 +225,7 @@ export class ProjectService { include: { user: { select: { + id: true, name: true, nickname: true, profile_url: true, @@ -234,11 +239,11 @@ export class ProjectService { take: 5, // 상위 5개 }); - // 반환 데이터 가공 - return popularProjects.map(project => ({ + const results = popularProjects.map(project => ({ projectId: project.id, title: project.title, user: { + userId: project.user.id, name: project.user.name, nickname: project.user.nickname, profileUrl: project.user.profile_url, @@ -246,6 +251,14 @@ export class ProjectService { }, hubType: project.hub_type, })); + // 반환 데이터 가공 + return { + message: { + code: 200, + text: '인기 프로젝트 조회에 성공했습니다', + }, + popularProjects: results, + }; } async uploadFeedImage(userId: number, file: Express.Multer.File) { @@ -312,14 +325,18 @@ export class ProjectService { project.user.id === userId ? true : false; // 프로젝트 데이터 정리 return { + message: { + code: 200, + text: '프로젝트 상세 조회에 성공했습니다', + }, projectId: project.id, title: project.title, content: project.content, role: project.role, - hub_type: project.hub_type, - start_date: project.start_date, + hubType: project.hub_type, + startDate: project.start_date, duration: project.duration, - work_type: project.work_type, + workType: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', skills: project.Tags.map(t => t.tag.name), detailRoles: project.Details.map(d => d.detail_role.name), @@ -368,6 +385,7 @@ export class ProjectService { text: '프로젝트에 지원되었습니다.', code: 200, }, + isApply: true, }; } @@ -529,11 +547,17 @@ export class ProjectService { } return { - message: '프로젝트가 성공적으로 수정되었습니다.', + message: { code: 200, text: '프로젝트가 성공적으로 수정되었습니다.' }, project: { projectId: updatedProject.id, title: updatedProject.title, content: updatedProject.content, + role: updatedProject.role, + hubType: updatedProject.hub_type, + thumbnailUrl: updatedProject.thumbnail_url, + startDate: updatedProject.start_date, + duration: updatedProject.duration, + workType: updatedProject.work_type, skills: updatedProject.Tags.map(t => t.tag.name), detailRoles: updatedProject.Details.map(d => d.detail_role.name), }, @@ -587,13 +611,18 @@ export class ProjectService { }, }); - return { message: '프로젝트 지원이 취소되었습니다.' }; + return { + message: { + code: 200, + text: '프로젝트 지원이 취소되었습니다.', + }, + }; } async updateApplicationStatus( userId: number, projectId: number, - applicationId: number, + targetUserId: number, // 지원자의 userId를 기반으로 업데이트 status: 'Accepted' | 'Rejected' | 'Pending' ) { // 프로젝트 작성자인지 확인 @@ -610,16 +639,29 @@ export class ProjectService { throw new ForbiddenException('해당 프로젝트의 작성자가 아닙니다.'); } + // 지원 정보 가져오기 + const application = await this.prisma.userApplyProject.findUnique({ + where: { + user_id_post_id: { user_id: targetUserId, post_id: projectId }, + }, + }); + + if (!application) { + throw new NotFoundException( + '해당 사용자가 프로젝트에 지원한 기록이 없습니다.' + ); + } + // 지원 상태 업데이트 const updatedApplication = await this.prisma.userApplyProject.update({ - where: { id: applicationId }, + where: { id: application.id }, data: { status }, }); return { message: '지원 상태가 변경되었습니다.', application: { - id: updatedApplication.id, + applicationId: updatedApplication.id, status: updatedApplication.status, }, }; @@ -699,7 +741,10 @@ export class ProjectService { }); return { - message: '북마크가 삭제되었습니다.', + message: { + code: 200, + text: '북마크가 삭제되었습니다.', + }, bookmarked: false, }; } @@ -713,7 +758,10 @@ export class ProjectService { }); return { - message: '북마크가 추가되었습니다.', + message: { + code: 200, + text: '북마크가 추가되었습니다.', + }, bookmarked: true, }; } @@ -725,6 +773,10 @@ export class ProjectService { }); return { + message: { + code: 200, + text: '북마크 여부 확인 성공.', + }, bookmarked: !!bookmark, }; } From 933b8b4ac20b74f704b756ada457a9cb045d09e5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:10:51 +0900 Subject: [PATCH 267/414] =?UTF-8?q?[Fix]=20Project=20API=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 5930914..57052fc 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -167,7 +167,7 @@ export class ProjectController { return this.projectService.cancelApplication(userId, projectId); } - @Patch(':projectId/applications/:applicationId/status') + @Patch(':projectId/applications/:userId/status') @UpdateApplicationStatusDocs.ApiOperation @UpdateApplicationStatusDocs.ApiParamProject @UpdateApplicationStatusDocs.ApiParamApplication @@ -175,7 +175,7 @@ export class ProjectController { @UpdateApplicationStatusDocs.ApiResponse async updateApplicationStatus( @Param('projectId', ParseIntPipe) projectId: number, - @Param('applicationId', ParseIntPipe) applicationId: number, + @Param('userId', ParseIntPipe) targetUserId: number, // 지원자의 userId를 받음 @Body('status') status: 'Accepted' | 'Rejected' | 'Pending', @Req() req ) { @@ -183,7 +183,7 @@ export class ProjectController { return this.projectService.updateApplicationStatus( userId, projectId, - applicationId, + targetUserId, status ); } From 7ed3693b2449ab53ea65a91b62411d9c89b361f2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:32:27 +0900 Subject: [PATCH 268/414] [Add] Package Update --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 789063d..20874d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6137,6 +6137,7 @@ "type": "github", "url": "https://github.com/sponsors/kossnocorp" } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", From de0acad37085e0e6e34e6bb64ad0c755dfb033d2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:33:20 +0900 Subject: [PATCH 269/414] =?UTF-8?q?[Fix]=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B0=B0=ED=8F=AC=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.service.ts | 4 ++-- src/modules/auth/strategies/github.strategy.ts | 2 +- src/modules/auth/strategies/google.strategy.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 3ed4d54..2964f65 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -24,7 +24,7 @@ export class AuthService { code, client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, + redirect_uri: process.env.GOOGLE_CALLBACK_DEPLOY_URL, grant_type: 'authorization_code', } ); @@ -80,7 +80,7 @@ export class AuthService { code, client_id: process.env.GITHUB_CLIENT_ID, client_secret: process.env.GITHUB_CLIENT_SECRET, - redirect_uri: process.env.GITHUB_CALLBACK_URL, + redirect_uri: process.env.GITHUB_CALLBACK_DEPLOY_URL, }, { headers: { Accept: 'application/json' }, diff --git a/src/modules/auth/strategies/github.strategy.ts b/src/modules/auth/strategies/github.strategy.ts index 31cadc3..98f12fe 100644 --- a/src/modules/auth/strategies/github.strategy.ts +++ b/src/modules/auth/strategies/github.strategy.ts @@ -9,7 +9,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { super({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: 'http://localhost:5173/auth/github/callback', + callbackURL: process.env.GITHUB_CALLBACK_DEPLOY_URL, scope: ['user:email'], }); } diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts index 2b7a85b..8a8d799 100644 --- a/src/modules/auth/strategies/google.strategy.ts +++ b/src/modules/auth/strategies/google.strategy.ts @@ -9,7 +9,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: 'http://localhost:5173/auth/google/callback', + callbackURL: process.env.GOOGLE_CALLBACK_DEPLOY_URL, scope: ['email', 'profile'], }); } From 5080e898c40014562bed52e3438ed90ca472e12c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:34:43 +0900 Subject: [PATCH 270/414] =?UTF-8?q?[Fix]=20db=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d78617c..c3bc582 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -229,16 +229,16 @@ model ProjectPost { title String content String @db.Text thumbnail_url String? - role String //[Programmer, Artist, Designer] - hub_type String //[Project, 아웃소싱] + role String start_date DateTime - duration String - work_type String //[Online, Offline] recruiting Boolean @default(true) applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) created_at DateTime @default(now()) + duration String + hub_type String + work_type String Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) Tags ProjectPostTag[] @@ -249,12 +249,12 @@ model ProjectPost { } model ProjectSave { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - created_at DateTime @default(now()) // 지원 생성 시간 - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([post_id], map: "ProjectSave_post_id_fkey") @@index([user_id], map: "ProjectSave_user_id_fkey") @@ -274,13 +274,13 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String @unique + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String @unique + name String @unique Tags ProjectPostTag[] } @@ -288,9 +288,9 @@ model ProjectPostTag { id Int @id @default(autoincrement()) post_id Int tag_id Int + created_at DateTime @default(now()) post ProjectPost @relation(fields: [post_id], references: [id]) tag ProjectTag @relation(fields: [tag_id], references: [id]) - created_at DateTime @default(now()) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") @@ -300,12 +300,12 @@ model UserApplyProject { id Int @id @default(autoincrement()) user_id Int post_id Int - status String @default("Pending") // "Accepted", "Rejected", "Pending" - created_at DateTime @default(now()) // 지원 생성 시간 + status String @default("Pending") + created_at DateTime @default(now()) post ProjectPost @relation(fields: [post_id], references: [id]) user User @relation(fields: [user_id], references: [id]) - @@unique([user_id, post_id]) // 중복 지원 방지 + @@unique([user_id, post_id]) @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From 3e52ee38519a6546ce89dcb6acf4049b5211e040 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:45:33 +0900 Subject: [PATCH 271/414] =?UTF-8?q?[Feat]=20=EC=83=81=EC=84=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=A6=9D=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 8 +++++--- src/modules/project/project.service.ts | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index 10139c2..6e2029a 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -319,6 +319,7 @@ export const GetProjectDetailDocs = { status: 'OPEN', skills: ['React', 'TypeScript'], detailRoles: ['Frontend Developer', 'Backend Developer'], + viewCount: 2, manager: { userId: 101, name: 'Lee Chan', @@ -351,7 +352,7 @@ export const ApplyToProjectDocs = { text: '프로젝트에 지원되었습니다.', code: 200, }, - isApply: true + isApply: true, }, }, }), @@ -484,7 +485,7 @@ export const UpdateProjectStatusDocs = { description: '프로젝트 상태 변경 요청 데이터 true: OPEN, false: CLOSE', schema: { example: { - recruiting: true, + recruiting: true, }, }, }), @@ -516,7 +517,8 @@ export const ToggleBookmarkDocs = { }), ApiResponse: ApiResponse({ status: 200, - description: '북마크 상태 변경 성공 or 실패 bookmarked: true => 북마크 추가됨 false => 북마크 삭제됨', + description: + '북마크 상태 변경 성공 or 실패 bookmarked: true => 북마크 추가됨 false => 북마크 삭제됨', schema: { example: { message: { diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index bf63aae..b40caf6 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -198,7 +198,7 @@ export class ProjectService { duration: project.duration, workType: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', - viewCount : project.view, + viewCount: project.view, skills: tags, detailRoles: roles, }, @@ -288,6 +288,16 @@ export class ProjectService { } async getProjectDetail(userId: number, numProjectId: number) { + // 조회수 증가 + await this.prisma.projectPost.update({ + where: { id: numProjectId }, + data: { + view: { + increment: 1, // view 값을 1 증가 + }, + }, + }); + // 프로젝트 상세 정보 조회 const project = await this.prisma.projectPost.findUnique({ where: { id: numProjectId }, @@ -321,9 +331,11 @@ export class ProjectService { if (!project) { throw new NotFoundException('프로젝트를 찾을 수 없습니다.'); } - let isOwnConnectionHub = true; - project.user.id === userId ? true : false; - // 프로젝트 데이터 정리 + + // 사용자가 작성자인지 여부 확인 + const isOwnConnectionHub = project.user.id === userId; + + // 데이터 반환 return { message: { code: 200, @@ -340,6 +352,7 @@ export class ProjectService { status: project.recruiting ? 'OPEN' : 'CLOSE', skills: project.Tags.map(t => t.tag.name), detailRoles: project.Details.map(d => d.detail_role.name), + viewCount: project.view, // 이미 증가된 view 값을 사용 manager: { userId: project.user.id, name: project.user.name, From ce6f5d1b5eea50f38cc6faa464298c2cd93d5cdf Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 00:58:09 +0900 Subject: [PATCH 272/414] =?UTF-8?q?[Fix]=20CORS=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 49378e5..883d364 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); app.enableCors({ - origin: ['http://localhost:5173', 'http://localhost:8080'], + origin: ['http://localhost:5173', 'http://localhost:8080', 'https://p-a-d.store'], credentials: true, exposedHeaders: ['Authorization'], }); From 77e53af29ed4db99e18f49df3b01c1ae80237ec0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 01:31:20 +0900 Subject: [PATCH 273/414] =?UTF-8?q?[Fix]=20=EC=9D=B8=EC=A6=9D=EC=9D=B4=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20API=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 57052fc..5c6c896 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -35,7 +35,8 @@ import { UpdateProjectStatusDocs, UploadFeedImageDocs, } from './docs/project.docs'; -@UseGuards(JwtAuthGuard) +import { ApiBearerAuth } from '@nestjs/swagger' + @Controller('projects') export class ProjectController { constructor(private readonly projectService: ProjectService) {} @@ -59,6 +60,8 @@ export class ProjectController { } @Post() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @CreateProjectDocs.ApiOperation @CreateProjectDocs.ApiBody @CreateProjectDocs.ApiResponseSuccess @@ -68,6 +71,8 @@ export class ProjectController { } @Put(':projectId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @UpdateProjectDocs.ApiOperation @UpdateProjectDocs.ApiParam @UpdateProjectDocs.ApiBody @@ -86,6 +91,8 @@ export class ProjectController { } @Delete(':projectId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @DeleteProjectDocs.ApiOperation @DeleteProjectDocs.ApiParam @DeleteProjectDocs.ApiResponse @@ -114,6 +121,8 @@ export class ProjectController { } @Get(':projectId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @GetProjectDetailDocs.ApiOperation @GetProjectDetailDocs.ApiParam @GetProjectDetailDocs.ApiResponse @@ -124,6 +133,8 @@ export class ProjectController { } @Post(':projectId/apply') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @ApplyToProjectDocs.ApiOperation @ApplyToProjectDocs.ApiParam @ApplyToProjectDocs.ApiResponse @@ -136,6 +147,8 @@ export class ProjectController { } @Get(':projectId/applicants') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @GetApplicantsDocs.ApiOperation @GetApplicantsDocs.ApiParam @GetApplicantsDocs.ApiResponse @@ -144,6 +157,8 @@ export class ProjectController { } @Get(':projectId/apply-status') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @CheckApplyStatusDocs.ApiOperation @CheckApplyStatusDocs.ApiParam @CheckApplyStatusDocs.ApiResponse @@ -156,6 +171,8 @@ export class ProjectController { } @Delete(':projectId/cancel-apply') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @CancelApplicationDocs.ApiOperation @CancelApplicationDocs.ApiParam @CancelApplicationDocs.ApiResponse @@ -168,6 +185,8 @@ export class ProjectController { } @Patch(':projectId/applications/:userId/status') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @UpdateApplicationStatusDocs.ApiOperation @UpdateApplicationStatusDocs.ApiParamProject @UpdateApplicationStatusDocs.ApiParamApplication @@ -189,6 +208,8 @@ export class ProjectController { } @Patch(':projectId/status') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @UpdateProjectStatusDocs.ApiOperation @UpdateProjectStatusDocs.ApiParam @UpdateProjectStatusDocs.ApiBody @@ -207,6 +228,8 @@ export class ProjectController { } @Post(':projectId/bookmark') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @ToggleBookmarkDocs.ApiOperation @ToggleBookmarkDocs.ApiParam @ToggleBookmarkDocs.ApiResponse @@ -219,6 +242,8 @@ export class ProjectController { } @Get(':projectId/bookmark') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @CheckBookmarkDocs.ApiOperation @CheckBookmarkDocs.ApiParam @CheckBookmarkDocs.ApiResponse From 3d0860003a33dbacddda8329c50e448e349fa9ef Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 02:00:33 +0900 Subject: [PATCH 274/414] =?UTF-8?q?[Feat]=20=EC=9D=BC=EB=B0=98=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=EC=8B=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=B4=20=EC=A4=91=EB=B3=B5=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.service.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 2964f65..47d3999 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -182,19 +182,36 @@ export class AuthService { // 회원가입 로직 async signup(email: string, nickname: string, password: string) { // 이메일 중복 확인 - const existingUser = await this.prisma.user.findUnique({ + const existingUserByEmail = await this.prisma.user.findUnique({ where: { email }, }); - if (existingUser) { + if (existingUserByEmail) { throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); } + // 닉네임 중복 확인 및 처리 + let uniqueNickname = nickname; + let isNicknameUnique = false; + + while (!isNicknameUnique) { + const existingUserByNickname = await this.prisma.user.findUnique({ + where: { nickname: uniqueNickname }, + }); + + if (!existingUserByNickname) { + isNicknameUnique = true; // 중복되지 않은 닉네임 + } else { + // 닉네임 뒤에 랜덤 문자열 추가 + uniqueNickname = `${nickname}_${Math.floor(1000 + Math.random() * 9000)}`; // 랜덤 4자리 숫자 추가 + } + } + // 새로운 사용자 생성 const newUser = await this.prisma.user.create({ data: { email, - name: nickname, - nickname, + name: uniqueNickname, + nickname: uniqueNickname, password, auth_provider: 'pad', // 소셜 로그인과 구분 role: { connect: { id: 1 } }, From c2ece6cfb3269eead8ac2adb64e5e0820540ed3b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 02:02:21 +0900 Subject: [PATCH 275/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EC=A1=B0=ED=9A=8C=20Swagger=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index f044b86..e358926 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -119,18 +119,18 @@ export const GetUserProfileDocs = { export const GetUserProfileHeaderDocs = { ApiOperation: ApiOperation({ - summary: '사용자 프로필 헤더 정보 조회', - description: '특정 사용자의 프로필 헤더 정보를 반환합니다. (간략한 정보)', + summary: '유저 프로필 헤더 조회', + description: '닉네임을 기반으로 특정 유저의 프로필 헤더를 조회합니다.', }), ApiParam: ApiParam({ - name: 'userId', + name: 'nickname', required: true, - description: '조회할 사용자의 ID', - type: 'string', + description: '조회할 유저의 닉네임', + example: 'testNickname', }), ApiResponse: ApiResponse({ status: 200, - description: '사용자 프로필 헤더 조회 성공', + description: '프로필 헤더 조회 성공', schema: { example: { message: { @@ -138,17 +138,13 @@ export const GetUserProfileHeaderDocs = { text: '유저 프로필(헤더 부분) 조회에 성공했습니다', }, userId: 1, - nickname: 'testForChangeNickName', - profileUrl: - '"https://user-profile-icons.s3.ap-northeast-2.amazonaws.com/pad_users/profile_7d52f324-8694-4789-a4f9-ab4dfc40e482.jpeg', - role: 'Programmer', - introduce: 'I am a software engineer.', - userLinks: [ - 'https://github.com/Ss0Mae', - 'https://www.linkedin.com/in/Ss0Mae', - ], - isOwnProfile: true, - isFollowing: false, + nickname: 'testNickname', + profileUrl: 'https://example.com/profile.jpg', + role: 'Developer', + introduce: '안녕하세요. 저는 개발자입니다.', + userLinks: ['https://github.com/test', 'https://test.com'], + isOwnProfile: false, + isFollowing: true, }, }, }), From 18f6782b514a00d899b20d1bcb5b9cf66734e891 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 02:02:50 +0900 Subject: [PATCH 276/414] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=ED=97=A4=EB=8D=94=20API=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.controller.ts | 10 ++++++---- src/modules/user/user.service.ts | 12 ++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index dbdfccd..1aea29d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -72,14 +72,16 @@ export class UserController { return this.userService.getUserProfile(loggedInUserId, numUserId); } - @Get(':userId/headers') + @Get(':nickname/headers') @GetUserProfileHeaderDocs.ApiOperation @GetUserProfileHeaderDocs.ApiParam @GetUserProfileHeaderDocs.ApiResponse - async getUserProfileHeader(@Param('userId') userId: string, @Req() req) { + async getUserProfileHeader(@Param('nickname') nickname: string, @Req() req) { const loggedInUserId = req.user?.user_id; - const numUserId = parseInt(userId); // 인증된 사용자 ID - return this.userService.getUserProfileHeader(loggedInUserId, numUserId); + return this.userService.getUserProfileHeaderByNickname( + loggedInUserId, + nickname + ); } @Get(':userId/followers') diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 67bf4f0..8938b68 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -95,9 +95,13 @@ export class UserService { return response; } - async getUserProfileHeader(loggedInUserId: number, targetUserId: number) { + async getUserProfileHeaderByNickname( + loggedInUserId: number, + nickname: string + ) { + // 닉네임으로 사용자 조회 const user = await this.prisma.user.findUnique({ - where: { id: targetUserId }, + where: { nickname }, // 닉네임을 기준으로 조회 include: { role: true, // 역할 정보 UserLinks: true, // 연결된 링크 @@ -112,7 +116,7 @@ export class UserService { const isFollowing = await this.prisma.follows.findFirst({ where: { following_user_id: loggedInUserId, - followed_user_id: targetUserId, + followed_user_id: user.id, }, }); @@ -128,7 +132,7 @@ export class UserService { role: user.role.name, introduce: user.introduce, userLinks: user.UserLinks.map(link => link.link), // 단순 URL 배열로 변경 - isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 + isOwnProfile: loggedInUserId === user.id, // 자신의 프로필인지 확인 isFollowing: !!isFollowing, // 팔로우 여부 확인 }; } From 8d2701af8430526ee0243843896e73bbacf6d685 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 02:04:27 +0900 Subject: [PATCH 277/414] =?UTF-8?q?[Fix]=20user=20nickname=20=EC=9C=A0?= =?UTF-8?q?=EB=8B=88=ED=81=AC=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3bc582..2e109ea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ model User { id Int @id @default(autoincrement()) email String @unique name String - nickname String + nickname String @unique auth_provider String profile_url String? role_id Int From d0fe024521abe0d8f6b5de4663c52053b17a511c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 18:37:17 +0900 Subject: [PATCH 278/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d78617c..2e109ea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ model User { id Int @id @default(autoincrement()) email String @unique name String - nickname String + nickname String @unique auth_provider String profile_url String? role_id Int @@ -229,16 +229,16 @@ model ProjectPost { title String content String @db.Text thumbnail_url String? - role String //[Programmer, Artist, Designer] - hub_type String //[Project, 아웃소싱] + role String start_date DateTime - duration String - work_type String //[Online, Offline] recruiting Boolean @default(true) applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) created_at DateTime @default(now()) + duration String + hub_type String + work_type String Details ProjectDetailRole[] user User @relation(fields: [user_id], references: [id]) Tags ProjectPostTag[] @@ -249,12 +249,12 @@ model ProjectPost { } model ProjectSave { - id Int @id @default(autoincrement()) - user_id Int - post_id Int - created_at DateTime @default(now()) // 지원 생성 시간 - post ProjectPost @relation(fields: [post_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user_id Int + post_id Int + created_at DateTime @default(now()) + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([post_id], map: "ProjectSave_post_id_fkey") @@index([user_id], map: "ProjectSave_user_id_fkey") @@ -274,13 +274,13 @@ model ProjectDetailRole { model DetailRole { id Int @id @default(autoincrement()) role_id Int - name String @unique + name String @unique Details ProjectDetailRole[] } model ProjectTag { id Int @id @default(autoincrement()) - name String @unique + name String @unique Tags ProjectPostTag[] } @@ -288,9 +288,9 @@ model ProjectPostTag { id Int @id @default(autoincrement()) post_id Int tag_id Int + created_at DateTime @default(now()) post ProjectPost @relation(fields: [post_id], references: [id]) tag ProjectTag @relation(fields: [tag_id], references: [id]) - created_at DateTime @default(now()) @@unique([post_id, tag_id]) @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") @@ -300,12 +300,12 @@ model UserApplyProject { id Int @id @default(autoincrement()) user_id Int post_id Int - status String @default("Pending") // "Accepted", "Rejected", "Pending" - created_at DateTime @default(now()) // 지원 생성 시간 + status String @default("Pending") + created_at DateTime @default(now()) post ProjectPost @relation(fields: [post_id], references: [id]) user User @relation(fields: [user_id], references: [id]) - @@unique([user_id, post_id]) // 중복 지원 방지 + @@unique([user_id, post_id]) @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } From c29fadc05da414a3e7e107050c18af0bff42e125 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 18:50:29 +0900 Subject: [PATCH 279/414] =?UTF-8?q?[Refactor]=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EA=B5=AC=ED=98=84,=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=EB=93=A4=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 57 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 2ab9b6e..f58fe67 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() @@ -181,18 +181,8 @@ export class ChatService { // 채널 개별 조회 async getChannel(userId: number, channelId: number) { try { - // 유저 아이디가 채널에 속해있는지 확인 - const auth = await this.prisma.channel_users.findMany({ - where: { - user_id: userId, - channel_id: channelId, - }, - }); - - // 아닐 시 예외처리 - if (!auth.length) { - throw new Error('권한 X'); - } + // 권한 확인 + await this.confirmAuth(userId, channelId); const channel = await this.getChannleObj(channelId); @@ -283,18 +273,8 @@ export class ChatService { direction: string ) { try { - // 유저 아이디가 채널에 속해있는지 확인 - const auth = await this.prisma.channel_users.findMany({ - where: { - user_id: userId, - channel_id: channelId, - }, - }); - - // 아닐 시 예외처리 - if (!auth.length) { - throw new Error('권한 X'); - } + // 권한 확인 + await this.confirmAuth(userId, channelId); // 메세지 데이터 조회 const result = await this.prisma.message.findMany({ @@ -368,17 +348,8 @@ export class ChatService { direction: string ) { try { - const auth = await this.prisma.channel_users.findMany({ - where: { - user_id: userId, - channel_id: channelId, - }, - }); - - // 아닐 시 예외처리 - if (!auth.length) { - throw new Error('권한 X'); - } + // 권한 확인 + await this.confirmAuth(userId, channelId); if (!cursor) { const res = await this.prisma.message.findFirst({ @@ -526,4 +497,18 @@ export class ChatService { messageId: msg.id, }; } + + // 요청 채널에 대한 유저 권한 확인 + async confirmAuth(userId: number, channelId: number) { + const auth = await this.prisma.channel_users.findMany({ + where: { + user_id: userId, + channel_id: channelId, + }, + }); + + if (!auth.length) { + throw new HttpException('권한이 없습니다.', HttpStatus.UNAUTHORIZED); + } + } } From a95316d044e9c08e6ba7b4b8b177da4117b4d692 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 18:51:31 +0900 Subject: [PATCH 280/414] =?UTF-8?q?[Refactor]=20=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BF=BC=EB=A6=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index b6b09ad..a504588 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -22,7 +22,6 @@ export class ChatController { @Param('id') channelId: number, @Query('limit') limit: number, @Query('cursor') cursor: number, - @Query('keyword') keyword: string, @Query('direction') direction: string ) { return await this.chatService.getMessages( From dffeccf019d9f603011727a536e9e53ec6484368 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:31:34 +0900 Subject: [PATCH 281/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20dto=20=EA=B5=AC=ED=98=84;=20getMessage.dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/dto/getMessage.dto.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/chat/dto/getMessage.dto.ts diff --git a/src/chat/dto/getMessage.dto.ts b/src/chat/dto/getMessage.dto.ts new file mode 100644 index 0000000..4da4eb2 --- /dev/null +++ b/src/chat/dto/getMessage.dto.ts @@ -0,0 +1,26 @@ +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + IsNumber, + IsIn, + IsOptional, +} from 'class-validator'; + +export class GetMessageDto { + @IsNumber({}, { message: 'limit은 숫자타입으로 주어져야 합니다.' }) + @Type(() => Number) + @IsNotEmpty({ message: 'limit 값을 입력해주세요' }) + limit?: number; + + @IsOptional() + @Type(() => Number) + cursor?: number; + + @IsString({ message: 'direction은 문자타입으로 주어져야 합니다' }) + @IsNotEmpty({ message: 'direction을 입력해주세요' }) + @IsIn(['forward', 'backward'], { + message: 'direction은 forward/backward 중 하나로 주어져야 합니다.', + }) + direction: string; +} From 05d6bf058b0ac69675a76ebead184e0bd4f66c63 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:31:52 +0900 Subject: [PATCH 282/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20dto=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 9 +++------ src/chat/chat.service.ts | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index a504588..05e7466 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; +import { GetMessageDto } from './dto/getMessage.dto'; @Controller('chat') export class ChatController { @@ -20,16 +21,12 @@ export class ChatController { async getChannelsMessages( @Req() req: any, @Param('id') channelId: number, - @Query('limit') limit: number, - @Query('cursor') cursor: number, - @Query('direction') direction: string + @Query() getMessageDto: GetMessageDto ) { return await this.chatService.getMessages( req.user.user_id, channelId, - limit, - cursor ? cursor : 0, - direction + getMessageDto ); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index f58fe67..474a738 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; +import { GetMessageDto } from './dto/getMessage.dto'; @Injectable() export class ChatService { @@ -268,11 +269,10 @@ export class ChatService { async getMessages( userId: number, channelId: number, - limit: number, - cursor: number, - direction: string + getMessageDto: GetMessageDto ) { try { + const { cursor, limit, direction } = getMessageDto; // 권한 확인 await this.confirmAuth(userId, channelId); From a2b6007475b2b0a38682346b8d8497079a6074aa Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:40:27 +0900 Subject: [PATCH 283/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20dto=20=EA=B5=AC=ED=98=84;=20searchMessage.?= =?UTF-8?q?dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/dto/serchMessage.dto.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/chat/dto/serchMessage.dto.ts diff --git a/src/chat/dto/serchMessage.dto.ts b/src/chat/dto/serchMessage.dto.ts new file mode 100644 index 0000000..2b3b1a1 --- /dev/null +++ b/src/chat/dto/serchMessage.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty } from 'class-validator'; +import { GetMessageDto } from './getMessage.dto'; + +export class SearchMessageDto extends GetMessageDto { + @IsNotEmpty({ message: '검색어를 입력해주세요' }) + keyword: any; +} From 52254a76e96edff8e62445ecab60690ab5b2d4a7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:40:39 +0900 Subject: [PATCH 284/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20dto=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.controller.ts | 11 +++-------- src/chat/chat.service.ts | 11 ++++++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index 05e7466..528f201 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { GetMessageDto } from './dto/getMessage.dto'; +import { SearchMessageDto } from './dto/serchMessage.dto'; @Controller('chat') export class ChatController { @@ -42,18 +43,12 @@ export class ChatController { async searchChannelMessages( @Req() req: any, @Param('id') channelId: number, - @Query('limit') limit: number, - @Query('cursor') cursor: number, - @Query('keyword') keyword: string, - @Query('direction') direction: string + @Query() searchMessageDto: SearchMessageDto ) { return await this.chatService.searchMessage( req.user.user_id, channelId, - limit, - cursor ? cursor : 0, - keyword, - direction + searchMessageDto ); } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 474a738..a44b814 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -1,6 +1,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; +import { SearchMessageDto } from './dto/serchMessage.dto'; @Injectable() export class ChatService { @@ -342,12 +343,12 @@ export class ChatService { async searchMessage( userId: number, channelId: number, - limit: number, - cursor: number, - keyword: string, - direction: string + searchMessageDto: SearchMessageDto ) { try { + const { limit, keyword } = searchMessageDto; + let { cursor, direction } = searchMessageDto; + // 권한 확인 await this.confirmAuth(userId, channelId); @@ -357,7 +358,7 @@ export class ChatService { where: { channel_id: channelId }, select: { id: true }, }); - + direction = 'backward'; cursor = res.id; } From 8fb652f779eab3264e5d1ce72d0315ffb97e869d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:44:36 +0900 Subject: [PATCH 285/414] =?UTF-8?q?[Refactor]=20=ED=94=BC=EB=93=9C=20API?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=82=B4=EC=9A=A9=20message=20->=20text?= =?UTF-8?q?=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 04998df..c8527ca 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -86,7 +86,7 @@ export class FeedService { return { posts, pagination: { lastCursor }, - message: { code: 200, message: '전체 피드를 정상적으로 조회했습니다.' }, + message: { code: 200, text: '전체 피드를 정상적으로 조회했습니다.' }, }; } catch (err) { console.log(err); @@ -146,7 +146,7 @@ export class FeedService { return { post, - message: { code: 200, message: '개별 피드를 정상적으로 조회했습니다.' }, + message: { code: 200, text: '개별 피드를 정상적으로 조회했습니다.' }, }; } catch (err) { console.log(err); @@ -213,7 +213,7 @@ export class FeedService { comments: [], message: { code: 200, - message: '개별 피드(댓글)를 정상적으로 조회했습니다.', + text: '개별 피드(댓글)를 정상적으로 조회했습니다.', }, }; } @@ -236,7 +236,7 @@ export class FeedService { comments, message: { code: 200, - message: '개별 피드(댓글)를 정상적으로 조회했습니다.', + text: '개별 피드(댓글)를 정상적으로 조회했습니다.', }, }; } catch (err) { @@ -275,7 +275,7 @@ export class FeedService { data: { like_count: { decrement: 1 } }, }); - return { message: { code: 200, message: '좋아요가 취소되었습니다.' } }; + return { message: { code: 200, text: '좋아요가 취소되었습니다.' } }; } else { await this.prisma.feedLike.create({ data: { @@ -289,7 +289,7 @@ export class FeedService { data: { like_count: { increment: 1 } }, }); - return { message: { code: 200, message: '좋아요가 추가되었습니다.' } }; + return { message: { code: 200, text: '좋아요가 추가되었습니다.' } }; } } catch (err) { console.log(err); @@ -335,7 +335,7 @@ export class FeedService { }); return { - message: { code: 201, message: '피드 작성이 완료되었습니다.' }, + message: { code: 201, text: '피드 작성이 완료되었습니다.' }, post: { id: feedData.id }, }; } catch (err) { @@ -382,7 +382,7 @@ export class FeedService { }, }); - return { message: { code: 200, message: '피드 수정이 완료되었습니다.' } }; + return { message: { code: 200, text: '피드 수정이 완료되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; @@ -425,7 +425,7 @@ export class FeedService { }), ]); - return { message: { code: 200, message: '피드가 삭제되었습니다.' } }; + return { message: { code: 200, text: '피드가 삭제되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; @@ -468,7 +468,7 @@ export class FeedService { data: { comment_count: { increment: 1 } }, }); - return { message: { code: 201, message: '댓글 등록이 완료되었습니다.' } }; + return { message: { code: 201, text: '댓글 등록이 완료되었습니다.' } }; } catch (err) { console.log(err); throw new HttpException( @@ -499,7 +499,7 @@ export class FeedService { data: { comment_count: { decrement: 1 } }, }), ]); - return { message: { code: 200, message: '댓글이 삭제되었습니다.' } }; + return { message: { code: 200, text: '댓글이 삭제되었습니다.' } }; } catch (err) { if (err instanceof HttpException) { throw err; @@ -532,7 +532,7 @@ export class FeedService { data: { content }, }); - return { message: { code: 200, message: '댓글 수정이 완료되었습니다.' } }; + return { message: { code: 200, text: '댓글 수정이 완료되었습니다.' } }; } // 댓글 좋아요 추가/제거 @@ -548,13 +548,13 @@ export class FeedService { where: { user_id: userId, comment_id: commentId }, }); - return { message: { code: 200, message: '좋아요가 취소되었습니다.' } }; + return { message: { code: 200, text: '좋아요가 취소되었습니다.' } }; } else { // 없을 시 좋아요 추가 await this.prisma.feedCommentLikes.create({ data: { user_id: userId, comment_id: commentId }, }); - return { message: { code: 200, message: '좋아요가 추가되었습니다.' } }; + return { message: { code: 200, text: '좋아요가 추가되었습니다.' } }; } } @@ -594,7 +594,7 @@ export class FeedService { return { imageUrl, - message: { code: 200, message: '이미지 업로드가 완료되었습니다.' }, + message: { code: 200, text: '이미지 업로드가 완료되었습니다.' }, }; } @@ -603,7 +603,7 @@ export class FeedService { return { tags, - message: { code: 200, message: '태그가 성공적으로 조회되었습니다.' }, + message: { code: 200, text: '태그가 성공적으로 조회되었습니다.' }, }; } @@ -652,7 +652,7 @@ export class FeedService { return { contents, - message: { code: 200, message: '성공적으로 조회되었습니다.' }, + message: { code: 200, text: '성공적으로 조회되었습니다.' }, }; } } From cad19bbdeee9134491acb82cf6b8bdb9199889a4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 19:45:02 +0900 Subject: [PATCH 286/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 75901e6..2e109ea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ model User { id Int @id @default(autoincrement()) email String @unique name String - nickname String + nickname String @unique auth_provider String profile_url String? role_id Int @@ -231,7 +231,7 @@ model ProjectPost { thumbnail_url String? role String start_date DateTime - recruiting Boolean + recruiting Boolean @default(true) applicant_count Int @default(0) view Int @default(0) saved_count Int @default(0) From e3fe02aaba57f758aeeb903b3cba68d029daed18 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 21:12:46 +0900 Subject: [PATCH 287/414] =?UTF-8?q?[Feat]=20search=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 +++- src/search/search.controller.ts | 7 +++++++ src/search/search.module.ts | 9 +++++++++ src/search/search.service.ts | 4 ++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/search/search.controller.ts create mode 100644 src/search/search.module.ts create mode 100644 src/search/search.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6b1541a..5f9ef0d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; import { FeedModule } from './feed/feed.module'; import { ProjectModule } from './modules/project/project.module'; +import { SearchModule } from './search/search.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -15,7 +16,8 @@ import { ProjectModule } from './modules/project/project.module'; UserModule, ChatModule, FeedModule, - ProjectModule + ProjectModule, + SearchModule ], providers: [ChatGateway], }) diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts new file mode 100644 index 0000000..2df4e0f --- /dev/null +++ b/src/search/search.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { SearchService } from './search.service'; + +@Controller('search') +export class SearchController { + constructor(private readonly searchService: SearchService) {} +} diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000..ea8938d --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { SearchService } from './search.service'; +import { SearchController } from './search.controller'; + +@Module({ + controllers: [SearchController], + providers: [SearchService], +}) +export class SearchModule {} diff --git a/src/search/search.service.ts b/src/search/search.service.ts new file mode 100644 index 0000000..b8ed7eb --- /dev/null +++ b/src/search/search.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SearchService {} From 7bdb2d75c788d65bcf73da3f992f80e09baad6bb Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 22:01:07 +0900 Subject: [PATCH 288/414] =?UTF-8?q?[Feat]=20=EC=A0=84=EC=B2=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20cursor=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 5c6c896..c2ca02b 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -35,7 +35,7 @@ import { UpdateProjectStatusDocs, UploadFeedImageDocs, } from './docs/project.docs'; -import { ApiBearerAuth } from '@nestjs/swagger' +import { ApiBearerAuth } from '@nestjs/swagger'; @Controller('projects') export class ProjectController { @@ -43,20 +43,20 @@ export class ProjectController { @Get() @GetProjectsDocs.ApiOperation - @GetProjectsDocs.ApiQuerySkip + @GetProjectsDocs.ApiQueryCursor @GetProjectsDocs.ApiQueryLimit @GetProjectsDocs.ApiQueryRole @GetProjectsDocs.ApiQueryUnit @GetProjectsDocs.ApiQuerySort @GetProjectsDocs.ApiResponseSuccess async getProjects( - @Query('skip') skip: number = 0, - @Query('limit') limit: number = 10, + @Query('cursor') cursor?: number, // 커서 추가 + @Query('limit') limit: number = 5, @Query('role') role?: string, @Query('unit') unit?: string, @Query('sort') sort: string = 'latest' ) { - return this.projectService.getProjects({ skip, limit, role, unit, sort }); + return this.projectService.getProjects({ cursor, limit, role, unit, sort }); } @Post() From 559bfcf0a208bbd05038cd4c4c79e301457bb823 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 22:01:17 +0900 Subject: [PATCH 289/414] =?UTF-8?q?[Feat]=20=EC=A0=84=EC=B2=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 29 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index b40caf6..47c9186 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -20,18 +20,20 @@ export class ProjectService { ) {} async getProjects(params: { - skip: number; + cursor?: number; // 커서 추가 limit: number; role?: string; unit?: string; sort: string; }) { - const { skip, limit, role, unit, sort } = params; + const { cursor, limit, role, unit, sort } = params; + // Where 조건 생성 const where: any = {}; if (role) where.role = role; if (unit) where.Tags = { some: { tag: { name: unit } } }; + // OrderBy 조건 생성 const orderBy: any[] = []; if (sort === 'latest') { orderBy.push({ created_at: 'desc' }); // 최신순 @@ -39,8 +41,13 @@ export class ProjectService { orderBy.push({ saved_count: 'desc' }); // 북마크 수 기준 정렬 } + // 커서 조건 추가 + if (cursor) { + where.id = { gt: cursor }; // 커서 이후의 데이터 가져오기 + } + + // 프로젝트 조회 const projects = await this.prisma.projectPost.findMany({ - skip, take: limit, where, orderBy, @@ -65,8 +72,7 @@ export class ProjectService { }, }); - const totalCount = await this.prisma.projectPost.count({ where }); - + // 포맷팅된 프로젝트 데이터 const formattedProjects = projects.map(project => ({ projectId: project.id, title: project.title, @@ -83,6 +89,7 @@ export class ProjectService { bookMarkCount: project.saved_count, viewCount: project.view + 1, status: project.recruiting ? 'OPEN' : 'CLOSED', + createdAt: project.created_at, user: { userId: project.user.id, nickname: project.user.nickname, @@ -92,14 +99,18 @@ export class ProjectService { }, })); + // 마지막 커서 계산 + const lastCursor = projects[projects.length - 1]?.id || null; + return { message: { code: 200, text: '전체 커넥션허브 조회에 성공했습니다', }, projects: formattedProjects, - page: Math.floor(skip / limit) + 1, - limit, + pagination: { + lastCursor, + }, }; } @@ -199,6 +210,9 @@ export class ProjectService { workType: project.work_type, status: project.recruiting ? 'OPEN' : 'CLOSE', viewCount: project.view, + applyCount: 0, + bookmarkCount: 0, + createdAt: project.created_at, skills: tags, detailRoles: roles, }, @@ -353,6 +367,7 @@ export class ProjectService { skills: project.Tags.map(t => t.tag.name), detailRoles: project.Details.map(d => d.detail_role.name), viewCount: project.view, // 이미 증가된 view 값을 사용 + createdAt: project.created_at, manager: { userId: project.user.id, name: project.user.name, From bb847a6bf4f8e3c9ed0befb3fc1ef5c315c7e819 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sat, 25 Jan 2025 22:01:33 +0900 Subject: [PATCH 290/414] =?UTF-8?q?[Feat]=20Projcet=20API=20Swagger=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 65 ++++++++++++++---------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index 6e2029a..40a4e60 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -9,43 +9,48 @@ import { export const GetProjectsDocs = { ApiOperation: ApiOperation({ - summary: '전체 프로젝트 조회', + summary: '프로젝트 목록 조회', description: - '서비스에 존재하는 모든 프로젝트를 조회합니다. 다양한 쿼리 옵션을 제공합니다.', + '프로젝트 목록을 페이징으로 조회합니다. 무한 스크롤을 지원합니다.', }), - ApiQuerySkip: ApiQuery({ - name: 'skip', + ApiQueryCursor: ApiQuery({ + name: 'cursor', required: false, - description: '건너뛸 데이터 수 (기본값: 0)', type: Number, + description: '마지막 항목의 ID. 무한 스크롤에서 사용.', + example: 10, }), ApiQueryLimit: ApiQuery({ name: 'limit', required: false, - description: '가져올 데이터 수 (기본값: 10)', type: Number, + description: '한 번에 가져올 데이터 수', + example: 10, }), ApiQueryRole: ApiQuery({ name: 'role', required: false, - description: '필터링할 역할 (예: Programmer)', type: String, + description: '필터링할 역할 (예: Developer, Designer)', + example: 'Developer', }), ApiQueryUnit: ApiQuery({ name: 'unit', required: false, - description: '모집 단위 (예: React)', type: String, + description: '필터링할 태그 유닛', + example: 'Frontend', }), ApiQuerySort: ApiQuery({ name: 'sort', required: false, - description: '정렬 기준 (latest 또는 popular)', type: String, + description: '정렬 기준 (latest: 최신순, popular: 인기순)', + example: 'latest', }), ApiResponseSuccess: ApiResponse({ status: 200, - description: '프로젝트 조회 성공', + description: '프로젝트 목록 조회 성공', schema: { example: { message: { @@ -55,31 +60,33 @@ export const GetProjectsDocs = { projects: [ { projectId: 1, - title: '프로젝트 제목', - content: '프로젝트 내용', - thumbnailUrl: 'https://example.com/thumbnail.png', - role: 'Programmer', - skills: ['React', 'TypeScript'], - detailRoles: ['웹 프로그래밍 개발자', '백엔드 개발자'], - hubType: 'PROJECT', - startDate: '2025-01-01', + title: 'Project 1', + content: 'Content of project 1', + thumbnailUrl: 'https://example.com/thumbnail1.jpg', + role: 'Developer', + skills: ['JavaScript', 'React'], + detailRoles: ['Frontend Developer'], + hubType: 'Remote', + startDate: '2023-01-01', duration: '3 months', - workType: 'ONLINE', + workType: 'Full-time', applyCount: 5, - bookMarkCount: 3, - viewCount: 10, + bookMarkCount: 10, + viewCount: 50, status: 'OPEN', + createdAt: '2023-01-01T00:00:00Z', user: { - userId: 101, - nickname: 'leechan_dev', - name: 'Lee Chan', - profileUrl: 'https://example.com/profile.png', + userId: 1, + nickname: 'testUser', + name: 'Test User', + profileUrl: 'https://example.com/profile.jpg', role: 'Developer', }, }, ], - page: 1, - limit: 10, + pagination: { + lastCursor: 1, + }, }, }, }), @@ -128,6 +135,9 @@ export const CreateProjectDocs = { workType: 'ONLINE', status: 'OPEN', viewCount: 0, + applyCount: 0, + bookmarkCount: 0, + createdAt: '2023-01-01T00:00:00Z', skills: ['React', 'TypeScript'], detailRoles: ['Frontend Developer', 'Fullstack Developer'], }, @@ -320,6 +330,7 @@ export const GetProjectDetailDocs = { skills: ['React', 'TypeScript'], detailRoles: ['Frontend Developer', 'Backend Developer'], viewCount: 2, + createdAt: '2025-01-01T00:00:00Z', manager: { userId: 101, name: 'Lee Chan', From 1aa79c341e0078a15f1ebb32df4dd30366fdcb81 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 23:21:02 +0900 Subject: [PATCH 291/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B2=B0=EA=B3=BC=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.module.ts | 2 ++ src/search/search.service.ts | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/search/search.module.ts b/src/search/search.module.ts index ea8938d..d3004eb 100644 --- a/src/search/search.module.ts +++ b/src/search/search.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { SearchService } from './search.service'; import { SearchController } from './search.controller'; +import { PrismaModule } from '@src/prisma/prisma.module'; @Module({ + imports: [PrismaModule], controllers: [SearchController], providers: [SearchService], }) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index b8ed7eb..daa0b27 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -1,4 +1,26 @@ import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() -export class SearchService {} +export class SearchService { + constructor(private readonly prisma: PrismaService) {} + + // 피드 검색결과 조회 + async searchFeed(keyword: string, limit: number) { + const result = await this.prisma.feedPost.findMany({ + where: { + OR: [ + { title: { contains: keyword } }, + { content: { contains: keyword } }, + ], + }, + include: { + user: true, + Tags: true, + }, + take: limit, + }); + + return result; + } +} From bbda8d3f9006ae662ca2995de7cc1539a56fad06 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 23:28:26 +0900 Subject: [PATCH 292/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=BB=AC=EB=9F=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index daa0b27..b67d914 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -15,8 +15,16 @@ export class SearchService { ], }, include: { - user: true, - Tags: true, + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + role: { select: { name: true } }, + }, + }, + Tags: { select: { tag: { select: { name: true } } } }, }, take: limit, }); From b02d415e8a8b7ba26e35f95d70980b4beaf78564 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 25 Jan 2025 23:34:06 +0900 Subject: [PATCH 293/414] =?UTF-8?q?[Feat]=20=EB=AA=A8=EB=8B=AC=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=83=89=EA=B2=B0=EA=B3=BC=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index b67d914..a006c7d 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -31,4 +31,21 @@ export class SearchService { return result; } + + // 모달 피드 검색결과 데이터 + async feedResultModal(result) { + const feeds = result.map(res => ({ + userId: res.user.id, + userName: res.user.name, + userNickname: res.user.nickname, + userProfileUrl: res.user.profile_url, + userRole: res.user.role.name, + feedId: res.id, + title: res.title, + tags: res.Tags.map(v => v.tag.name), + createdAt: res.created_at, + })); + + return feeds; + } } From b2de27fd85f668d54c1310b0777124041fd2c5d7 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 00:24:15 +0900 Subject: [PATCH 294/414] =?UTF-8?q?[Fix]=20GOOGLE=20CALLBACK=20URI=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.service.ts | 2 +- src/modules/auth/strategies/google.strategy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 47d3999..f3cc7c4 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -24,7 +24,7 @@ export class AuthService { code, client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: process.env.GOOGLE_CALLBACK_DEPLOY_URL, + redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, grant_type: 'authorization_code', } ); diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts index 8a8d799..6d23dba 100644 --- a/src/modules/auth/strategies/google.strategy.ts +++ b/src/modules/auth/strategies/google.strategy.ts @@ -9,7 +9,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_DEPLOY_URL, + callbackURL: process.env.GOOGLE_CALLBACK_DEVELOP_URL, scope: ['email', 'profile'], }); } From 59872cd14f9eb139840cfc6944550a434a1287e4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 00:35:29 +0900 Subject: [PATCH 295/414] =?UTF-8?q?[Feat]=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EA=B2=80=EC=83=89=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index a006c7d..7bc907c 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -48,4 +48,48 @@ export class SearchService { return feeds; } + + // 커넥션허브 검색결과 조회 + async searchConnectionhub(keyword: string, limit: number) { + const result = await this.prisma.projectPost.findMany({ + where: { + OR: [ + { title: { contains: keyword } }, + { content: { contains: keyword } }, + { + Tags: { + some: { + tag: { name: { contains: keyword } }, + }, + }, + }, + { + Details: { + some: { + detail_role: { + name: { contains: keyword }, + }, + }, + }, + }, + ], + }, + include: { + Tags: { select: { tag: { select: { name: true } } } }, + Details: { select: { detail_role: { select: { name: true } } } }, + user: { + select: { + id: true, + name: true, + nickname: true, + profile_url: true, + role: { select: { name: true } }, + }, + }, + }, + take: limit, + }); + + return result; + } } From f3a8d5965a39c9506bb1b8eb1942ff3e0c65dc10 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 00:42:51 +0900 Subject: [PATCH 296/414] =?UTF-8?q?[Feat]=20=EB=AA=A8=EB=8B=AC=20=EC=BB=A4?= =?UTF-8?q?=EB=84=A5=EC=85=98=ED=97=88=EB=B8=8C=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 7bc907c..dca89d0 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -92,4 +92,26 @@ export class SearchService { return result; } + + // 모달 커넥션허브 검색결과 데이터 + async connectionhubResultModal(result) { + const projects = result.map(res => ({ + userId: res.user.id, + userName: res.user.name, + userNickname: res.user.nickname, + userProfileUrl: res.user.profile_url, + userRole: res.user.role.name, + projectId: res.id, + title: res.title, + role: res.role, + detailRoles: res.Details.map(v => v.detail_role.name), + tags: res.Tags.map(v => v.tag.name), + startDate: res.start_date, + duration: res.duration, + hubType: res.hub_type, + workType: res.work_type, + })); + + return projects; + } } From 83545954b9d8c2af871d6a39d7308f4672a0688b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 01:10:54 +0900 Subject: [PATCH 297/414] [Fix] Swagger Update --- src/modules/project/docs/project.docs.ts | 49 +++++++++++------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index 40a4e60..a7a819c 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -20,13 +20,6 @@ export const GetProjectsDocs = { description: '마지막 항목의 ID. 무한 스크롤에서 사용.', example: 10, }), - ApiQueryLimit: ApiQuery({ - name: 'limit', - required: false, - type: Number, - description: '한 번에 가져올 데이터 수', - example: 10, - }), ApiQueryRole: ApiQuery({ name: 'role', required: false, @@ -318,27 +311,29 @@ export const GetProjectDetailDocs = { code: 200, text: '프로젝트 상세 조회에 성공했습니다', }, - projectId: 1, - title: '프로젝트 제목', - content: '프로젝트 상세 내용', - role: 'Programmer', - hubType: 'PROJECT', - startDate: '2025-01-01T00:00:00Z', - duration: '6 months', - workType: 'ONLINE', - status: 'OPEN', - skills: ['React', 'TypeScript'], - detailRoles: ['Frontend Developer', 'Backend Developer'], - viewCount: 2, - createdAt: '2025-01-01T00:00:00Z', - manager: { - userId: 101, - name: 'Lee Chan', - nickname: 'leechan_dev', - profileUrl: 'https://example.com/profile.png', - introduce: '프론트엔드 개발자입니다.', + project: { + projectId: 1, + title: '프로젝트 제목', + content: '프로젝트 상세 내용', + role: 'Programmer', + hubType: 'PROJECT', + startDate: '2025-01-01T00:00:00Z', + duration: '6 months', + workType: 'ONLINE', + status: 'OPEN', + skills: ['React', 'TypeScript'], + detailRoles: ['Frontend Developer', 'Backend Developer'], + viewCount: 2, + createdAt: '2025-01-01T00:00:00Z', + manager: { + userId: 101, + name: 'Lee Chan', + nickname: 'leechan_dev', + profileUrl: 'https://example.com/profile.png', + introduce: '프론트엔드 개발자입니다.', + }, + isOwnConnectionHub: true, }, - isOwnConnectionHub: true, }, }, }), From 6514f353ff90e5f38a0bf92a0e02d792f603f043 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 01:17:57 +0900 Subject: [PATCH 298/414] =?UTF-8?q?[Feat]=20=EB=AA=A8=EB=8B=AC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index dca89d0..fd86a44 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -4,6 +4,35 @@ import { PrismaService } from '@src/prisma/prisma.service'; @Injectable() export class SearchService { constructor(private readonly prisma: PrismaService) {} + // 모달 검색 핸들러 + async handleModalSearch(keyword: string, category: string) { + let result; + const limit = 4; + switch (category) { + case 'all': + result = { + feed: this.feedResultModal(this.searchFeed(keyword, limit)), + + projects: this.connectionhubResultModal( + this.searchConnectionhub(keyword, limit) + ), + }; + break; + case 'feed': + result = { + feeds: this.feedResultModal(this.searchFeed(keyword, limit)), + }; + break; + case 'connectionhub': + result = { + projects: this.connectionhubResultModal( + this.searchConnectionhub(keyword, limit) + ), + }; + } + + return result; + } // 피드 검색결과 조회 async searchFeed(keyword: string, limit: number) { From 96746fa283064bbba763bea49aaec9cd3511f2cd Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 01:24:33 +0900 Subject: [PATCH 299/414] =?UTF-8?q?[Fix]=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index fd86a44..f9eb396 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -8,29 +8,35 @@ export class SearchService { async handleModalSearch(keyword: string, category: string) { let result; const limit = 4; + + // category 값에 따라 응답데이터 변경 switch (category) { case 'all': result = { - feed: this.feedResultModal(this.searchFeed(keyword, limit)), + feed: await this.feedResultModal( + await this.searchFeed(keyword, limit) + ), - projects: this.connectionhubResultModal( - this.searchConnectionhub(keyword, limit) + projects: await this.connectionhubResultModal( + await this.searchConnectionhub(keyword, limit) ), }; break; case 'feed': result = { - feeds: this.feedResultModal(this.searchFeed(keyword, limit)), + feeds: await this.feedResultModal( + await this.searchFeed(keyword, limit) + ), }; break; case 'connectionhub': result = { - projects: this.connectionhubResultModal( - this.searchConnectionhub(keyword, limit) + projects: await this.connectionhubResultModal( + await this.searchConnectionhub(keyword, limit) ), }; } - + result.messgae = { code: 200, text: '검색 결과 조회에 성공했습니다.' }; return result; } @@ -57,12 +63,12 @@ export class SearchService { }, take: limit, }); - return result; } // 모달 피드 검색결과 데이터 async feedResultModal(result) { + if (!result.length) return '검색 결과가 없습니다.'; const feeds = result.map(res => ({ userId: res.user.id, userName: res.user.name, @@ -124,6 +130,7 @@ export class SearchService { // 모달 커넥션허브 검색결과 데이터 async connectionhubResultModal(result) { + if (!result.length) return '검색 결과가 없습니다.'; const projects = result.map(res => ({ userId: res.user.id, userName: res.user.name, From 3cae57758f1fdb767fe9101b52d1c9dd6de360df Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 01:38:26 +0900 Subject: [PATCH 300/414] =?UTF-8?q?[Feat]=20=EB=AA=A8=EB=8B=AC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.controller.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index 2df4e0f..3601e09 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -1,7 +1,16 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { SearchService } from './search.service'; @Controller('search') export class SearchController { constructor(private readonly searchService: SearchService) {} + + // 모달 창에서 검색 + @Get('modal') + async handleModalSearch( + @Query('keyword') keyword: string, + @Query('category') category: string + ) { + return await this.searchService.handleModalSearch(keyword, category); + } } From d6238237f67804338f07ede32e7d7825f735d8ac Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 01:43:17 +0900 Subject: [PATCH 301/414] =?UTF-8?q?[Fix]=20=ED=86=A0=ED=81=B0=20=EA=B0=92?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 21 ++++--- src/modules/project/project.service.ts | 71 +++++++++++------------ 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index c2ca02b..7df9f1b 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -44,18 +44,17 @@ export class ProjectController { @Get() @GetProjectsDocs.ApiOperation @GetProjectsDocs.ApiQueryCursor - @GetProjectsDocs.ApiQueryLimit @GetProjectsDocs.ApiQueryRole @GetProjectsDocs.ApiQueryUnit @GetProjectsDocs.ApiQuerySort @GetProjectsDocs.ApiResponseSuccess async getProjects( - @Query('cursor') cursor?: number, // 커서 추가 - @Query('limit') limit: number = 5, + @Query('cursor') cursor?: number, @Query('role') role?: string, @Query('unit') unit?: string, - @Query('sort') sort: string = 'latest' + @Query('sort') sort: boolean = true ) { + const limit = 10; return this.projectService.getProjects({ cursor, limit, role, unit, sort }); } @@ -142,7 +141,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id;; // 인증된 사용자 ID return this.projectService.applyToProject(userId, projectId); } @@ -166,7 +165,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id;; // 인증된 사용자 ID return this.projectService.checkApplyStatus(userId, projectId); } @@ -180,7 +179,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id;; // 인증된 사용자 ID return this.projectService.cancelApplication(userId, projectId); } @@ -198,7 +197,7 @@ export class ProjectController { @Body('status') status: 'Accepted' | 'Rejected' | 'Pending', @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id;; // 인증된 사용자 ID return this.projectService.updateApplicationStatus( userId, projectId, @@ -219,7 +218,7 @@ export class ProjectController { @Body('recruiting') recruiting: boolean, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id;; // 인증된 사용자 ID return this.projectService.updateProjectStatus( userId, projectId, @@ -237,7 +236,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.toggleBookmark(userId, projectId); } @@ -251,7 +250,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.checkBookmark(userId, projectId); } } diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 47c9186..872746c 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -20,35 +20,27 @@ export class ProjectService { ) {} async getProjects(params: { - cursor?: number; // 커서 추가 + cursor?: number; limit: number; role?: string; unit?: string; - sort: string; + sort: boolean; }) { const { cursor, limit, role, unit, sort } = params; - // Where 조건 생성 const where: any = {}; if (role) where.role = role; if (unit) where.Tags = { some: { tag: { name: unit } } }; - // OrderBy 조건 생성 const orderBy: any[] = []; - if (sort === 'latest') { - orderBy.push({ created_at: 'desc' }); // 최신순 - } else if (sort === 'popular') { - orderBy.push({ saved_count: 'desc' }); // 북마크 수 기준 정렬 - } + orderBy.push(sort ? { created_at: 'desc' } : { saved_count: 'desc' }); - // 커서 조건 추가 if (cursor) { - where.id = { gt: cursor }; // 커서 이후의 데이터 가져오기 + where.id = { gt: cursor }; } - // 프로젝트 조회 const projects = await this.prisma.projectPost.findMany({ - take: limit, + take: limit, // limit 값은 컨트롤러에서 설정 where, orderBy, include: { @@ -63,16 +55,13 @@ export class ProjectService { profile_url: true, introduce: true, role: { - select: { - name: true, - }, + select: { name: true }, }, }, }, }, }); - // 포맷팅된 프로젝트 데이터 const formattedProjects = projects.map(project => ({ projectId: project.id, title: project.title, @@ -99,7 +88,6 @@ export class ProjectService { }, })); - // 마지막 커서 계산 const lastCursor = projects[projects.length - 1]?.id || null; return { @@ -339,6 +327,9 @@ export class ProjectService { introduce: true, }, }, + Applications: { + select: { id: true }, // 지원 데이터를 가져옴 + }, }, }); @@ -355,27 +346,31 @@ export class ProjectService { code: 200, text: '프로젝트 상세 조회에 성공했습니다', }, - projectId: project.id, - title: project.title, - content: project.content, - role: project.role, - hubType: project.hub_type, - startDate: project.start_date, - duration: project.duration, - workType: project.work_type, - status: project.recruiting ? 'OPEN' : 'CLOSE', - skills: project.Tags.map(t => t.tag.name), - detailRoles: project.Details.map(d => d.detail_role.name), - viewCount: project.view, // 이미 증가된 view 값을 사용 - createdAt: project.created_at, - manager: { - userId: project.user.id, - name: project.user.name, - nickname: project.user.nickname, - profileUrl: project.user.profile_url, - introduce: project.user.introduce ? project.user.introduce : null, + project: { + projectId: project.id, + title: project.title, + content: project.content, + role: project.role, + hubType: project.hub_type, + startDate: project.start_date, + duration: project.duration, + workType: project.work_type, + status: project.recruiting ? 'OPEN' : 'CLOSE', + skills: project.Tags.map(t => t.tag.name), + detailRoles: project.Details.map(d => d.detail_role.name), + viewCount: project.view, // 이미 증가된 view 값을 사용 + bookmarkCount: project.saved_count, + applyCount: project.Applications.length, + createdAt: project.created_at, + manager: { + userId: project.user.id, + name: project.user.name, + nickname: project.user.nickname, + profileUrl: project.user.profile_url, + introduce: project.user.introduce ? project.user.introduce : null, + }, + isOwnConnectionHub, }, - isOwnConnectionHub, }; } From 6dc14956638ea7a57f6e682a5919fd115491af6e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 01:44:27 +0900 Subject: [PATCH 302/414] =?UTF-8?q?[Fix]=20API=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 33 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 872746c..f241999 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ConflictException, ForbiddenException, HttpException, @@ -32,15 +33,24 @@ export class ProjectService { if (role) where.role = role; if (unit) where.Tags = { some: { tag: { name: unit } } }; - const orderBy: any[] = []; - orderBy.push(sort ? { created_at: 'desc' } : { saved_count: 'desc' }); - + // 커서 조건 추가 if (cursor) { - where.id = { gt: cursor }; + const validCursor = await this.prisma.projectPost.findUnique({ + where: { id: cursor }, + }); + + if (!validCursor) { + throw new BadRequestException('유효하지 않은 커서 값입니다.'); + } + + where.id = { gt: cursor }; // 유효한 커서 이후의 데이터 가져오기 } + const orderBy: any[] = []; + orderBy.push(sort ? { created_at: 'desc' } : { saved_count: 'desc' }); + const projects = await this.prisma.projectPost.findMany({ - take: limit, // limit 값은 컨트롤러에서 설정 + take: limit, where, orderBy, include: { @@ -54,9 +64,7 @@ export class ProjectService { nickname: true, profile_url: true, introduce: true, - role: { - select: { name: true }, - }, + role: { select: { name: true } }, }, }, }, @@ -93,7 +101,10 @@ export class ProjectService { return { message: { code: 200, - text: '전체 커넥션허브 조회에 성공했습니다', + text: + projects.length > 0 + ? '프로젝트 조회에 성공했습니다.' + : '더 이상 프로젝트가 없습니다.', }, projects: formattedProjects, pagination: { @@ -249,7 +260,7 @@ export class ProjectService { name: project.user.name, nickname: project.user.nickname, profileUrl: project.user.profile_url, - role: project.user.role, + role: project.user.role.name, }, hubType: project.hub_type, })); @@ -369,8 +380,8 @@ export class ProjectService { profileUrl: project.user.profile_url, introduce: project.user.introduce ? project.user.introduce : null, }, - isOwnConnectionHub, }, + isOwnConnectionHub, }; } From 0d889bfe54daacdfce16a49a5ab279d9aecaa739 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 01:45:35 +0900 Subject: [PATCH 303/414] =?UTF-8?q?[Fix]=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index a7a819c..4afe5c7 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -25,21 +25,19 @@ export const GetProjectsDocs = { required: false, type: String, description: '필터링할 역할 (예: Developer, Designer)', - example: 'Developer', }), ApiQueryUnit: ApiQuery({ name: 'unit', required: false, type: String, - description: '필터링할 태그 유닛', - example: 'Frontend', + description: '필터링할 직업 세부 정보', }), ApiQuerySort: ApiQuery({ name: 'sort', required: false, - type: String, - description: '정렬 기준 (latest: 최신순, popular: 인기순)', - example: 'latest', + type: Boolean, + description: '정렬 기준 (true: 최신순, false: 인기순)', + example: 'true', }), ApiResponseSuccess: ApiResponse({ status: 200, @@ -332,8 +330,8 @@ export const GetProjectDetailDocs = { profileUrl: 'https://example.com/profile.png', introduce: '프론트엔드 개발자입니다.', }, - isOwnConnectionHub: true, }, + isOwnConnectionHub: true, }, }, }), From 382392aca317f99877a3741541b439f4dbbd971f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 15:38:17 +0900 Subject: [PATCH 304/414] =?UTF-8?q?[Feat]=20=EA=B2=80=EC=83=89=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=8D=94=EB=B3=B4=EA=B8=B0=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8;=20hasMore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index f9eb396..c713a80 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -13,25 +13,25 @@ export class SearchService { switch (category) { case 'all': result = { - feed: await this.feedResultModal( + feedResult: await this.feedResultModal( await this.searchFeed(keyword, limit) ), - projects: await this.connectionhubResultModal( + projectResult: await this.connectionhubResultModal( await this.searchConnectionhub(keyword, limit) ), }; break; case 'feed': result = { - feeds: await this.feedResultModal( + feedResult: await this.feedResultModal( await this.searchFeed(keyword, limit) ), }; break; case 'connectionhub': result = { - projects: await this.connectionhubResultModal( + projectResult: await this.connectionhubResultModal( await this.searchConnectionhub(keyword, limit) ), }; @@ -61,14 +61,16 @@ export class SearchService { }, Tags: { select: { tag: { select: { name: true } } } }, }, - take: limit, + take: limit + 1, }); return result; } // 모달 피드 검색결과 데이터 async feedResultModal(result) { - if (!result.length) return '검색 결과가 없습니다.'; + const hasMore = result.length == 5; + if (hasMore) result = result.slice(0, 4); + if (!result.length) return { feeds: [], hasMore }; const feeds = result.map(res => ({ userId: res.user.id, userName: res.user.name, @@ -81,7 +83,7 @@ export class SearchService { createdAt: res.created_at, })); - return feeds; + return { feeds, hasMore }; } // 커넥션허브 검색결과 조회 @@ -122,7 +124,7 @@ export class SearchService { }, }, }, - take: limit, + take: limit + 1, }); return result; @@ -130,7 +132,9 @@ export class SearchService { // 모달 커넥션허브 검색결과 데이터 async connectionhubResultModal(result) { - if (!result.length) return '검색 결과가 없습니다.'; + const hasMore = result.length == 5; + if (hasMore) result = result.slice(0, 4); + if (!result.length) return { projects: [], hasMore }; const projects = result.map(res => ({ userId: res.user.id, userName: res.user.name, @@ -148,6 +152,6 @@ export class SearchService { workType: res.work_type, })); - return projects; + return { projects, hasMore }; } } From d3424f1fe865732e211ad27019b6bf55c92b8d67 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 16:20:23 +0900 Subject: [PATCH 305/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=EB=A7=88=20=EC=BF=BC=EB=A6=AC=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=80=EC=83=89=EA=B2=B0=EA=B3=BC=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=EC=88=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index c713a80..7f5ed2b 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -49,6 +49,8 @@ export class SearchService { { content: { contains: keyword } }, ], }, + orderBy: { id: 'desc' }, + take: limit + 1, include: { user: { select: { @@ -61,7 +63,6 @@ export class SearchService { }, Tags: { select: { tag: { select: { name: true } } } }, }, - take: limit + 1, }); return result; } @@ -111,6 +112,8 @@ export class SearchService { }, ], }, + orderBy: { id: 'desc' }, + take: limit + 1, include: { Tags: { select: { tag: { select: { name: true } } } }, Details: { select: { detail_role: { select: { name: true } } } }, @@ -124,7 +127,6 @@ export class SearchService { }, }, }, - take: limit + 1, }); return result; From 78b87ac1c908c6d3a95ccda501011479cd5bf295 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 18:20:35 +0900 Subject: [PATCH 306/414] =?UTF-8?q?[Fix]=20=EC=83=81=EC=84=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=EC=9D=98=20=EC=A7=81=EC=97=85?= =?UTF-8?q?=EA=B5=B0=20=EC=9D=91=EB=8B=B5=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index f241999..4fdd85a 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -336,6 +336,7 @@ export class ProjectService { nickname: true, profile_url: true, introduce: true, + role: true, }, }, Applications: { @@ -377,6 +378,7 @@ export class ProjectService { userId: project.user.id, name: project.user.name, nickname: project.user.nickname, + role: project.user.role.name, profileUrl: project.user.profile_url, introduce: project.user.introduce ? project.user.introduce : null, }, From bae1e40307c60654e678d7c41be11d80f8dcf327 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 26 Jan 2025 18:20:57 +0900 Subject: [PATCH 307/414] =?UTF-8?q?[Fix]=20Swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index 4afe5c7..820138d 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -327,6 +327,7 @@ export const GetProjectDetailDocs = { userId: 101, name: 'Lee Chan', nickname: 'leechan_dev', + role: 'Programmer', profileUrl: 'https://example.com/profile.png', introduce: '프론트엔드 개발자입니다.', }, From d51dd459e0d6937827aedf78855a1bf6e9326d42 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 21:32:09 +0900 Subject: [PATCH 308/414] =?UTF-8?q?[Fix]=20=ED=94=BC=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EC=B5=9C=EC=8B=A0=EC=88=9C=20?= =?UTF-8?q?=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index c8527ca..da216fe 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -40,11 +40,14 @@ export class FeedService { : null; const result = await this.prisma.feedPost.findMany({ + orderBy: { [orderKey]: 'desc' }, where: { - ...(cursor ? { id: { gt: cursor } } : {}), // cursor 조건 추가 (옵셔널) + ...(cursor ? { id: { lt: cursor } } : {}), // cursor 조건 추가 (옵셔널) ...(feedTagIds ? { id: { in: feedTagIds } } : {}), // 태그 조건 추가 (옵셔널) }, + take: limit, + include: { Likes: { where: { user_id: userId }, @@ -69,9 +72,6 @@ export class FeedService { }, }, }, - take: limit, - // 인기순 정렬 : 좋아요 순 - orderBy: { [orderKey]: 'desc' }, }); const posts = []; From 8cc61552a72bfb76f37fd5dbb083ec91cdd6f4fd Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 21:37:23 +0900 Subject: [PATCH 309/414] =?UTF-8?q?[Refactor]=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=EC=85=98=ED=97=88=EB=B8=8C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 7f5ed2b..cd937aa 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -147,7 +147,7 @@ export class SearchService { title: res.title, role: res.role, detailRoles: res.Details.map(v => v.detail_role.name), - tags: res.Tags.map(v => v.tag.name), + skills: res.Tags.map(v => v.tag.name), startDate: res.start_date, duration: res.duration, hubType: res.hub_type, From 6a99ff1daf9208a61fa2cdc761e75b42347d6f7c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 26 Jan 2025 21:41:12 +0900 Subject: [PATCH 310/414] =?UTF-8?q?[Feat]=20=EA=B2=80=EC=83=89=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94/=EB=B6=81=EB=A7=88=ED=81=AC=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=8F=84=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index cd937aa..5507a8e 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -62,6 +62,7 @@ export class SearchService { }, }, Tags: { select: { tag: { select: { name: true } } } }, + Likes: { select: { user_id: true } }, }, }); return result; @@ -126,6 +127,7 @@ export class SearchService { role: { select: { name: true } }, }, }, + Saves: { select: { user_id: true } }, }, }); From 297b638e7d27760c7f9c38361647ed8a5859a3b3 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 03:07:08 +0900 Subject: [PATCH 311/414] =?UTF-8?q?[Feat]=20=EB=AA=A8=EB=8B=AC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95;=20=EB=AF=B8?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EB=90=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EB=8F=84=20=EB=B9=88=EB=B0=B0=EC=97=B4=20?= =?UTF-8?q?=EB=B0=8F=20hasMore=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 5507a8e..7f3c78c 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -27,10 +27,12 @@ export class SearchService { feedResult: await this.feedResultModal( await this.searchFeed(keyword, limit) ), + projectResult: { projects: [], hasMore: false }, }; break; case 'connectionhub': result = { + feedResult: { feeds: [], hasMore: false }, projectResult: await this.connectionhubResultModal( await this.searchConnectionhub(keyword, limit) ), From 811028c7bfa43f8b58712625efb3c4c15ddcdcd1 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 06:51:27 +0900 Subject: [PATCH 312/414] =?UTF-8?q?[Feat]=20=EC=9D=B8=EA=B8=B0=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20&=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=EC=BB=A4=EC=84=9C?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 78 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index da216fe..3b7ea84 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -20,9 +20,6 @@ export class FeedService { const userId = user ? user.user_id : 0; const { latest = false, limit = 10, cursor = 0, tags } = queryDto; - // 정렬 기준 - const orderKey = latest ? 'created_at' : 'like_count'; - // 쿼리로 전달받은 태그 const tagIds = tags ? tags.split(',').map(id => parseInt(id)) : []; @@ -39,8 +36,13 @@ export class FeedService { ).map(p => p.post_id) : null; + // 쿼리값이 인기순이면 따로 처리 + if (!latest) { + return this.getPopularFeed(userId, limit, cursor, feedTagIds); + } + const result = await this.prisma.feedPost.findMany({ - orderBy: { [orderKey]: 'desc' }, + orderBy: { id: 'desc' }, where: { ...(cursor ? { id: { lt: cursor } } : {}), // cursor 조건 추가 (옵셔널) ...(feedTagIds ? { id: { in: feedTagIds } } : {}), // 태그 조건 추가 (옵셔널) @@ -97,6 +99,74 @@ export class FeedService { } } + // 인기 피드 조회 (좋아요 순) + async getPopularFeed( + userId: number, + limit: number, + cursor: number, + feedTagIds + ) { + try { + const result = await this.prisma.feedPost.findMany({ + orderBy: [{ like_count: 'desc' }, { view: 'desc' }], + where: { + ...(feedTagIds ? { id: { in: feedTagIds } } : {}), // 태그 조건 추가 (옵셔널) + }, + skip: cursor * limit, + take: limit, + include: { + Likes: { + where: { user_id: userId }, + }, + user: { + select: { + id: true, + name: true, + email: true, + nickname: true, + role: { + select: { name: true }, + }, + profile_url: true, + }, + }, + Tags: { + select: { + tag: { + select: { name: true }, + }, + }, + }, + }, + }); + + const posts = []; + for (const res of result) { + const post = await this.getPostObj(res); + posts.push(post); + } + + let lastCursor; + if (result.length) { + lastCursor = cursor + 1; + } else { + lastCursor = null; + } + + return { + posts, + pagination: { lastCursor }, + message: { code: 200, text: '전체 피드를 정상적으로 조회했습니다.' }, + }; + } catch (err) { + console.log(err); + throw new HttpException( + '서버에서 오류가 발생했습니다.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + // 피드 조회 (게시글 부분) async getFeed(feedId: number, user) { try { From 612f05939206db2a8d107e8cfafec200e6ffa984 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 07:02:11 +0900 Subject: [PATCH 313/414] =?UTF-8?q?[Feat]=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=9A=A9=20=ED=94=BC=EB=93=9C=20=EA=B2=80=EC=83=89=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=96=91=EC=8B=9D?= =?UTF-8?q?=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 7f3c78c..4b28b31 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -90,6 +90,29 @@ export class SearchService { return { feeds, hasMore }; } + // 페이지 피드 검색결과 데이터 + async feedResultPage(result) { + const feeds = result.map(result => ({ + userId: result.user.id, + userName: result.user.name, + userNickname: result.user.nickname, + userRole: result.user.role.name, + userProfileUrl: result.user.profile_url, + title: result.title, + postId: result.id, + thumnailUrl: result.thumbnail_url, + content: result.content, + tags: result.Tags.map(v => v.tag.name), + commentCount: result.comment_count, + likeCount: result.like_count, + viewCount: result.view, + createdAt: result.created_at, + isLiked: !!result.Likes.length, + })); + + return feeds; + } + // 커넥션허브 검색결과 조회 async searchConnectionhub(keyword: string, limit: number) { const result = await this.prisma.projectPost.findMany({ From 46e365d1cab85f9f519508b6e04e063296ac7454 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 07:14:59 +0900 Subject: [PATCH 314/414] =?UTF-8?q?[Feat]=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=A4=EB=84=A5=EC=85=98=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B2=B0=EA=B3=BC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 4b28b31..df76e7d 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -183,4 +183,32 @@ export class SearchService { return { projects, hasMore }; } + + // 페이지 커넥션허브 검색결과 데이터 + async connectionhubResultPage(result) { + const projects = result.map(res => ({ + userId: res.user.id, + userName: res.user.name, + userNickname: res.user.nickname, + userProfileUrl: res.user.profile_url, + userRole: res.user.role.name, + projectId: res.id, + title: res.title, + content: res.content, + thumbnailUrl: res.thumbnail_url, + role: res.role, + skills: res.Tags.map(tag => `${tag.tag.name}`), + detailRoles: res.Details.map(d => `${d.detail_role.name}`), + hubType: res.hub_type, + startDate: res.start_date.toISOString().split('T')[0], + duration: res.duration, + workType: res.work_type, + applyCount: res.applicant_count, + bookMarkCount: res.saved_count, + viewCount: res.view + 1, + status: res.recruiting ? 'OPEN' : 'CLOSED', + isMarked: res.Saves.length, + })); + return { projects }; + } } From befa27920a878654ee94f372109011b9367465e7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 07:25:21 +0900 Subject: [PATCH 315/414] =?UTF-8?q?[Feat]=20=EA=B2=80=EC=83=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EC=9C=A0=EC=A0=80=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.controller.ts | 11 +++++++++-- src/search/search.service.ts | 20 +++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index 3601e09..002948f 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -1,5 +1,6 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import { SearchService } from './search.service'; +import { OptionalAuthGuard } from '@src/modules/auth/guards/optional-auth.guard'; @Controller('search') export class SearchController { @@ -7,10 +8,16 @@ export class SearchController { // 모달 창에서 검색 @Get('modal') + @UseGuards(OptionalAuthGuard) async handleModalSearch( + @Req() req, @Query('keyword') keyword: string, @Query('category') category: string ) { - return await this.searchService.handleModalSearch(keyword, category); + return await this.searchService.handleModalSearch( + req.user, + keyword, + category + ); } } diff --git a/src/search/search.service.ts b/src/search/search.service.ts index df76e7d..dbd9660 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -5,7 +5,9 @@ import { PrismaService } from '@src/prisma/prisma.service'; export class SearchService { constructor(private readonly prisma: PrismaService) {} // 모달 검색 핸들러 - async handleModalSearch(keyword: string, category: string) { + async handleModalSearch(user, keyword: string, category: string) { + const userId = user ? user.user_id : 0; + let result; const limit = 4; @@ -14,18 +16,18 @@ export class SearchService { case 'all': result = { feedResult: await this.feedResultModal( - await this.searchFeed(keyword, limit) + await this.searchFeed(userId, keyword, limit) ), projectResult: await this.connectionhubResultModal( - await this.searchConnectionhub(keyword, limit) + await this.searchConnectionhub(userId, keyword, limit) ), }; break; case 'feed': result = { feedResult: await this.feedResultModal( - await this.searchFeed(keyword, limit) + await this.searchFeed(userId, keyword, limit) ), projectResult: { projects: [], hasMore: false }, }; @@ -34,7 +36,7 @@ export class SearchService { result = { feedResult: { feeds: [], hasMore: false }, projectResult: await this.connectionhubResultModal( - await this.searchConnectionhub(keyword, limit) + await this.searchConnectionhub(userId, keyword, limit) ), }; } @@ -43,7 +45,7 @@ export class SearchService { } // 피드 검색결과 조회 - async searchFeed(keyword: string, limit: number) { + async searchFeed(userId: number, keyword: string, limit: number) { const result = await this.prisma.feedPost.findMany({ where: { OR: [ @@ -64,7 +66,7 @@ export class SearchService { }, }, Tags: { select: { tag: { select: { name: true } } } }, - Likes: { select: { user_id: true } }, + Likes: { where: { user_id: userId } }, }, }); return result; @@ -114,7 +116,7 @@ export class SearchService { } // 커넥션허브 검색결과 조회 - async searchConnectionhub(keyword: string, limit: number) { + async searchConnectionhub(userId: number, keyword: string, limit: number) { const result = await this.prisma.projectPost.findMany({ where: { OR: [ @@ -152,7 +154,7 @@ export class SearchService { role: { select: { name: true } }, }, }, - Saves: { select: { user_id: true } }, + Saves: { where: { user_id: userId } }, }, }); From 6de9080b747a4fa2369ada90b2a4833e6c65714d Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 27 Jan 2025 08:56:11 +0900 Subject: [PATCH 316/414] =?UTF-8?q?[Feat]=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=EC=8B=9C=20saved=5Fcount=20=EC=A6=9D=EA=B0=80,=20=EA=B0=90?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 67 +++++++++++++++++--------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 4fdd85a..7a6f2aa 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -765,42 +765,63 @@ export class ProjectService { } async toggleBookmark(userId: number, projectId: number) { - // 북마크 존재 여부 확인 - const existingBookmark = await this.prisma.projectSave.findFirst({ - where: { user_id: userId, post_id: projectId }, + // 북마크 존재 여부 확인 + const existingBookmark = await this.prisma.projectSave.findFirst({ + where: { user_id: userId, post_id: projectId }, + }); + + if (existingBookmark) { + // 북마크 삭제 + await this.prisma.projectSave.delete({ + where: { id: existingBookmark.id }, }); - if (existingBookmark) { - // 북마크 삭제 - await this.prisma.projectSave.delete({ - where: { id: existingBookmark.id }, - }); - - return { - message: { - code: 200, - text: '북마크가 삭제되었습니다.', - }, - bookmarked: false, - }; - } - - // 북마크 추가 - await this.prisma.projectSave.create({ + // saved_count 감소 + await this.prisma.projectPost.update({ + where: { id: projectId }, data: { - user_id: userId, - post_id: projectId, + saved_count: { + decrement: 1, // saved_count 감소 + }, }, }); return { message: { code: 200, - text: '북마크가 추가되었습니다.', + text: '북마크가 삭제되었습니다.', }, + bookmarked: false, + }; + } + + // 북마크 추가 + await this.prisma.projectSave.create({ + data: { + user_id: userId, + post_id: projectId, + }, + }); + + // saved_count 증가 + await this.prisma.projectPost.update({ + where: { id: projectId }, + data: { + saved_count: { + increment: 1, // saved_count 증가 + }, + }, + }); + + return { + message: { + code: 200, + text: '북마크가 추가되었습니다.', bookmarked: true, }; } +} + async checkBookmark(userId: number, projectId: number) { // 북마크 여부 확인 From 14185782225df22fccedbcdedb0f0dfa4ed19236 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 08:59:49 +0900 Subject: [PATCH 317/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B2=80=EC=83=89=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index dbd9660..4dd37c8 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -44,6 +44,17 @@ export class SearchService { return result; } + // 피드 페이지 검색 핸들러 + async handleFeedPageSearch(user, keyword: string, cursor: number) { + const userId = user ? user.user_id : 0; + const limit = 10; + const posts = await this.feedResultPage( + await this.searchFeed(userId, keyword, limit) + ); + + return { posts }; + } + // 피드 검색결과 조회 async searchFeed(userId: number, keyword: string, limit: number) { const result = await this.prisma.feedPost.findMany({ From 7bea4b69d29d58f0f98a8a57889cbb7c6f2defb7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 09:04:40 +0900 Subject: [PATCH 318/414] =?UTF-8?q?[Feat]=20=EA=B2=80=EC=83=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EC=BB=A4=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.service.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/search/search.service.ts b/src/search/search.service.ts index 4dd37c8..bf27d65 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -16,18 +16,18 @@ export class SearchService { case 'all': result = { feedResult: await this.feedResultModal( - await this.searchFeed(userId, keyword, limit) + await this.searchFeed(userId, keyword, limit, 0) ), projectResult: await this.connectionhubResultModal( - await this.searchConnectionhub(userId, keyword, limit) + await this.searchConnectionhub(userId, keyword, limit, 0) ), }; break; case 'feed': result = { feedResult: await this.feedResultModal( - await this.searchFeed(userId, keyword, limit) + await this.searchFeed(userId, keyword, limit, 0) ), projectResult: { projects: [], hasMore: false }, }; @@ -36,7 +36,7 @@ export class SearchService { result = { feedResult: { feeds: [], hasMore: false }, projectResult: await this.connectionhubResultModal( - await this.searchConnectionhub(userId, keyword, limit) + await this.searchConnectionhub(userId, keyword, limit, 0) ), }; } @@ -49,16 +49,22 @@ export class SearchService { const userId = user ? user.user_id : 0; const limit = 10; const posts = await this.feedResultPage( - await this.searchFeed(userId, keyword, limit) + await this.searchFeed(userId, keyword, limit, 0) ); return { posts }; } // 피드 검색결과 조회 - async searchFeed(userId: number, keyword: string, limit: number) { + async searchFeed( + userId: number, + keyword: string, + limit: number, + cursor: number + ) { const result = await this.prisma.feedPost.findMany({ where: { + ...(cursor ? { id: { lt: cursor } } : {}), OR: [ { title: { contains: keyword } }, { content: { contains: keyword } }, @@ -127,9 +133,15 @@ export class SearchService { } // 커넥션허브 검색결과 조회 - async searchConnectionhub(userId: number, keyword: string, limit: number) { + async searchConnectionhub( + userId: number, + keyword: string, + limit: number, + cursor: number + ) { const result = await this.prisma.projectPost.findMany({ where: { + ...(cursor ? { id: { lt: cursor } } : {}), OR: [ { title: { contains: keyword } }, { content: { contains: keyword } }, From 7a24f13b29f86e82d96319da47f542c880002c55 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 09:12:32 +0900 Subject: [PATCH 319/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B2=80=EC=83=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.controller.ts | 15 +++++++++++++++ src/search/search.service.ts | 10 +++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index 002948f..d324a48 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -20,4 +20,19 @@ export class SearchController { category ); } + + // 피드 페이지에서 검색 + @Get('feed') + @UseGuards(OptionalAuthGuard) + async handleFeedSearch( + @Req() req, + @Query('keyword') keyword: string, + @Query('cursor') cursor: number + ) { + return await this.searchService.handleFeedPageSearch( + req.user, + keyword, + cursor + ); + } } diff --git a/src/search/search.service.ts b/src/search/search.service.ts index bf27d65..b93f988 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -49,10 +49,14 @@ export class SearchService { const userId = user ? user.user_id : 0; const limit = 10; const posts = await this.feedResultPage( - await this.searchFeed(userId, keyword, limit, 0) + await this.searchFeed(userId, keyword, limit, cursor) ); - - return { posts }; + const lastCursor = posts[posts.length - 1]?.postId || null; + return { + posts, + pagination: { lastCursor }, + message: { code: 200, text: '전체 피드를 정상적으로 조회했습니다.' }, + }; } // 피드 검색결과 조회 From 42eaae910ccbe6565b551c4afb5a8b4605914cb9 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Mon, 27 Jan 2025 09:24:22 +0900 Subject: [PATCH 320/414] =?UTF-8?q?[Feat]=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/search/search.controller.ts | 15 +++++++++++++++ src/search/search.service.ts | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index d324a48..4c3cc18 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -35,4 +35,19 @@ export class SearchController { cursor ); } + + // 커넥션허브 페이지에서 검색 + @Get('connectionhub') + @UseGuards(OptionalAuthGuard) + async handleConnectionhubSearch( + @Req() req, + @Query('keyword') keyword: string, + @Query('cursor') cursor: number + ) { + return await this.searchService.handleConnectionhubSearch( + req.user, + keyword, + cursor + ); + } } diff --git a/src/search/search.service.ts b/src/search/search.service.ts index b93f988..6a8a0e6 100644 --- a/src/search/search.service.ts +++ b/src/search/search.service.ts @@ -55,7 +55,24 @@ export class SearchService { return { posts, pagination: { lastCursor }, - message: { code: 200, text: '전체 피드를 정상적으로 조회했습니다.' }, + message: { code: 200, text: '피드 검색 결과 조회에 성공했습니다.' }, + }; + } + + // 커넥션허브 페이지 검색 핸들러 + async handleConnectionhubSearch(user, keyword: string, cursor: number) { + const userId = user ? user.user_id : 0; + const limit = 10; + + const projects = await this.connectionhubResultPage( + await this.searchConnectionhub(userId, keyword, limit, cursor) + ); + + const lastCursor = projects[projects.length - 1]?.projectId || null; + return { + projects, + pagination: { lastCursor }, + message: { code: 200, text: '커넥션허브 검색 결과 조회에 성공했습니다.' }, }; } @@ -238,6 +255,6 @@ export class SearchService { status: res.recruiting ? 'OPEN' : 'CLOSED', isMarked: res.Saves.length, })); - return { projects }; + return projects; } } From eeca5a8d3487e3b56bb9af18e0f8269bbb4ea403 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 27 Jan 2025 09:24:55 +0900 Subject: [PATCH 321/414] =?UTF-8?q?[Fix]=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=B8=EC=A6=9D=20=EA=B0=80=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 7df9f1b..d652955 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -241,8 +241,6 @@ export class ProjectController { } @Get(':projectId/bookmark') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() @CheckBookmarkDocs.ApiOperation @CheckBookmarkDocs.ApiParam @CheckBookmarkDocs.ApiResponse From e1d4e369e080a4dc0965fe699ef581f32669413c Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 27 Jan 2025 09:25:49 +0900 Subject: [PATCH 322/414] =?UTF-8?q?[Fix]=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=EC=8B=9C=20=EC=97=B0=EA=B4=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A6=9D=EA=B0=90=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 79 +++++++++++++------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 7a6f2aa..9e92a2e 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -765,23 +765,50 @@ export class ProjectService { } async toggleBookmark(userId: number, projectId: number) { - // 북마크 존재 여부 확인 - const existingBookmark = await this.prisma.projectSave.findFirst({ - where: { user_id: userId, post_id: projectId }, - }); - - if (existingBookmark) { - // 북마크 삭제 - await this.prisma.projectSave.delete({ - where: { id: existingBookmark.id }, + // 북마크 존재 여부 확인 + const existingBookmark = await this.prisma.projectSave.findFirst({ + where: { user_id: userId, post_id: projectId }, }); - // saved_count 감소 + if (existingBookmark) { + // 북마크 삭제 + await this.prisma.projectSave.delete({ + where: { id: existingBookmark.id }, + }); + + // saved_count 감소 + await this.prisma.projectPost.update({ + where: { id: projectId }, + data: { + saved_count: { + decrement: 1, // saved_count 감소 + }, + }, + }); + + return { + message: { + code: 200, + text: '북마크가 삭제되었습니다.', + }, + bookmarked: false, + }; + } + + // 북마크 추가 + await this.prisma.projectSave.create({ + data: { + user_id: userId, + post_id: projectId, + }, + }); + + // saved_count 증가 await this.prisma.projectPost.update({ where: { id: projectId }, data: { saved_count: { - decrement: 1, // saved_count 감소 + increment: 1, // saved_count 증가 }, }, }); @@ -789,39 +816,11 @@ export class ProjectService { return { message: { code: 200, - text: '북마크가 삭제되었습니다.', + text: '북마크가 추가되었습니다.', }, - bookmarked: false, - }; - } - - // 북마크 추가 - await this.prisma.projectSave.create({ - data: { - user_id: userId, - post_id: projectId, - }, - }); - - // saved_count 증가 - await this.prisma.projectPost.update({ - where: { id: projectId }, - data: { - saved_count: { - increment: 1, // saved_count 증가 - }, - }, - }); - - return { - message: { - code: 200, - text: '북마크가 추가되었습니다.', bookmarked: true, }; } -} - async checkBookmark(userId: number, projectId: number) { // 북마크 여부 확인 From 82f56a8d2b8315a7eb762241a5704bea9846defa Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Mon, 27 Jan 2025 15:19:12 +0900 Subject: [PATCH 323/414] =?UTF-8?q?[Fix]=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index d652955..04454d3 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -141,7 +141,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.user_id;; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.applyToProject(userId, projectId); } @@ -165,7 +165,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.user_id;; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.checkApplyStatus(userId, projectId); } @@ -179,7 +179,7 @@ export class ProjectController { @Param('projectId', ParseIntPipe) projectId: number, @Req() req ) { - const userId = req.user.user_id;; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.cancelApplication(userId, projectId); } @@ -197,7 +197,7 @@ export class ProjectController { @Body('status') status: 'Accepted' | 'Rejected' | 'Pending', @Req() req ) { - const userId = req.user.user_id;; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.updateApplicationStatus( userId, projectId, @@ -218,7 +218,7 @@ export class ProjectController { @Body('recruiting') recruiting: boolean, @Req() req ) { - const userId = req.user.user_id;; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.updateProjectStatus( userId, projectId, @@ -241,6 +241,8 @@ export class ProjectController { } @Get(':projectId/bookmark') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @CheckBookmarkDocs.ApiOperation @CheckBookmarkDocs.ApiParam @CheckBookmarkDocs.ApiResponse From 9fc907310f3c8b03058946741151f6989fa96ed8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Tue, 28 Jan 2025 08:38:37 +0900 Subject: [PATCH 324/414] =?UTF-8?q?[Refactor]=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 59 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index a44b814..ca6ad79 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -7,6 +7,35 @@ import { SearchMessageDto } from './dto/serchMessage.dto'; export class ChatService { constructor(private readonly prisma: PrismaService) {} + // 온라인 유저 DB에 저장 + async addUserOnline(userId: number, clientId: string) { + await this.prisma.online_users.create({ + data: { + user_id: userId, + client_id: clientId, + }, + }); + } + + // 오프라인 유저 DB에서 삭제 + async deleteUserOnline(userId: number) { + await this.prisma.online_users.deleteMany({ + where: { + user_id: userId, + }, + }); + } + + // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 + async getSocketIds(Ids: number[]) { + const socketIds = await this.prisma.online_users.findMany({ + where: { user_id: { in: Ids } }, + select: { client_id: true }, + }); + + return socketIds.map(id => id.client_id); + } + // 기존 채널 조회 or 새 채널 생성 // 채널id 리턴 (개인 채팅방) async getChannelId(userId1: number, userId2: number) { @@ -135,35 +164,6 @@ export class ChatService { return data; } - // 온라인 유저 DB에 저장 - async addUserOnline(userId: number, clientId: string) { - await this.prisma.online_users.create({ - data: { - user_id: userId, - client_id: clientId, - }, - }); - } - - // 오프라인 유저 DB에서 삭제 - async deleteUserOnline(userId: number) { - await this.prisma.online_users.deleteMany({ - where: { - user_id: userId, - }, - }); - } - - // 유저 아이디를 통해 유저의 소켓 아이디 가져오기 - async getSocketIds(Ids: number[]) { - const socketIds = await this.prisma.online_users.findMany({ - where: { user_id: { in: Ids } }, - select: { client_id: true }, - }); - - return socketIds.map(id => id.client_id); - } - // 유저가 참여한 채널 전체 조회 async getAllChannels(id: number) { const result = await this.prisma.channel_users.findMany({ @@ -468,6 +468,7 @@ export class ChatService { return data; } + // 유저 채널에서 삭제 async deleteUser(userId: number, channelId: number) { await this.prisma.channel_users.deleteMany({ where: { From 3fd48ae7a1b61812ba787868b491cf9cb6266535 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 28 Jan 2025 13:04:13 +0900 Subject: [PATCH 325/414] =?UTF-8?q?[Feat]=20Follow=20Module=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 5f9ef0d..8c95788 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,8 +5,9 @@ import { UserModule } from '@modules/user/user.module'; import { ChatGateway } from './chat/chat.gateway'; import { ChatModule } from './chat/chat.module'; import { FeedModule } from './feed/feed.module'; -import { ProjectModule } from './modules/project/project.module'; +import { ProjectModule } from '@modules/project/project.module'; import { SearchModule } from './search/search.module'; +import { FollowModule } from '@modules/follow/follow.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -17,7 +18,8 @@ import { SearchModule } from './search/search.module'; ChatModule, FeedModule, ProjectModule, - SearchModule + SearchModule, + FollowModule, ], providers: [ChatGateway], }) From e35c8984e0c35202c543fd0664de846c0c34150a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 28 Jan 2025 13:04:21 +0900 Subject: [PATCH 326/414] =?UTF-8?q?[Feat]=20Follow=20Module=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.module.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/modules/follow/follow.module.ts diff --git a/src/modules/follow/follow.module.ts b/src/modules/follow/follow.module.ts new file mode 100644 index 0000000..dea6a84 --- /dev/null +++ b/src/modules/follow/follow.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FollowController } from './follow.controller'; +import { PrismaModule } from '@prisma/prisma.module'; +import { FollowService } from '@modules/follow/follow.service'; +@Module({ + imports: [PrismaModule], + controllers: [FollowController], + providers: [FollowService], +}) +export class FollowModule {} From fb761d9a52f6f56ed3fbf98095b6c1c4998a27d6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 28 Jan 2025 13:04:32 +0900 Subject: [PATCH 327/414] =?UTF-8?q?[Feat]=20Follow=20Toggle=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.service.ts | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/modules/follow/follow.service.ts diff --git a/src/modules/follow/follow.service.ts b/src/modules/follow/follow.service.ts new file mode 100644 index 0000000..e7c29ca --- /dev/null +++ b/src/modules/follow/follow.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@prisma/prisma.service'; + +@Injectable() +export class FollowService { + constructor(private readonly prisma: PrismaService) {} + + async toggleFollow(userId: number, targetUserId: number) { + // 현재 팔로우 상태 확인 + const existingFollow = await this.prisma.follows.findFirst({ + where: { + following_user_id: userId, + followed_user_id: targetUserId, + }, + }); + + if (existingFollow) { + // 언팔로우 처리 + await this.prisma.follows.delete({ + where: { id: existingFollow.id }, + }); + + return { + message: { code: 200, text: '언팔로우 성공' }, + isFollowing: false, + }; + } else { + // 팔로우 처리 + await this.prisma.follows.create({ + data: { + following_user_id: userId, + followed_user_id: targetUserId, + }, + }); + + return { + message: { code: 200, text: '팔로우 성공' }, + isFollowing: true, + }; + } + } +} From a412150530c719c11e323e616a1791341e5c3c8a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 28 Jan 2025 13:04:41 +0900 Subject: [PATCH 328/414] =?UTF-8?q?[Feat]=20Follow=20Toggle=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.controller.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/modules/follow/follow.controller.ts diff --git a/src/modules/follow/follow.controller.ts b/src/modules/follow/follow.controller.ts new file mode 100644 index 0000000..55b6da8 --- /dev/null +++ b/src/modules/follow/follow.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; +import { FollowService } from '@modules/follow/follow.service'; + +@UseGuards(JwtAuthGuard) +@Controller('follow') +export class FollowController { + constructor(private readonly followService: FollowService) {} + + @Post(':targetUserId') + async togggleFollow( + @Req() req: any, + @Param('targetUserId') targetUserId: string + ) { + const userId = req.user.user_id; + const numTargetUserId = parseInt(targetUserId); + return this.followService.toggleFollow(userId, numTargetUserId); + } +} From d6140f6e7efd9916e253413959b5d6e28f3c7e73 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 01:07:48 +0900 Subject: [PATCH 329/414] =?UTF-8?q?[Fix]=20prettier=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index e358926..e928d05 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -706,7 +706,7 @@ export const AddUserLinksDocs = { description: '추가할 링크 목록', schema: { example: { - url: 'https://github.com/user' + url: 'https://github.com/user', }, }, }), From a40f7efb1cfaa55f0000ed3fac3f8401ad64cd72 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 01:09:23 +0900 Subject: [PATCH 330/414] =?UTF-8?q?[Fix]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=20API=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 2 +- src/modules/project/project.module.ts | 1 - src/modules/project/project.service.ts | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 04454d3..1329070 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -81,7 +81,7 @@ export class ProjectController { @Body() updateProjectDto: CreateProjectDto, @Req() req ) { - const userId = req.user.id; // 인증된 사용자 ID + const userId = req.user.user_id; // 인증된 사용자 ID return this.projectService.updateProject( userId, projectId, diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts index 1042561..d1a8ae9 100644 --- a/src/modules/project/project.module.ts +++ b/src/modules/project/project.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { ProjectController } from './project.controller'; import { ProjectService } from './project.service'; import { AuthModule } from '@modules/auth/auth.module'; -import { UserService } from '@modules/user/user.service'; import { PrismaService } from '@prisma/prisma.service'; import { S3Module } from '@src/s3/s3.module'; diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 9e92a2e..d69f14d 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -601,12 +601,11 @@ export class ProjectService { } // 게시글 권한 확인 - async feedAuth(userId: number, projectId: number) { + async feedAuth(userId: number, projectId: number): Promise { const auth = await this.prisma.projectPost.findUnique({ where: { id: projectId }, select: { user_id: true }, }); - return auth.user_id === userId; } From ab8f775ec72acc67b3b5226fba390bd2564c1ba7 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 01:10:11 +0900 Subject: [PATCH 331/414] =?UTF-8?q?[Fix]=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/follow/follow.controller.ts b/src/modules/follow/follow.controller.ts index 55b6da8..4ad3320 100644 --- a/src/modules/follow/follow.controller.ts +++ b/src/modules/follow/follow.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { FollowService } from '@modules/follow/follow.service'; From a8542915858ddf66569b71a0751a30ce78b7fa50 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 01:56:23 +0900 Subject: [PATCH 332/414] =?UTF-8?q?[Fix]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=9D=B8=EC=A6=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts index 1329070..d31c283 100644 --- a/src/modules/project/project.controller.ts +++ b/src/modules/project/project.controller.ts @@ -109,6 +109,8 @@ export class ProjectController { } @Post('image') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @UseInterceptors(FileInterceptor('file')) @UploadFeedImageDocs.ApiOperation @UploadFeedImageDocs.ApiConsumes @@ -116,6 +118,7 @@ export class ProjectController { @UploadFeedImageDocs.ApiResponse async func(@Req() req, @UploadedFile() file: Express.Multer.File) { const userId = req.user.user_id; + console.log(userId); return await this.projectService.uploadFeedImage(userId, file); } From 8b70daa37db535cd75635df708d052f8cbc7990d Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 14:30:08 +0900 Subject: [PATCH 333/414] =?UTF-8?q?[Fix]=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index d69f14d..b62b283 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -43,7 +43,12 @@ export class ProjectService { throw new BadRequestException('유효하지 않은 커서 값입니다.'); } - where.id = { gt: cursor }; // 유효한 커서 이후의 데이터 가져오기 + // 정렬 조건에 따라 커서 조건 추가 + if (sort) { + where.created_at = { lt: validCursor.created_at }; // created_at 기준 + } else { + where.saved_count = { lt: validCursor.saved_count }; // saved_count 기준 + } } const orderBy: any[] = []; From 48484940e6beb6f1359653a15a5e9cfd0e87c4dc Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Wed, 29 Jan 2025 22:40:29 +0900 Subject: [PATCH 334/414] =?UTF-8?q?[Fix]=20Open=20Closed=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index b62b283..d12efbc 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -212,7 +212,7 @@ export class ProjectService { startDate: project.start_date, duration: project.duration, workType: project.work_type, - status: project.recruiting ? 'OPEN' : 'CLOSE', + status: project.recruiting ? 'OPEN' : 'CLOSED', viewCount: project.view, applyCount: 0, bookmarkCount: 0, @@ -372,7 +372,7 @@ export class ProjectService { startDate: project.start_date, duration: project.duration, workType: project.work_type, - status: project.recruiting ? 'OPEN' : 'CLOSE', + status: project.recruiting ? 'OPEN' : 'CLOSED', skills: project.Tags.map(t => t.tag.name), detailRoles: project.Details.map(d => d.detail_role.name), viewCount: project.view, // 이미 증가된 view 값을 사용 @@ -737,7 +737,7 @@ export class ProjectService { project: { projectId: updatedProject.id, recruiting: updatedProject.recruiting, - status: recruiting ? 'OPEN' : 'CLOSE', + status: recruiting ? 'OPEN' : 'CLOSED', }, }; } From 7acfa86e008b0bf072b4ed2c19205a95e546cc63 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 30 Jan 2025 14:51:58 +0900 Subject: [PATCH 335/414] =?UTF-8?q?[Fix]=20=EC=A7=80=EC=9B=90=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/docs/project.docs.ts | 1 + src/modules/project/project.service.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/modules/project/docs/project.docs.ts b/src/modules/project/docs/project.docs.ts index 820138d..6e79691 100644 --- a/src/modules/project/docs/project.docs.ts +++ b/src/modules/project/docs/project.docs.ts @@ -384,6 +384,7 @@ export const GetApplicantsDocs = { name: 'Lee Chan', nickname: 'leechan_dev', profileUrl: 'https://example.com/profile.png', + status: 'Pending', }, ], message: { diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index d12efbc..142b399 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -453,6 +453,7 @@ export class ProjectService { introduce: true, }, }, + status: true, }, }); const resultapplicants = applicants.map(applicant => ({ @@ -460,6 +461,7 @@ export class ProjectService { name: applicant.user.name, nickname: applicant.user.nickname, profileUrl: applicant.user.profile_url, + status: applicant.status, })); return { applicants: resultapplicants, From 46ff58bd918df2f7a0874fc1c21d908f3bcfc88b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 30 Jan 2025 19:00:15 +0900 Subject: [PATCH 336/414] =?UTF-8?q?[Fix]=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 16 ++++++++++++++++ .../notification/notification.module.ts | 10 ++++++++++ .../notification/notification.service.ts | 19 +++++++++++++++++++ src/modules/project/project.service.ts | 2 +- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/modules/notification/notification.controller.ts create mode 100644 src/modules/notification/notification.module.ts create mode 100644 src/modules/notification/notification.service.ts diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts new file mode 100644 index 0000000..61f66c4 --- /dev/null +++ b/src/modules/notification/notification.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Sse } from '@nestjs/common'; +import { Observable, Subject } from 'rxjs'; + +@Controller('notifications') +export class NotificationsController { + private notifications$ = new Subject(); + + @Sse() + streamNotifications(): Observable { + return this.notifications$.asObservable(); + } + + sendNotification(data: any) { + this.notifications$.next(data); + } +} diff --git a/src/modules/notification/notification.module.ts b/src/modules/notification/notification.module.ts new file mode 100644 index 0000000..198a143 --- /dev/null +++ b/src/modules/notification/notification.module.ts @@ -0,0 +1,10 @@ +// import { Module } from '@nestjs/common'; +// import { NotificationsController } from './notification.controller'; +// import { NotificationsService } from '@modules/notification/notification.service'; +// import { AuthModule } from '@modules/auth/auth.module'; +// @Module({ +// imports: [AuthModule], +// controllers: [NotificationsController], +// providers: [NotificationsService], +// }) +// export class NotificationModule {} diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts new file mode 100644 index 0000000..8d8acc4 --- /dev/null +++ b/src/modules/notification/notification.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { NotificationsController } from '@src/modules/notification/notification.controller'; + +@Injectable() +export class NotificationsService { + constructor( + private readonly notificationsController: NotificationsController + ) {} + + sendEvent(userId: number, eventType: string, payload: any) { + const notification = { + userId, + eventType, + payload, + timestamp: new Date(), + }; + this.notificationsController.sendNotification(notification); + } +} diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 142b399..51961db 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -89,7 +89,7 @@ export class ProjectService { workType: project.work_type, applyCount: project.Applications.length, bookMarkCount: project.saved_count, - viewCount: project.view + 1, + viewCount: project.view, status: project.recruiting ? 'OPEN' : 'CLOSED', createdAt: project.created_at, user: { From f64c1f9460849aa9b6df3516788078d5489e47fd Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 30 Jan 2025 19:01:39 +0900 Subject: [PATCH 337/414] =?UTF-8?q?[Fix]=20=EC=9C=A0=EC=A0=80=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C,=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EB=B0=8F=20=EC=BF=BC=EB=A6=AC,=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=20=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 35 +++--- src/modules/user/user.controller.ts | 45 +++---- src/modules/user/user.service.ts | 186 ++++++++++++++++------------ 3 files changed, 142 insertions(+), 124 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index e928d05..ce27c63 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -1003,9 +1003,7 @@ export const GetUserFeedPostsDocs = { tags: ['Tag1', 'Tag2'], }, ], - totalCount: 1, - currentPage: 1, - totalPages: 1, + pagination: { lastCursor: 10 }, }, }, }), @@ -1047,26 +1045,27 @@ export const GetUserConnectionHubProjectsDocs = { description: '커넥션 허브 프로젝트 조회 성공', schema: { example: { - message: { - code: 200, - text: '사용자 커넥션허브 조회에 성공했습니다.', - }, + message: { code: 200, text: '프로젝트 조회 성공' }, projects: [ { projectPostId: 1, - title: 'Project 1', - content: 'Description of project 1', - thumbnailUrl: 'https://example.com/project-thumbnail.jpg', - startDate: '2023-01-01', - duration: '6 months', - recruiting: true, - view: 100, - tags: ['Node.js', 'TypeScript'], + title: '프로젝트 제목', + content: '프로젝트 설명', + thumbnailUrl: 'https://example.com/thumbnail.jpg', + role: 'Programmer', + skills: ['React', 'TypeScript'], + detailRoles: ['프론트엔드 개발자'], + hubType: 'Project', + startDate: '2024-01-01', + duration: 6, + workType: 'OFFLINE', + applyCount: 10, + bookMarkCount: 5, + viewCount: 100, + status: 'OPEN', }, ], - totalCount: 1, - currentPage: 1, - totalPages: 1, + pagination: { lastCursor: 10 }, }, }, }), diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 1aea29d..332b209 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -426,60 +426,49 @@ export class UserController { @GetUserFeedPostsDocs.ApiOperation @GetUserFeedPostsDocs.ApiParam @ApiQuery({ - name: 'page', + name: 'cursor', required: false, - description: '페이지 번호 (기본값: 1)', - type: 'number', - }) - @ApiQuery({ - name: 'limit', - required: false, - description: '페이지 당 항목 수 (기본값: 10)', - type: 'number', + type: Number, + description: '마지막으로 조회된 피드 ID (무한스크롤 구현을 위한 커서)', }) @GetUserFeedPostsDocs.ApiResponse async getUserFeedPosts( @Param('userId') userId: string, - @Query('page') page: number = 1, - @Query('limit') limit: number = 10 + @Query('cursor') cursor?: number ) { const numUserId = parseInt(userId, 10); - return this.userService.getFeeds(numUserId, page, limit); + const limit = 10; + return this.userService.getFeeds(numUserId, cursor, limit); } @Get(':userId/connection-hub') @GetUserConnectionHubProjectsDocs.ApiOperation @GetUserConnectionHubProjectsDocs.ApiParam @ApiQuery({ - name: 'type', - required: true, - description: "프로젝트 유형 ('applied' 또는 'created')", - enum: ['applied', 'created'], - }) - @ApiQuery({ - name: 'page', + name: 'cursor', required: false, - description: '페이지 번호 (기본값: 1)', - type: 'number', + type: Number, + description: '마지막으로 조회된 프로젝트 ID (무한스크롤 구현을 위한 커서)', }) @ApiQuery({ - name: 'limit', + name: 'type', required: false, - description: '페이지 당 항목 수 (기본값: 10)', - type: 'number', + enum: ['applied', 'created'], + description: + '프로젝트 유형 (`created`: 생성한 프로젝트, `applied`: 지원한 프로젝트)', }) @GetUserConnectionHubProjectsDocs.ApiResponse async getUserConnectionHubProjects( @Param('userId') userId: string, - @Query('type') type: 'applied' | 'created', - @Query('page') page: number = 1, - @Query('limit') limit: number = 10 + @Query('cursor') cursor?: number, + @Query('type') type: 'applied' | 'created' = 'created' // type 기본값 설정 ) { const numUserId = parseInt(userId, 10); + const limit = 10; return this.userService.getConnectionHubProjects( numUserId, type, - page, + cursor, limit ); } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 8938b68..84defdf 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -529,26 +529,59 @@ export class UserService { projectAlert?: boolean; } ) { - const updatedUser = await this.prisma.user.update({ - where: { id: userId }, - data: { - push_alert: notifications.pushAlert, - following_alert: notifications.followingAlert, - project_alert: notifications.projectAlert, - }, - }); + // Validation + if ( + notifications.pushAlert === undefined && + notifications.followingAlert === undefined && + notifications.projectAlert === undefined + ) { + return { + message: { + code: 400, + text: '업데이트할 알림 설정이 제공되지 않았습니다.', + }, + }; + } - return { - message: { - code: 200, - text: '알림 설정이 성공적으로 업데이트되었습니다.', - }, - notifications: { - pushAlert: updatedUser.push_alert, - followingAlert: updatedUser.following_alert, - projectAlert: updatedUser.project_alert, - }, - }; + const updateData: any = {}; + + if (notifications.pushAlert !== undefined) { + updateData.push_alert = notifications.pushAlert; + } + if (notifications.followingAlert !== undefined) { + updateData.following_alert = notifications.followingAlert; + } + if (notifications.projectAlert !== undefined) { + updateData.project_alert = notifications.projectAlert; + } + + try { + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: updateData, + select: { + push_alert: true, + following_alert: true, + project_alert: true, + }, + }); + + return { + message: { + code: 200, + text: '알림 설정이 성공적으로 업데이트되었습니다.', + }, + notifications: { + pushAlert: updatedUser.push_alert, + followingAlert: updatedUser.following_alert, + projectAlert: updatedUser.project_alert, + }, + }; + } catch (error) { + throw new Error( + `알림 설정 업데이트 중 오류가 발생했습니다: ${error.message}` + ); + } } async addUserSkills(userId: number, skills: string[]) { @@ -944,15 +977,13 @@ export class UserService { }; } - async getFeeds(userId: number, page: number = 1, limit: number = 10) { - // Offset 계산 - const offset = (page - 1) * limit; - - // 특정 유저의 피드 조회 + async getFeeds(userId: number, cursor?: number, limit: number = 10) { + // 특정 유저의 피드 조회 (cursor 기반 페이징) const feeds = await this.prisma.feedPost.findMany({ where: { user_id: userId }, // 특정 유저의 글만 가져옴 - skip: offset, - take: limit, + take: limit, // 가져올 개수 + skip: cursor ? 1 : undefined, // 커서가 있는 경우 첫 번째 항목 제외 + cursor: cursor ? { id: cursor } : undefined, // 커서 적용 orderBy: { created_at: 'desc' }, include: { user: { @@ -970,16 +1001,16 @@ export class UserService { }, }); - // 총 피드 개수 (페이지네이션 용) - const totalCount = await this.prisma.feedPost.count({ - where: { user_id: userId }, // 특정 유저의 글만 카운트 - }); + // 마지막 커서 설정 (다음 요청을 위해) + const lastCursor = feeds.length > 0 ? feeds[feeds.length - 1].id : null; - // 반환 데이터 구성 return { message: { code: 200, - text: '사용자 피드 조회에 성공했습니다.', + text: + feeds.length > 0 + ? '사용자 피드 조회에 성공했습니다.' + : '더 이상 데이터가 없습니다.', }, feeds: feeds.map(feed => ({ id: feed.id, @@ -997,74 +1028,62 @@ export class UserService { }, tags: feed.Tags.map(tag => tag.tag.name), })), - totalCount, - currentPage: page, - totalPages: Math.ceil(totalCount / limit), + pagination: { + lastCursor, // 다음 요청을 위한 커서 반환 + }, }; } async getConnectionHubProjects( userId: number, - type: 'applied' | 'created', - page: number = 1, - limit: number = 10 + type: 'applied' | 'created' = 'created', // 기본값 설정 + cursor?: number, // cursor 추가 + limit: number = 10 // limit 기본값 ) { - const offset = (page - 1) * limit; - - let projectsQuery, totalCountQuery; + const safeLimit = limit || 10; // Prisma에 전달할 안전한 limit 값 + let projectsQuery; - // 쿼리 조건 설정 if (type === 'applied') { + // 지원한 프로젝트 projectsQuery = this.prisma.userApplyProject.findMany({ where: { user_id: userId }, - skip: offset, - take: limit, + take: safeLimit, + skip: cursor ? 1 : undefined, // 커서가 있는 경우 첫 번째 항목 제외 + cursor: cursor ? { id: cursor } : undefined, // 커서 설정 include: { post: { include: { - Tags: { - include: { tag: true }, // 태그 정보 포함 - }, + Tags: { select: { tag: { select: { name: true } } } }, + Applications: { select: { id: true } }, + Details: { select: { detail_role: { select: { name: true } } } }, }, }, }, orderBy: { - post: { - created_at: 'desc', // post의 created_at 기준으로 정렬 - }, + post: { created_at: 'desc' }, }, }); - - totalCountQuery = this.prisma.userApplyProject.count({ - where: { user_id: userId }, - }); } else if (type === 'created') { + // 생성한 프로젝트 projectsQuery = this.prisma.projectPost.findMany({ where: { user_id: userId }, - skip: offset, - take: limit, + take: safeLimit, + skip: cursor ? 1 : undefined, // 커서가 있는 경우 첫 번째 항목 제외 + cursor: cursor ? { id: cursor } : undefined, // 커서 설정 include: { - Tags: { - include: { tag: true }, // 태그 정보 포함 - }, + Tags: { select: { tag: { select: { name: true } } } }, + Applications: { select: { id: true } }, + Details: { select: { detail_role: { select: { name: true } } } }, }, orderBy: { created_at: 'desc' }, }); - - totalCountQuery = this.prisma.projectPost.count({ - where: { user_id: userId }, - }); } else { throw new BadRequestException('유효하지 않은 타입입니다.'); } - // 쿼리 실행 - const [projects, totalCount] = await Promise.all([ - projectsQuery, - totalCountQuery, - ]); + const projects = await projectsQuery; - // 데이터 매핑 + // 데이터 포맷팅 const formattedProjects = projects.map(project => { const projectData = type === 'applied' ? project.post : project; return { @@ -1072,23 +1091,34 @@ export class UserService { title: projectData.title, content: projectData.content, thumbnailUrl: projectData.thumbnail_url, - startDate: projectData.start_date, - duration: `${projectData.unit}`, - recruiting: projectData.recruiting, - view: projectData.view, - tags: projectData.Tags.map(tag => tag.tag.name), + role: projectData.role, + skills: projectData.Tags.map(tag => `${tag.tag.name}`), + detailRoles: projectData.Details.map(d => `${d.detail_role.name}`), + hubType: projectData.hub_type, + startDate: projectData.start_date.toISOString().split('T')[0], + duration: projectData.duration, + workType: projectData.work_type, + applyCount: projectData.Applications.length, + bookMarkCount: projectData.saved_count, + viewCount: projectData.view, + status: projectData.recruiting ? 'OPEN' : 'CLOSED', }; }); + const lastCursor = projects[projects.length - 1]?.id || null; // 마지막 커서 설정 + return { message: { code: 200, - text: '사용자 커넥션허브 조회에 성공했습니다.', + text: + formattedProjects.length > 0 + ? '프로젝트 조회 성공' + : '더 이상 데이터가 없습니다.', }, projects: formattedProjects, - totalCount, - currentPage: page, - totalPages: Math.ceil(totalCount / limit), + pagination: { + lastCursor, + }, }; } From 9a4927937b87a8b74721913e8d9145f931d0c012 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Thu, 30 Jan 2025 19:05:07 +0900 Subject: [PATCH 338/414] =?UTF-8?q?[Fix]=20Comment=20out=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.module.ts | 1 + src/modules/follow/follow.service.ts | 23 +++++-- .../notification/notification.controller.ts | 57 +++++++++++++---- .../notification/notification.module.ts | 3 +- .../notification/notification.service.ts | 63 ++++++++++++++----- 5 files changed, 111 insertions(+), 36 deletions(-) diff --git a/src/modules/follow/follow.module.ts b/src/modules/follow/follow.module.ts index dea6a84..1e8fe28 100644 --- a/src/modules/follow/follow.module.ts +++ b/src/modules/follow/follow.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { FollowController } from './follow.controller'; import { PrismaModule } from '@prisma/prisma.module'; import { FollowService } from '@modules/follow/follow.service'; +//import { NotificationModule } from '../notification/notification.module'; @Module({ imports: [PrismaModule], controllers: [FollowController], diff --git a/src/modules/follow/follow.service.ts b/src/modules/follow/follow.service.ts index e7c29ca..c9640c9 100644 --- a/src/modules/follow/follow.service.ts +++ b/src/modules/follow/follow.service.ts @@ -1,9 +1,13 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; +//import { NotificationsService } from '@modules/notification/notification.service'; @Injectable() export class FollowService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + //private readonly notificationsService: NotificationsService + ) {} async toggleFollow(userId: number, targetUserId: number) { // 현재 팔로우 상태 확인 @@ -33,10 +37,19 @@ export class FollowService { }, }); - return { - message: { code: 200, text: '팔로우 성공' }, - isFollowing: true, - }; + // 알림 생성 및 SSE 전송 + // const message = `사용자 ${userId}님이 당신을 팔로우하기 시작했습니다.`; + // await this.notificationsService.sendRealTimeNotification( + // targetUserId, + // { type: 'follow', message }, + // this.notificationsService.notifications$ // SSE 스트림 + // ); + // return { + // message: { code: 200, text: '팔로우 성공' }, + // isFollowing: true, + // }; + // } + // } } } } diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 61f66c4..00d12c9 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -1,16 +1,47 @@ -import { Controller, Sse } from '@nestjs/common'; -import { Observable, Subject } from 'rxjs'; +// import { Controller, Get, Sse, Param, Patch, Body, Req } from '@nestjs/common'; +// import { Observable, Subject } from 'rxjs'; +// import { map } from 'rxjs/operators'; +// import { NotificationsService } from './notification.service'; -@Controller('notifications') -export class NotificationsController { - private notifications$ = new Subject(); +// @Controller('notifications') +// export class NotificationsController { +// private notifications$ = new Subject(); - @Sse() - streamNotifications(): Observable { - return this.notifications$.asObservable(); - } +// constructor(private readonly notificationService: NotificationsService) {} - sendNotification(data: any) { - this.notifications$.next(data); - } -} +// // SSE 스트림 +// @Sse('stream') +// streamNotifications(@Req() req: any): Observable { +// req.on('close', () => { +// console.log('클라이언트 연결 종료'); +// }); + +// return this.notifications$.asObservable().pipe( +// map(data => ({ +// data, +// type: 'notification', +// })) +// ); +// } + +// // 알림 전송 메서드 +// sendNotification(data: any) { +// this.notifications$.next(data); +// } +// // 특정 사용자의 알림 조회 +// @Get() +// async getUserNotifications(@Req() req: any) { +// const userId = req.user.user_id; // 인증된 사용자 정보에서 ID 가져오기 +// if (!userId) { +// throw new Error('사용자 ID를 찾을 수 없습니다.'); +// } + +// return this.notificationService.getNotificationsForUser(userId); +// } + +// // 알림 읽음 처리 +// @Patch(':id/read') +// async markNotificationAsRead(@Param('id') notificationId: number) { +// return await this.notificationService.markAsRead(notificationId); +// } +// } diff --git a/src/modules/notification/notification.module.ts b/src/modules/notification/notification.module.ts index 198a143..c386b0d 100644 --- a/src/modules/notification/notification.module.ts +++ b/src/modules/notification/notification.module.ts @@ -2,8 +2,9 @@ // import { NotificationsController } from './notification.controller'; // import { NotificationsService } from '@modules/notification/notification.service'; // import { AuthModule } from '@modules/auth/auth.module'; +// import { PrismaModule } from '@src/prisma/prisma.module'; // @Module({ -// imports: [AuthModule], +// imports: [AuthModule, PrismaModule], // controllers: [NotificationsController], // providers: [NotificationsService], // }) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 8d8acc4..f2f16fd 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -1,19 +1,48 @@ -import { Injectable } from '@nestjs/common'; -import { NotificationsController } from '@src/modules/notification/notification.controller'; +// import { Injectable } from '@nestjs/common'; +// import { PrismaService } from '@src/prisma/prisma.service'; +// import { Subject } from 'rxjs'; -@Injectable() -export class NotificationsService { - constructor( - private readonly notificationsController: NotificationsController - ) {} +// @Injectable() +// export class NotificationsService { +// constructor(private readonly prisma: PrismaService) {} - sendEvent(userId: number, eventType: string, payload: any) { - const notification = { - userId, - eventType, - payload, - timestamp: new Date(), - }; - this.notificationsController.sendNotification(notification); - } -} +// // 특정 사용자 알림 조회 +// async getNotificationsForUser(userId: number, isRead?: boolean) { +// return this.prisma.notification.findMany({ +// where: { +// userId, +// ...(isRead !== undefined && { isRead }), +// }, +// orderBy: { createdAt: 'desc' }, +// }); +// } + +// // 알림 생성 +// async createNotification(userId: number, type: string, message: string) { +// return this.prisma.notification.create({ +// data: { userId, type, message }, +// }); +// } + +// // 알림 읽음 처리 +// async markAsRead(notificationId: number) { +// return this.prisma.notification.update({ +// where: { id: notificationId }, +// data: { isRead: true }, +// }); +// } + +// // SSE 알림 전송 +// async sendRealTimeNotification( +// userId: number, +// data: any, +// notifications$: Subject +// ) { +// const notification = await this.createNotification( +// userId, +// data.type, +// data.message +// ); +// notifications$.next({ data: notification, type: 'notification' }); +// } +// } From 6ecc2a3d9f8eacc1c4116f1f24fa2d405f5c62ba Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 00:11:47 +0900 Subject: [PATCH 339/414] =?UTF-8?q?[Feat]=20=EC=BB=A4=EB=84=A5=EC=85=98=20?= =?UTF-8?q?=ED=97=88=EB=B8=8C=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20createdAt=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/user.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 84defdf..2bb7ffe 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1102,6 +1102,7 @@ export class UserService { bookMarkCount: projectData.saved_count, viewCount: projectData.view, status: projectData.recruiting ? 'OPEN' : 'CLOSED', + createdAt: projectData.created_at, }; }); From b9775a4e5d5ec5a16d5cb31bf5bcdcd2f06f9a55 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:01:56 +0900 Subject: [PATCH 340/414] =?UTF-8?q?[Fix]=20db=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e109ea..c38b580 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,6 +38,8 @@ model User { Message_status Message_status[] MyPageProject MyPageProject[] UserLinks MyPageUserLink[] + notifications Notification[] @relation("UserNotifications") // 받는 알림 + sentNotifications Notification[] @relation("SenderNotifications") // 보낸 알림 ProgrammerData ProgrammerData? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] @@ -376,3 +378,17 @@ model online_users { client_id String user_id Int } + +model Notification { + id Int @id @default(autoincrement()) + userId Int + senderId Int // 알림을 보낸 사용자 ID + type String + message String + isRead Boolean @default(false) + createdAt DateTime @default(now()) + user User @relation("UserNotifications", fields: [userId], references: [id]) + sender User @relation("SenderNotifications", fields: [senderId], references: [id]) + + @@index([userId], map: "Notification_userId_fkey") +} From 90bfbe0f50d90c2d6f717a1a8ba4ccbf43ab7249 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:02:23 +0900 Subject: [PATCH 341/414] =?UTF-8?q?[Fix]=20prettier=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 883d364..e4aabbf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,11 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); app.enableCors({ - origin: ['http://localhost:5173', 'http://localhost:8080', 'https://p-a-d.store'], + origin: [ + 'http://localhost:5173', + 'http://localhost:8080', + 'https://p-a-d.store', + ], credentials: true, exposedHeaders: ['Authorization'], }); From a982c6e3ddf1bc7990e5250e51d3908f1333ce0a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:03:30 +0900 Subject: [PATCH 342/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A7=80=EC=9B=90=EC=8B=9C=20SSE=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.module.ts | 3 +- src/modules/project/project.service.ts | 49 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts index d1a8ae9..6af336b 100644 --- a/src/modules/project/project.module.ts +++ b/src/modules/project/project.module.ts @@ -4,9 +4,10 @@ import { ProjectService } from './project.service'; import { AuthModule } from '@modules/auth/auth.module'; import { PrismaService } from '@prisma/prisma.service'; import { S3Module } from '@src/s3/s3.module'; +import { NotificationModule } from '../notification/notification.module'; @Module({ - imports: [AuthModule, S3Module], + imports: [AuthModule, S3Module, NotificationModule], controllers: [ProjectController], providers: [ProjectService, PrismaService], exports: [ProjectService], diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 51961db..b180283 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -12,12 +12,14 @@ import { CreateProjectDto } from './dto/CreateProject.dto'; import { startOfWeek, endOfWeek } from 'date-fns'; import { S3Service } from '@src/s3/s3.service'; import * as cheerio from 'cheerio'; +import { NotificationsService } from '../notification/notification.service'; @Injectable() export class ProjectService { constructor( private readonly prisma: PrismaService, - private readonly s3: S3Service + private readonly s3: S3Service, + private readonly notificationsService: NotificationsService ) {} async getProjects(params: { @@ -396,6 +398,7 @@ export class ProjectService { // 프로젝트 존재 여부 확인 const project = await this.prisma.projectPost.findUnique({ where: { id: projectId }, + select: { user_id: true }, // 프로젝트 작성자 ID 가져오기 }); if (!project) { @@ -421,6 +424,27 @@ export class ProjectService { }, }); + // 지원 알림 전송 + const sender = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { nickname: true, profile_url: true }, + }); + + const message = `${sender.nickname}님이 회원님의 프로젝트에 지원했습니다.`; + await this.notificationsService.createNotification( + project.user_id, // 프로젝트 작성자 ID + userId, // 지원자 ID + 'application', + message + ); + + this.notificationsService.sendRealTimeNotification(project.user_id, { + type: 'application', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profile_url, + }); + return { message: { text: '프로젝트에 지원되었습니다.', @@ -664,7 +688,7 @@ export class ProjectService { async updateApplicationStatus( userId: number, projectId: number, - targetUserId: number, // 지원자의 userId를 기반으로 업데이트 + targetUserId: number, // 지원자의 userId status: 'Accepted' | 'Rejected' | 'Pending' ) { // 프로젝트 작성자인지 확인 @@ -700,6 +724,27 @@ export class ProjectService { data: { status }, }); + // 지원 상태 변경 알림 전송 + const sender = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { nickname: true, profile_url: true }, + }); + + const message = `${sender.nickname}님이 회원님의 프로젝트 지원 상태를 '${status}'로 변경했습니다.`; + await this.notificationsService.createNotification( + targetUserId, // 지원자 ID + userId, // 프로젝트 작성자 ID + 'applicationStatus', + message + ); + + this.notificationsService.sendRealTimeNotification(targetUserId, { + type: 'applicationStatus', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profile_url, + }); + return { message: '지원 상태가 변경되었습니다.', application: { From 490df179e3e84deb7c14c4a2994d98f6d0fc07a0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:03:42 +0900 Subject: [PATCH 343/414] =?UTF-8?q?[Feat]=20SSE=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.module.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/modules/notification/notification.module.ts b/src/modules/notification/notification.module.ts index c386b0d..79ae0f2 100644 --- a/src/modules/notification/notification.module.ts +++ b/src/modules/notification/notification.module.ts @@ -1,11 +1,12 @@ -// import { Module } from '@nestjs/common'; -// import { NotificationsController } from './notification.controller'; -// import { NotificationsService } from '@modules/notification/notification.service'; -// import { AuthModule } from '@modules/auth/auth.module'; -// import { PrismaModule } from '@src/prisma/prisma.module'; -// @Module({ -// imports: [AuthModule, PrismaModule], -// controllers: [NotificationsController], -// providers: [NotificationsService], -// }) -// export class NotificationModule {} +import { Module } from '@nestjs/common'; +import { NotificationsController } from './notification.controller'; +import { NotificationsService } from '@modules/notification/notification.service'; +import { AuthModule } from '@modules/auth/auth.module'; +import { PrismaModule } from '@src/prisma/prisma.module'; +@Module({ + imports: [AuthModule, PrismaModule], + controllers: [NotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], +}) +export class NotificationModule {} From 0c762c78a311b3be9a485bd932e08ecbe072d713 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:03:50 +0900 Subject: [PATCH 344/414] =?UTF-8?q?[Feat]=20SSE=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.service.ts | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index f2f16fd..d949652 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -1,48 +1,44 @@ -// import { Injectable } from '@nestjs/common'; -// import { PrismaService } from '@src/prisma/prisma.service'; -// import { Subject } from 'rxjs'; +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { Subject } from 'rxjs'; -// @Injectable() -// export class NotificationsService { -// constructor(private readonly prisma: PrismaService) {} +@Injectable() +export class NotificationsService { + public readonly notifications$ = new Subject(); -// // 특정 사용자 알림 조회 -// async getNotificationsForUser(userId: number, isRead?: boolean) { -// return this.prisma.notification.findMany({ -// where: { -// userId, -// ...(isRead !== undefined && { isRead }), -// }, -// orderBy: { createdAt: 'desc' }, -// }); -// } + constructor(private readonly prisma: PrismaService) {} -// // 알림 생성 -// async createNotification(userId: number, type: string, message: string) { -// return this.prisma.notification.create({ -// data: { userId, type, message }, -// }); -// } + // 알림 생성 + async createNotification( + userId: number, + senderId: number, + type: string, + message: string + ) { + try { + return await this.prisma.notification.create({ + data: { + userId, + senderId, + type, + message, + }, + }); + } catch (error) { + console.error('알림 생성 중 오류:', error.message); + throw new Error('알림 생성에 실패했습니다.'); + } + } -// // 알림 읽음 처리 -// async markAsRead(notificationId: number) { -// return this.prisma.notification.update({ -// where: { id: notificationId }, -// data: { isRead: true }, -// }); -// } - -// // SSE 알림 전송 -// async sendRealTimeNotification( -// userId: number, -// data: any, -// notifications$: Subject -// ) { -// const notification = await this.createNotification( -// userId, -// data.type, -// data.message -// ); -// notifications$.next({ data: notification, type: 'notification' }); -// } -// } + // 실시간 알림 전송 + sendRealTimeNotification(userId: number, data: any) { + this.notifications$.next({ + userId, + type: data.type || 'notification', // 이벤트 유형 + message: data.message, // 알림 메시지 + senderNickname: data.senderNickname, // 보낸 사람 닉네임 + senderProfileUrl: data.senderProfileUrl, // 보낸 사람 프로필 URL + timestamp: new Date().toISOString(), // 알림 전송 시간 + }); + } +} From 57625fd7bb1bdbc0e30016c79b725e999bdbb4d5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:03:59 +0900 Subject: [PATCH 345/414] =?UTF-8?q?[Feat]=20SSE=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 00d12c9..0d5c8e4 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -1,47 +1,29 @@ -// import { Controller, Get, Sse, Param, Patch, Body, Req } from '@nestjs/common'; -// import { Observable, Subject } from 'rxjs'; -// import { map } from 'rxjs/operators'; -// import { NotificationsService } from './notification.service'; +import { Controller, Sse, Req } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { NotificationsService } from './notification.service'; -// @Controller('notifications') -// export class NotificationsController { -// private notifications$ = new Subject(); +@Controller('notifications') +export class NotificationsController { + constructor(private readonly notificationsService: NotificationsService) {} -// constructor(private readonly notificationService: NotificationsService) {} + @Sse('stream') + streamNotifications(@Req() req): Observable { + const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 -// // SSE 스트림 -// @Sse('stream') -// streamNotifications(@Req() req: any): Observable { -// req.on('close', () => { -// console.log('클라이언트 연결 종료'); -// }); + if (!userId) { + throw new Error('사용자 인증 정보가 필요합니다.'); + } -// return this.notifications$.asObservable().pipe( -// map(data => ({ -// data, -// type: 'notification', -// })) -// ); -// } - -// // 알림 전송 메서드 -// sendNotification(data: any) { -// this.notifications$.next(data); -// } -// // 특정 사용자의 알림 조회 -// @Get() -// async getUserNotifications(@Req() req: any) { -// const userId = req.user.user_id; // 인증된 사용자 정보에서 ID 가져오기 -// if (!userId) { -// throw new Error('사용자 ID를 찾을 수 없습니다.'); -// } - -// return this.notificationService.getNotificationsForUser(userId); -// } - -// // 알림 읽음 처리 -// @Patch(':id/read') -// async markNotificationAsRead(@Param('id') notificationId: number) { -// return await this.notificationService.markAsRead(notificationId); -// } -// } + return this.notificationsService.notifications$.asObservable().pipe( + filter(notification => notification.userId === userId), + map(notification => ({ + type: notification.type, + message: notification.message, + senderNickname: notification.senderNickname, + senderProfileUrl: notification.senderProfileUrl, + timestamp: new Date().toISOString(), // 알림 전송 시간 추가 + })) + ); + } +} From 5ff448332fc9e98e2860bda31aee569e2e34559a Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:04:12 +0900 Subject: [PATCH 346/414] =?UTF-8?q?[Feat]=20SSE=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 8c95788..7d97953 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { FeedModule } from './feed/feed.module'; import { ProjectModule } from '@modules/project/project.module'; import { SearchModule } from './search/search.module'; import { FollowModule } from '@modules/follow/follow.module'; +import { NotificationModule } from '@modules/notification/notification.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -20,6 +21,7 @@ import { FollowModule } from '@modules/follow/follow.module'; ProjectModule, SearchModule, FollowModule, + NotificationModule, ], providers: [ChatGateway], }) From 5ad66e2b57231328d811ce4b4919ed8eef6109f5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:04:20 +0900 Subject: [PATCH 347/414] =?UTF-8?q?[Feat]=20SSE=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/feed/feed.module.ts b/src/feed/feed.module.ts index 0b3be44..3654747 100644 --- a/src/feed/feed.module.ts +++ b/src/feed/feed.module.ts @@ -3,9 +3,10 @@ import { FeedService } from './feed.service'; import { FeedController } from './feed.controller'; import { PrismaModule } from '@src/prisma/prisma.module'; import { S3Module } from '@src/s3/s3.module'; +import { NotificationModule } from '@src/modules/notification/notification.module'; @Module({ - imports: [PrismaModule, S3Module], + imports: [PrismaModule, S3Module, NotificationModule], controllers: [FeedController], providers: [FeedService], }) From 2511bce4cba1f9da5e3e8f63921c2944192336d9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:04:28 +0900 Subject: [PATCH 348/414] =?UTF-8?q?[Feat]=20SSE=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 79 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 3b7ea84..f09803a 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -6,12 +6,14 @@ import { CommentDto } from './dto/comment.dto'; import { GetFeedsQueryDto } from './dto/getFeedsQuery.dto'; import { S3Service } from '@src/s3/s3.service'; import * as dayjs from 'dayjs'; +import { NotificationsService } from '@src/modules/notification/notification.service'; @Injectable() export class FeedService { constructor( private readonly prisma: PrismaService, - private readonly s3: S3Service + private readonly s3: S3Service, + private readonly notificationsService: NotificationsService ) {} // 피드 전체 조회 @@ -333,6 +335,7 @@ export class FeedService { }); if (exist.length) { + // 좋아요 취소 await this.prisma.feedLike.deleteMany({ where: { post_id: feedId, @@ -347,6 +350,7 @@ export class FeedService { return { message: { code: 200, text: '좋아요가 취소되었습니다.' } }; } else { + // 좋아요 추가 await this.prisma.feedLike.create({ data: { post_id: feedId, @@ -359,10 +363,45 @@ export class FeedService { data: { like_count: { increment: 1 } }, }); + // 피드 작성자 정보 가져오기 + const feed = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + include: { user: true }, // 작성자 정보 포함 + }); + + if (!feed) { + throw new HttpException( + '피드를 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + + if (feed.user_id !== userId) { + const sender = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { nickname: true, profile_url: true }, + }); + + const message = `${sender.nickname}님이 회원님의 피드를 좋아합니다.`; + await this.notificationsService.createNotification( + feed.user_id, + userId, + 'like', + message + ); + + this.notificationsService.sendRealTimeNotification(feed.user_id, { + type: 'like', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profile_url, + }); + } + return { message: { code: 200, text: '좋아요가 추가되었습니다.' } }; } } catch (err) { - console.log(err); + console.error(err); throw new HttpException( '서버에서 오류가 발생했습니다.', HttpStatus.INTERNAL_SERVER_ERROR @@ -538,6 +577,42 @@ export class FeedService { data: { comment_count: { increment: 1 } }, }); + // 피드 작성자 정보 가져오기 + const feed = await this.prisma.feedPost.findUnique({ + where: { id: feedId }, + include: { user: true }, // 작성자 정보 포함 + }); + + if (!feed) { + throw new HttpException( + '피드를 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + + // 알림 전송 + if (feed.user_id !== userId) { + const sender = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { nickname: true, profile_url: true }, + }); + + const message = `${sender.nickname}님이 회원님의 피드에 댓글을 남겼습니다.`; + await this.notificationsService.createNotification( + feed.user_id, + userId, + 'comment', + message + ); + + this.notificationsService.sendRealTimeNotification(feed.user_id, { + type: 'comment', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profile_url, + }); + } + return { message: { code: 201, text: '댓글 등록이 완료되었습니다.' } }; } catch (err) { console.log(err); From 2e5c7796eb57c63aa9b6efe9b6c45d77799aafb9 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 13:04:44 +0900 Subject: [PATCH 349/414] =?UTF-8?q?[Feat]=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=EC=8B=9C=20SSE=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.module.ts | 8 ++-- src/modules/follow/follow.service.ts | 56 ++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/modules/follow/follow.module.ts b/src/modules/follow/follow.module.ts index 1e8fe28..9fa3c00 100644 --- a/src/modules/follow/follow.module.ts +++ b/src/modules/follow/follow.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { FollowController } from './follow.controller'; -import { PrismaModule } from '@prisma/prisma.module'; import { FollowService } from '@modules/follow/follow.service'; -//import { NotificationModule } from '../notification/notification.module'; +import { NotificationModule } from '../notification/notification.module'; +import { PrismaService } from '@src/prisma/prisma.service'; @Module({ - imports: [PrismaModule], + imports: [NotificationModule], controllers: [FollowController], - providers: [FollowService], + providers: [FollowService, PrismaService], }) export class FollowModule {} diff --git a/src/modules/follow/follow.service.ts b/src/modules/follow/follow.service.ts index c9640c9..1663f88 100644 --- a/src/modules/follow/follow.service.ts +++ b/src/modules/follow/follow.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; -//import { NotificationsService } from '@modules/notification/notification.service'; +import { NotificationsService } from '@modules/notification/notification.service'; @Injectable() export class FollowService { constructor( private readonly prisma: PrismaService, - //private readonly notificationsService: NotificationsService + private readonly notificationsService: NotificationsService ) {} async toggleFollow(userId: number, targetUserId: number) { @@ -37,19 +37,45 @@ export class FollowService { }, }); - // 알림 생성 및 SSE 전송 - // const message = `사용자 ${userId}님이 당신을 팔로우하기 시작했습니다.`; - // await this.notificationsService.sendRealTimeNotification( - // targetUserId, - // { type: 'follow', message }, - // this.notificationsService.notifications$ // SSE 스트림 - // ); - // return { - // message: { code: 200, text: '팔로우 성공' }, - // isFollowing: true, - // }; - // } - // } + // 상대방 정보 가져오기 + const sender = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + const targetUser = await this.prisma.user.findUnique({ + where: { id: targetUserId }, + }); + + if (!sender || !targetUser) { + throw new Error('사용자 정보를 찾을 수 없습니다.'); + } + + // 알림 메시지 생성 및 전송 + const message = `${sender.nickname}님이 회원님을 팔로우하기 시작했습니다.`; + const notificationData = { + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profile_url, + type: 'follow', + }; + + // 알림 생성 + await this.notificationsService.createNotification( + targetUserId, + userId, + notificationData.type, + notificationData.message + ); + + // SSE를 통해 실시간 알림 전송 + this.notificationsService.sendRealTimeNotification( + targetUserId, + notificationData + ); + + return { + message: { code: 200, text: '팔로우 성공' }, + isFollowing: true, + }; } } } From bfdf93d179b606363084317a1d6bd474894b32b4 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 14:08:43 +0900 Subject: [PATCH 350/414] [Fix] try catch --- .../notification/notification.controller.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 0d5c8e4..f16b27c 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -9,21 +9,25 @@ export class NotificationsController { @Sse('stream') streamNotifications(@Req() req): Observable { - const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 + try { + const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 - if (!userId) { - throw new Error('사용자 인증 정보가 필요합니다.'); - } + if (!userId) { + throw new Error('사용자 인증 정보가 필요합니다.'); + } - return this.notificationsService.notifications$.asObservable().pipe( - filter(notification => notification.userId === userId), - map(notification => ({ - type: notification.type, - message: notification.message, - senderNickname: notification.senderNickname, - senderProfileUrl: notification.senderProfileUrl, - timestamp: new Date().toISOString(), // 알림 전송 시간 추가 - })) - ); + return this.notificationsService.notifications$.asObservable().pipe( + filter(notification => notification.userId === userId), + map(notification => ({ + type: notification.type, + message: notification.message, + senderNickname: notification.senderNickname, + senderProfileUrl: notification.senderProfileUrl, + timestamp: new Date().toISOString(), // 알림 전송 시간 추가 + })) + ); + } catch (err) { + console.log(err.message); + } } } From 63c09e9e9a29f96c637872c127987eac63a04f05 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 14:22:56 +0900 Subject: [PATCH 351/414] =?UTF-8?q?[Fix]=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index e4aabbf..5625fa3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -43,7 +43,7 @@ async function bootstrap() { customSiteTitle: 'API 문서', }); - app.useGlobalFilters(new HttpExceptionFilter()); + //app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); } bootstrap(); From d3b7eed7bcc64349edcb48c569980327390d26d0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 14:23:08 +0900 Subject: [PATCH 352/414] =?UTF-8?q?[Feat]=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index f16b27c..6e86a9d 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -1,6 +1,14 @@ -import { Controller, Sse, Req } from '@nestjs/common'; -import { Observable } from 'rxjs'; +import { + Controller, + Sse, + Req, + HttpException, + HttpStatus, + UseGuards, +} from '@nestjs/common'; +import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { NotificationsService } from './notification.service'; @Controller('notifications') @@ -8,26 +16,31 @@ export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @Sse('stream') + @UseGuards(JwtAuthGuard) streamNotifications(@Req() req): Observable { - try { - const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 + const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 - if (!userId) { - throw new Error('사용자 인증 정보가 필요합니다.'); - } - - return this.notificationsService.notifications$.asObservable().pipe( - filter(notification => notification.userId === userId), - map(notification => ({ - type: notification.type, - message: notification.message, - senderNickname: notification.senderNickname, - senderProfileUrl: notification.senderProfileUrl, - timestamp: new Date().toISOString(), // 알림 전송 시간 추가 - })) - ); - } catch (err) { - console.log(err.message); + // 인증된 사용자 확인 + if (!userId) { + // 에러를 발생시키되 기본 Observable을 반환 + console.error('사용자 인증 정보가 필요합니다.'); + return of({ + type: 'error', + message: '사용자 인증 정보가 필요합니다.', + timestamp: new Date().toISOString(), + }); } + + // 사용자별 필터링된 알림 스트림 반환 + return this.notificationsService.notifications$.asObservable().pipe( + filter(notification => notification.userId === userId), + map(notification => ({ + type: notification.type, + message: notification.message, + senderNickname: notification.senderNickname, + senderProfileUrl: notification.senderProfileUrl, + timestamp: new Date().toISOString(), + })) + ); } } From 03f060523bf583f2d5ca07f58e9496580035f896 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 14:48:27 +0900 Subject: [PATCH 353/414] =?UTF-8?q?[Fix]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/notification/notification.controller.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 6e86a9d..2587237 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -18,11 +18,9 @@ export class NotificationsController { @Sse('stream') @UseGuards(JwtAuthGuard) streamNotifications(@Req() req): Observable { - const userId = req.user?.user_id; // 인증된 사용자 ID 가져오기 + const userId = req.user?.user_id; - // 인증된 사용자 확인 if (!userId) { - // 에러를 발생시키되 기본 Observable을 반환 console.error('사용자 인증 정보가 필요합니다.'); return of({ type: 'error', @@ -31,7 +29,11 @@ export class NotificationsController { }); } - // 사용자별 필터링된 알림 스트림 반환 + req.on('close', () => { + console.log(`사용자 ${userId}와의 SSE 연결이 종료되었습니다.`); + // 필요시 리소스 정리 로직 추가 + }); + return this.notificationsService.notifications$.asObservable().pipe( filter(notification => notification.userId === userId), map(notification => ({ From b5a2f99d71bc5da33eef1aef53a7d18f0aba990f Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 14:48:42 +0900 Subject: [PATCH 354/414] =?UTF-8?q?[Fix]=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index f09803a..7fbf8d0 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -561,7 +561,6 @@ export class FeedService { // 댓글 등록 async createComment(userId: number, feedId: number, commentDto: CommentDto) { try { - // 댓글 데이터 저장 const content = commentDto.content; await this.prisma.feedComment.create({ data: { @@ -571,16 +570,14 @@ export class FeedService { }, }); - // 피드 댓글 카운트 증가 await this.prisma.feedPost.update({ where: { id: feedId }, data: { comment_count: { increment: 1 } }, }); - // 피드 작성자 정보 가져오기 const feed = await this.prisma.feedPost.findUnique({ where: { id: feedId }, - include: { user: true }, // 작성자 정보 포함 + include: { user: true }, }); if (!feed) { @@ -590,13 +587,19 @@ export class FeedService { ); } - // 알림 전송 if (feed.user_id !== userId) { const sender = await this.prisma.user.findUnique({ where: { id: userId }, select: { nickname: true, profile_url: true }, }); + if (!sender) { + throw new HttpException( + '보낸 사람 정보를 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + const message = `${sender.nickname}님이 회원님의 피드에 댓글을 남겼습니다.`; await this.notificationsService.createNotification( feed.user_id, @@ -615,7 +618,7 @@ export class FeedService { return { message: { code: 201, text: '댓글 등록이 완료되었습니다.' } }; } catch (err) { - console.log(err); + console.error('댓글 등록 중 오류:', err.message); throw new HttpException( '서버에서 오류가 발생했습니다.', HttpStatus.INTERNAL_SERVER_ERROR From c694109054d48794bb69c7ee45ae750d819540ad Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:07:21 +0900 Subject: [PATCH 355/414] =?UTF-8?q?[Feat]=20SSE=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interceptors/notification.interceptor.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/modules/notification/Interceptors/notification.interceptor.ts diff --git a/src/modules/notification/Interceptors/notification.interceptor.ts b/src/modules/notification/Interceptors/notification.interceptor.ts new file mode 100644 index 0000000..ed51c09 --- /dev/null +++ b/src/modules/notification/Interceptors/notification.interceptor.ts @@ -0,0 +1,23 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Response } from 'express'; +import { Observable } from 'rxjs'; + +@Injectable() +export class SseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const res: Response = context.switchToHttp().getResponse(); + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache, no-transform'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', '*'); // CORS 설정 필요시 추가 + res.flushHeaders(); // 즉시 헤더를 전송하여 연결이 끊기지 않도록 함 + + return next.handle(); + } +} From 05291d13fe020aa9921a9fd60a2cea6676425745 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:07:35 +0900 Subject: [PATCH 356/414] =?UTF-8?q?[Fix]=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/notification/notification.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 2587237..0bd14eb 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -2,16 +2,17 @@ import { Controller, Sse, Req, - HttpException, - HttpStatus, UseGuards, + UseInterceptors, } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { NotificationsService } from './notification.service'; +import { SseInterceptor } from './Interceptors/notification.interceptor'; // 🔹 추가한 Interceptor import @Controller('notifications') +@UseInterceptors(SseInterceptor) // 🔹 Interceptor 적용 export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @@ -31,7 +32,6 @@ export class NotificationsController { req.on('close', () => { console.log(`사용자 ${userId}와의 SSE 연결이 종료되었습니다.`); - // 필요시 리소스 정리 로직 추가 }); return this.notificationsService.notifications$.asObservable().pipe( From da2d51936eb3290bb098deef8d7b248c4a51f6a2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:15:23 +0900 Subject: [PATCH 357/414] =?UTF-8?q?[Fix]=20CORS=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/Interceptors/notification.interceptor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/notification/Interceptors/notification.interceptor.ts b/src/modules/notification/Interceptors/notification.interceptor.ts index ed51c09..f9c4dbc 100644 --- a/src/modules/notification/Interceptors/notification.interceptor.ts +++ b/src/modules/notification/Interceptors/notification.interceptor.ts @@ -16,6 +16,7 @@ export class SseInterceptor implements NestInterceptor { res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); res.setHeader('Access-Control-Allow-Origin', '*'); // CORS 설정 필요시 추가 + res.setHeader('Access-Control-Allow-Credentials', 'true'); res.flushHeaders(); // 즉시 헤더를 전송하여 연결이 끊기지 않도록 함 return next.handle(); From c54c81c8f6735ec547ef28a0598f050fa513cfc8 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:15:30 +0900 Subject: [PATCH 358/414] =?UTF-8?q?[Fix]=20CORS=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 5625fa3..65eadb9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,8 +16,10 @@ async function bootstrap() { 'http://localhost:8080', 'https://p-a-d.store', ], + methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'], // 💡 GET 포함 credentials: true, - exposedHeaders: ['Authorization'], + allowedHeaders: ['Authorization', 'Content-Type'], // 💡 CORS 요청 헤더 허용 + exposedHeaders: ['Authorization'], // 💡 클라이언트에서 응답 헤더 사용 가능 }); app.useGlobalPipes( From 6b6217e0197cac563c25fee701b2c29a2470ae39 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:28:58 +0900 Subject: [PATCH 359/414] =?UTF-8?q?[Fix]=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interceptors/notification.interceptor.ts | 15 ++++++----- .../notification/notification.controller.ts | 27 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/modules/notification/Interceptors/notification.interceptor.ts b/src/modules/notification/Interceptors/notification.interceptor.ts index f9c4dbc..8bae288 100644 --- a/src/modules/notification/Interceptors/notification.interceptor.ts +++ b/src/modules/notification/Interceptors/notification.interceptor.ts @@ -12,13 +12,14 @@ export class SseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const res: Response = context.switchToHttp().getResponse(); - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache, no-transform'); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('Access-Control-Allow-Origin', '*'); // CORS 설정 필요시 추가 - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.flushHeaders(); // 즉시 헤더를 전송하여 연결이 끊기지 않도록 함 - + if (!res.headersSent) { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache, no-transform'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173'); // CORS 설정 필요시 추가 + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.flushHeaders(); // 즉시 헤더를 전송하여 연결이 끊기지 않도록 함 + } return next.handle(); } } diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 0bd14eb..598f1e0 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -9,10 +9,10 @@ import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { NotificationsService } from './notification.service'; -import { SseInterceptor } from './Interceptors/notification.interceptor'; // 🔹 추가한 Interceptor import +import { SseInterceptor } from './Interceptors/notification.interceptor'; // ✅ 추가 @Controller('notifications') -@UseInterceptors(SseInterceptor) // 🔹 Interceptor 적용 +@UseInterceptors(SseInterceptor) // ✅ Interceptor 사용 export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @@ -22,7 +22,7 @@ export class NotificationsController { const userId = req.user?.user_id; if (!userId) { - console.error('사용자 인증 정보가 필요합니다.'); + console.error('🚨 사용자 인증 정보가 필요합니다.'); return of({ type: 'error', message: '사용자 인증 정보가 필요합니다.', @@ -30,19 +30,24 @@ export class NotificationsController { }); } + console.log(`✅ SSE 연결 성공 - 사용자 ${userId}`); + req.on('close', () => { - console.log(`사용자 ${userId}와의 SSE 연결이 종료되었습니다.`); + console.log(`❌ 사용자 ${userId}와의 SSE 연결 종료`); }); return this.notificationsService.notifications$.asObservable().pipe( filter(notification => notification.userId === userId), - map(notification => ({ - type: notification.type, - message: notification.message, - senderNickname: notification.senderNickname, - senderProfileUrl: notification.senderProfileUrl, - timestamp: new Date().toISOString(), - })) + map(notification => { + console.log('📡 SSE 이벤트 전송:', notification); + return { + type: notification.type, + message: notification.message, + senderNickname: notification.senderNickname, + senderProfileUrl: notification.senderProfileUrl, + timestamp: new Date().toISOString(), + }; + }) ); } } From 7c4bb95fa21ec3661d98a942aef57d7d89f5709d Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 15:57:24 +0900 Subject: [PATCH 360/414] =?UTF-8?q?[Fix]=20=EC=9D=91=EB=8B=B5=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 598f1e0..b5348ed 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -9,10 +9,10 @@ import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { NotificationsService } from './notification.service'; -import { SseInterceptor } from './Interceptors/notification.interceptor'; // ✅ 추가 +import { SseInterceptor } from './Interceptors/notification.interceptor'; @Controller('notifications') -@UseInterceptors(SseInterceptor) // ✅ Interceptor 사용 +@UseInterceptors(SseInterceptor) export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @@ -24,9 +24,12 @@ export class NotificationsController { if (!userId) { console.error('🚨 사용자 인증 정보가 필요합니다.'); return of({ - type: 'error', - message: '사용자 인증 정보가 필요합니다.', - timestamp: new Date().toISOString(), + event: 'error', // ✅ 이벤트 이름 추가 + data: { + type: 'error', + message: '사용자 인증 정보가 필요합니다.', + timestamp: new Date().toISOString(), + }, }); } @@ -38,16 +41,16 @@ export class NotificationsController { return this.notificationsService.notifications$.asObservable().pipe( filter(notification => notification.userId === userId), - map(notification => { - console.log('📡 SSE 이벤트 전송:', notification); - return { + map(notification => ({ + event: 'message', // ✅ 'message' 이벤트 이름 추가 + data: { type: notification.type, message: notification.message, senderNickname: notification.senderNickname, senderProfileUrl: notification.senderProfileUrl, timestamp: new Date().toISOString(), - }; - }) + }, + })) ); } } From a1c85f9ee2af5f6cf9e6cb8526e2ac0fc2a126d0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 31 Jan 2025 23:36:46 +0900 Subject: [PATCH 361/414] =?UTF-8?q?[Fix]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/user/docs/user.docs.ts | 4 ++-- src/modules/user/user.service.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/modules/user/docs/user.docs.ts b/src/modules/user/docs/user.docs.ts index ce27c63..abe6b27 100644 --- a/src/modules/user/docs/user.docs.ts +++ b/src/modules/user/docs/user.docs.ts @@ -109,8 +109,8 @@ export const GetUserProfileDocs = { ], followerCount: 12, followingCount: 34, - applyCount: 12, - postCount: 17, + feedCount: 12, + applyCount: 17, isOwnProfile: true, }, }, diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 2bb7ffe..3a2b1be 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -49,6 +49,15 @@ export class UserService { where: { following_user_id: targetUserId }, }); + // 유저가 작성한 피드 개수 + const feedCount = await this.prisma.feedPost.count({ + where: { user_id: targetUserId }, + }); + + // 유저가 지원한 프로젝트의 개수 + const applyCount = await this.prisma.userApplyProject.count({ + where: { user_id: targetUserId }, + }); // 반환 데이터 구성 const response = { message: { @@ -56,10 +65,10 @@ export class UserService { text: '유저 프로필 조회에 성공했습니다', }, status: user.status.name, - applyCount: user.apply_count, - postCount: user.post_count, followerCount, // 팔로워 수 followingCount, // 팔로잉 수 + feedCount, // 유저가 작성한 피드 개수 + applyCount, // 유저가 지원한 프로젝트 개수 isOwnProfile: loggedInUserId === targetUserId, // 자신의 프로필인지 확인 }; From 9498303144cc7be85578e568f43e6b7651e31b8c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 14:29:09 +0900 Subject: [PATCH 362/414] =?UTF-8?q?[Feat]=20DB=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 93 ++++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e109ea..c056e1e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,44 +8,46 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique - name String - nickname String @unique - auth_provider String - profile_url String? - role_id Int - introduce String? - status_id Int - apply_count Int? @default(0) - post_count Int? @default(0) - push_alert Boolean @default(false) - following_alert Boolean @default(false) - project_alert Boolean @default(false) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - password String? - job_detail String? - ArtistData ArtistData[] - Channel_users Channel_users[] - FeedComments FeedComment[] - FeedCommentLikes FeedCommentLikes[] - FeedLikes FeedLike[] - FeedPosts FeedPost[] - Followed Follows[] @relation("FollowedUsers") - Follows Follows[] @relation("UserFollows") - Message Message[] - Message_status Message_status[] - MyPageProject MyPageProject[] - UserLinks MyPageUserLink[] - ProgrammerData ProgrammerData? - ProjectPost ProjectPost[] - ProjectSaves ProjectSave[] - Resume Resume[] - role Role @relation(fields: [role_id], references: [id]) - status Status @relation(fields: [status_id], references: [id]) - UserApplyProject UserApplyProject[] - UserSkills UserSkill[] + id Int @id @default(autoincrement()) + email String @unique + name String + nickname String @unique + auth_provider String + profile_url String? + role_id Int + introduce String? + status_id Int + apply_count Int? @default(0) + post_count Int? @default(0) + push_alert Boolean @default(false) + following_alert Boolean @default(false) + project_alert Boolean @default(false) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + password String? + job_detail String? + ArtistData ArtistData[] + Channel_users Channel_users[] + FeedComments FeedComment[] + FeedCommentLikes FeedCommentLikes[] + FeedLikes FeedLike[] + FeedPosts FeedPost[] + Followed Follows[] @relation("FollowedUsers") + Follows Follows[] @relation("UserFollows") + Message Message[] + Message_status Message_status[] + MyPageProject MyPageProject[] + UserLinks MyPageUserLink[] + Notification_Notification_senderIdToUser Notification[] @relation("Notification_senderIdToUser") + Notification_Notification_userIdToUser Notification[] @relation("Notification_userIdToUser") + ProgrammerData ProgrammerData? + ProjectPost ProjectPost[] + ProjectSaves ProjectSave[] + Resume Resume[] + role Role @relation(fields: [role_id], references: [id]) + status Status @relation(fields: [status_id], references: [id]) + UserApplyProject UserApplyProject[] + UserSkills UserSkill[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -376,3 +378,18 @@ model online_users { client_id String user_id Int } + +model Notification { + id Int @id @default(autoincrement()) + userId Int + type String + message String + isRead Boolean @default(false) + createdAt DateTime @default(now()) + senderId Int + User_Notification_senderIdToUser User @relation("Notification_senderIdToUser", fields: [senderId], references: [id]) + User_Notification_userIdToUser User @relation("Notification_userIdToUser", fields: [userId], references: [id]) + + @@index([senderId], map: "Notification_senderId_fkey") + @@index([userId], map: "Notification_userId_fkey") +} From f7a7a7d797a103b394abc68b8eab6c8239eb4e7a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 14:45:23 +0900 Subject: [PATCH 363/414] =?UTF-8?q?[Feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=EB=A5=BC=20=EC=9C=84=ED=95=B4=20S3?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=9E=84=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 6eed9ed..1c0595c 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -3,9 +3,10 @@ import { ChatService } from './chat.service'; import { PrismaModule } from '@src/prisma/prisma.module'; import { JwtModule } from '@nestjs/jwt'; import { ChatController } from './chat.controller'; +import { S3Module } from '@src/s3/s3.module'; @Module({ - imports: [PrismaModule, JwtModule], + imports: [PrismaModule, JwtModule, S3Module], providers: [ChatService], exports: [ChatService], controllers: [ChatController], From 092125bb4b9013cfb1048235c893764d48f6fb0a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 14:53:36 +0900 Subject: [PATCH 364/414] =?UTF-8?q?[Feat]=20=EB=B2=84=ED=8D=BC=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=82=AC=EC=9A=A9=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20file-type=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 109 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d6255a6..c94ab92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "dotenv": "^16.4.7", + "file-type": "^20.0.1", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", @@ -3849,6 +3850,30 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", + "integrity": "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -7222,6 +7247,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7261,6 +7292,24 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.0.1.tgz", + "integrity": "sha512-R80W+NZ+s1M8PsC8L7xn+J0N72vEyxstMH+4vwIyt5B7ojfG80h/nKu2Qw/NYJTIYyGUeaPNLnzfJd78SWnG3A==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -7996,7 +8045,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -10435,6 +10483,19 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/peek-readable": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-6.1.0.tgz", + "integrity": "sha512-1H5ECS+rPH35Foh4JD/XohQKWsx6Jzn37ESOVTFuCSoI8wMB9r2e2aDSLgHSiyucVrPfoc0DRiipBEP1gr9wLw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11774,6 +11835,23 @@ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "license": "MIT" }, + "node_modules/strtok3": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.0.tgz", + "integrity": "sha512-S884oIGzokq3LkL/6jXw5c5oJXRiGt4jB42cuWdaooJdxFMSn99snaShh3cQmJx3jALV2eoyW3rV/TJxwOaBPA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -12181,6 +12259,23 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -12458,6 +12553,18 @@ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici": { "version": "6.21.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", diff --git a/package.json b/package.json index 373435c..89ef8de 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "dotenv": "^16.4.7", + "file-type": "^20.0.1", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", From 7a09de9d4959b32835af4ea18c46dfa4ac2d51d8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 15:27:07 +0900 Subject: [PATCH 365/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EC=B0=BD=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 31 +++++++++++++++++++++++-------- src/chat/chat.service.ts | 24 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index c91c300..59d96b3 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -175,14 +175,29 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { } ) { const { userId } = data; + let messageData; - // 메세지 데이터 저장 - const messageData = await this.chatService.createMessage( - data.type, - data.channelId, - userId, - data.content - ); + // 전달 타입에 따라 메세지 데이터 저장 + if (data.type == 'image') { + const result = await this.chatService.handleChatFiles( + userId, + data.content + ); + + messageData = await this.chatService.createMessage( + data.type, + data.channelId, + userId, + result.imageUrl + ); + } else { + messageData = await this.chatService.createMessage( + data.type, + data.channelId, + userId, + data.content + ); + } const messageId = messageData.id; @@ -194,7 +209,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 전달 데이터 양식 const sendData = { type: data.type, - content: data.content, + content: messageData.content, channelId: data.channelId, messageId, user, diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index ca6ad79..bfc17eb 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -2,10 +2,15 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; import { SearchMessageDto } from './dto/serchMessage.dto'; +import { S3Service } from '@src/s3/s3.service'; +import { fileTypeFromBuffer } from 'file-type'; @Injectable() export class ChatService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly s3: S3Service + ) {} // 온라인 유저 DB에 저장 async addUserOnline(userId: number, clientId: string) { @@ -513,4 +518,21 @@ export class ChatService { throw new HttpException('권한이 없습니다.', HttpStatus.UNAUTHORIZED); } } + + // 이미지 업로드 + async handleChatFiles(userId: number, file) { + const data = await fileTypeFromBuffer(file); + const fileType = data.ext; + const imageUrl = await this.s3.uploadImage( + userId, + file, + fileType, + 'pad_chat/images' + ); + + return { + imageUrl, + message: { code: 200, message: '이미지 업로드가 완료되었습니다.' }, + }; + } } From ccd328d4e6fc2e8eeef2526b1227a154a5b16cf6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 17:08:33 +0900 Subject: [PATCH 366/414] =?UTF-8?q?[Feat]=20file-type=20import=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index bfc17eb..7ebe433 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -3,7 +3,6 @@ import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; import { SearchMessageDto } from './dto/serchMessage.dto'; import { S3Service } from '@src/s3/s3.service'; -import { fileTypeFromBuffer } from 'file-type'; @Injectable() export class ChatService { @@ -521,7 +520,9 @@ export class ChatService { // 이미지 업로드 async handleChatFiles(userId: number, file) { + const { fileTypeFromBuffer } = await import('file-type'); const data = await fileTypeFromBuffer(file); + const fileType = data.ext; const imageUrl = await this.s3.uploadImage( userId, From 7f35327b975f6e02226696bc26c760352c92b114 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 17:14:26 +0900 Subject: [PATCH 367/414] =?UTF-8?q?[Feat]=20file-type=20import=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 109 ++++++++++++++++++--------------------- package.json | 2 +- src/chat/chat.service.ts | 8 +-- 3 files changed, 56 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index c94ab92..15e12bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "dotenv": "^16.4.7", - "file-type": "^20.0.1", + "file-type": "^16.5.4", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", @@ -3850,24 +3850,6 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, - "node_modules/@tokenizer/inflate": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", - "integrity": "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.7", - "fflate": "^0.8.2", - "token-types": "^6.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -7247,12 +7229,6 @@ "bser": "2.1.1" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7293,18 +7269,17 @@ } }, "node_modules/file-type": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.0.1.tgz", - "integrity": "sha512-R80W+NZ+s1M8PsC8L7xn+J0N72vEyxstMH+4vwIyt5B7ojfG80h/nKu2Qw/NYJTIYyGUeaPNLnzfJd78SWnG3A==", + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -10484,12 +10459,12 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/peek-readable": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-6.1.0.tgz", - "integrity": "sha512-1H5ECS+rPH35Foh4JD/XohQKWsx6Jzn37ESOVTFuCSoI8wMB9r2e2aDSLgHSiyucVrPfoc0DRiipBEP1gr9wLw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "type": "github", @@ -10876,6 +10851,36 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11836,16 +11841,16 @@ "license": "MIT" }, "node_modules/strtok3": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.0.tgz", - "integrity": "sha512-S884oIGzokq3LkL/6jXw5c5oJXRiGt4jB42cuWdaooJdxFMSn99snaShh3cQmJx3jALV2eoyW3rV/TJxwOaBPA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", - "peek-readable": "^6.1.0" + "peek-readable": "^4.1.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "type": "github", @@ -12260,16 +12265,16 @@ } }, "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, "engines": { - "node": ">=14.16" + "node": ">=10" }, "funding": { "type": "github", @@ -12553,18 +12558,6 @@ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", "license": "MIT" }, - "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici": { "version": "6.21.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", diff --git a/package.json b/package.json index 89ef8de..6de338e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "dotenv": "^16.4.7", - "file-type": "^20.0.1", + "file-type": "^16.5.4", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7ebe433..e19836f 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; import { SearchMessageDto } from './dto/serchMessage.dto'; import { S3Service } from '@src/s3/s3.service'; +import * as fileType from 'file-type'; @Injectable() export class ChatService { @@ -520,14 +521,13 @@ export class ChatService { // 이미지 업로드 async handleChatFiles(userId: number, file) { - const { fileTypeFromBuffer } = await import('file-type'); - const data = await fileTypeFromBuffer(file); + const data = await fileType.fromBuffer(file); - const fileType = data.ext; + const type = data.ext; const imageUrl = await this.s3.uploadImage( userId, file, - fileType, + type, 'pad_chat/images' ); From 9924f940a878d6fb0f1c02cf403a5b87218b4fa5 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 17:30:36 +0900 Subject: [PATCH 368/414] =?UTF-8?q?[Fix]=20=ED=8C=8C=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD;=20mime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 26 +++++++++++++++++++++----- package.json | 1 + src/chat/chat.service.ts | 7 +++---- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15e12bf..c3025a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "file-type": "^16.5.4", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", + "mime": "^4.0.6", "passport": "^0.7.0", "passport-github": "^1.1.0", "passport-github2": "^0.1.12", @@ -9739,15 +9740,18 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.6.tgz", + "integrity": "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -11353,6 +11357,18 @@ "node": ">= 0.8" } }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", diff --git a/package.json b/package.json index 6de338e..96080f7 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "file-type": "^16.5.4", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", + "mime": "^4.0.6", "passport": "^0.7.0", "passport-github": "^1.1.0", "passport-github2": "^0.1.12", diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e19836f..17ad38a 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; import { SearchMessageDto } from './dto/serchMessage.dto'; import { S3Service } from '@src/s3/s3.service'; -import * as fileType from 'file-type'; +import mime from 'mime'; @Injectable() export class ChatService { @@ -521,13 +521,12 @@ export class ChatService { // 이미지 업로드 async handleChatFiles(userId: number, file) { - const data = await fileType.fromBuffer(file); + const fileType = mime.getType(file); - const type = data.ext; const imageUrl = await this.s3.uploadImage( userId, file, - type, + fileType, 'pad_chat/images' ); From 0448f47bd21af2fb02b15d260bcf77217a1fed5f Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 17:44:12 +0900 Subject: [PATCH 369/414] =?UTF-8?q?[Fix]=20=ED=8C=8C=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 17ad38a..b20146a 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -526,7 +526,7 @@ export class ChatService { const imageUrl = await this.s3.uploadImage( userId, file, - fileType, + fileType || 'png', 'pad_chat/images' ); From dfa7e461d236dd337082dcfa974635944efd13ba Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 19:52:10 +0900 Subject: [PATCH 370/414] =?UTF-8?q?[Feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=97=90=EB=9F=AC=20=EC=B5=9C=EC=A2=85=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index b20146a..e296a19 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@src/prisma/prisma.service'; import { GetMessageDto } from './dto/getMessage.dto'; import { SearchMessageDto } from './dto/serchMessage.dto'; import { S3Service } from '@src/s3/s3.service'; -import mime from 'mime'; +import * as fileType from 'file-type'; @Injectable() export class ChatService { @@ -521,12 +521,13 @@ export class ChatService { // 이미지 업로드 async handleChatFiles(userId: number, file) { - const fileType = mime.getType(file); + const data = await fileType.fromBuffer(file); + const type = data.ext; const imageUrl = await this.s3.uploadImage( userId, file, - fileType || 'png', + type, 'pad_chat/images' ); From 1f09518dd43e062c8e5120cd14e2a4ea13b3dfd8 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 20:53:56 +0900 Subject: [PATCH 371/414] =?UTF-8?q?[Feat]=20=EA=B7=B8=EB=A3=B9=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=A4=91=EB=B3=B5=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=EC=82=AC=ED=95=AD=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index e296a19..672a65e 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -87,13 +87,24 @@ export class ChatService { thumnailUrl: string ) { // 기존 채널 조회 - const exist = await this.prisma.channel.findMany({ - where: { title }, - select: { id: true }, + const result = await this.prisma.channel_users.groupBy({ + by: ['channel_id'], + where: { + user_id: { + in: userIds, + }, + }, + _count: { + user_id: true, + }, }); - if (exist.length) { - return exist[0].id; + if (result.length) { + const exist = result.filter( + data => data._count.user_id == userIds.length + )[0]; + + return exist.channel_id; } // 새로운 채널 생성 From 5db0123d8d403e1dd72caf91c30596270c5cf241 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sat, 1 Feb 2025 22:28:12 +0900 Subject: [PATCH 372/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 89 ++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c056e1e..3dffbe5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,22 +8,22 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique name String - nickname String @unique + nickname String @unique auth_provider String profile_url String? role_id Int introduce String? status_id Int - apply_count Int? @default(0) - post_count Int? @default(0) - push_alert Boolean @default(false) - following_alert Boolean @default(false) - project_alert Boolean @default(false) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + apply_count Int? @default(0) + post_count Int? @default(0) + push_alert Boolean @default(false) + following_alert Boolean @default(false) + project_alert Boolean @default(false) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt password String? job_detail String? ArtistData ArtistData[] @@ -32,22 +32,22 @@ model User { FeedCommentLikes FeedCommentLikes[] FeedLikes FeedLike[] FeedPosts FeedPost[] - Followed Follows[] @relation("FollowedUsers") - Follows Follows[] @relation("UserFollows") + Followed Follows[] @relation("FollowedUsers") + Follows Follows[] @relation("UserFollows") Message Message[] - Message_status Message_status[] MyPageProject MyPageProject[] UserLinks MyPageUserLink[] - Notification_Notification_senderIdToUser Notification[] @relation("Notification_senderIdToUser") - Notification_Notification_userIdToUser Notification[] @relation("Notification_userIdToUser") + Notification_Notification_senderIdToUser Notification[] @relation("Notification_senderIdToUser") + Notification_Notification_userIdToUser Notification[] @relation("Notification_userIdToUser") ProgrammerData ProgrammerData? ProjectPost ProjectPost[] ProjectSaves ProjectSave[] Resume Resume[] - role Role @relation(fields: [role_id], references: [id]) - status Status @relation(fields: [status_id], references: [id]) + role Role @relation(fields: [role_id], references: [id]) + status Status @relation(fields: [status_id], references: [id]) UserApplyProject UserApplyProject[] UserSkills UserSkill[] + Last_message_status Last_message_status[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -313,14 +313,15 @@ model UserApplyProject { } model Channel { - id Int @id @default(autoincrement()) - title String @default("default_channel_title") - type String - thumbnail_url String? - active Boolean @default(true) - created_at DateTime @default(now()) - Channel_users Channel_users[] - Message Message[] + id Int @id @default(autoincrement()) + title String @default("default_channel_title") + type String + thumbnail_url String? + active Boolean @default(true) + created_at DateTime @default(now()) + Channel_users Channel_users[] + Message Message[] + Last_message_status Last_message_status[] } model Channel_users { @@ -336,30 +337,30 @@ model Channel_users { } model Message { - id Int @id @default(autoincrement()) - user_id Int - created_at DateTime @default(now()) - channel_id Int - content String - type String - channel Channel @relation(fields: [channel_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) - message_status Message_status[] + id Int @id @default(autoincrement()) + user_id Int + created_at DateTime @default(now()) + channel_id Int + content String + type String + channel Channel @relation(fields: [channel_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + read_count Int @default(1) + Last_message_status Last_message_status[] @@index([channel_id], map: "Message_channel_id_fkey") @@index([user_id], map: "Message_user_id_fkey") } -model Message_status { - id Int @id @default(autoincrement()) - message_id Int - user_id Int - is_read Boolean @default(false) - message Message @relation(fields: [message_id], references: [id]) - user User @relation(fields: [user_id], references: [id]) - - @@index([message_id], map: "Message_status_message_id_fkey") - @@index([user_id], map: "Message_status_user_id_fkey") +model Last_message_status { + id Int @id @default(autoincrement()) + channel_id Int + user_id Int + last_message_id Int + message Message @relation(fields: [last_message_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + channel Channel @relation(fields: [channel_id], references: [id]) } model FeedCommentLikes { From 6fd675316dd662ae52db9f17e3b84bb06a1b69f6 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 00:39:47 +0900 Subject: [PATCH 373/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=9D=BD=EC=9D=8C=EC=B2=98?= =?UTF-8?q?=EB=A6=AC/=20readMessage=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 7 +++++++ src/chat/chat.service.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 59d96b3..d511298 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -237,4 +237,11 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { this.server.to(channelId.toString()).emit('message', leaveMessage); } + + // 메세지 실시간 읽음처리 + @SubscribeMessage('readMessage') + async handleReadCount(@MessageBody() data: { messageId: number }) { + const { messageId } = data; + await this.chatService.increaseReadCount(messageId); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 672a65e..c211c7f 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -547,4 +547,11 @@ export class ChatService { message: { code: 200, message: '이미지 업로드가 완료되었습니다.' }, }; } + + async increaseReadCount(messageId) { + await this.prisma.message.update({ + where: { id: messageId }, + data: { read_count: { increment: 1 } }, + }); + } } From d4851a0416ea8b40370b92dfaf5ff596801111bf Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 00:45:19 +0900 Subject: [PATCH 374/414] =?UTF-8?q?[Feat]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20id=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 14 ++++++++++++++ src/chat/chat.service.ts | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index d511298..1b47ae9 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -244,4 +244,18 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const { messageId } = data; await this.chatService.increaseReadCount(messageId); } + + // 라스트 메세지 id 저장 로직 + @SubscribeMessage('disconnectChannle') + async handleLastMessage( + @MessageBody() + data: { + userId: number; + channelId: number; + lastMessageId: number; + } + ) { + const { userId, channelId, lastMessageId } = data; + await this.chatService.setLastMessageId(userId, channelId, lastMessageId); + } } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index c211c7f..06c5093 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -554,4 +554,14 @@ export class ChatService { data: { read_count: { increment: 1 } }, }); } + + async setLastMessageId(userId, channelId, lastMessageId) { + await this.prisma.last_message_status.create({ + data: { + user_id: userId, + channel_id: channelId, + last_message_id: lastMessageId, + }, + }); + } } From 5ff48d8a642432d9e68215a4e68f799a06a881f0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 00:50:52 +0900 Subject: [PATCH 375/414] =?UTF-8?q?[Feat]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20id=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 06c5093..a7f0a85 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -564,4 +564,17 @@ export class ChatService { }, }); } + + // 라스트 메세지 id 조회 + async getLastMessageId(userId, channelId) { + const lastMessageId = await this.prisma.last_message_status.findFirst({ + where: { + user_id: userId, + channel_id: channelId, + }, + select: { last_message_id: true }, + }); + + return lastMessageId; + } } From 47af06ee2fe97917a58bbfe39a89a7c6d2a7863d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 00:58:59 +0900 Subject: [PATCH 376/414] =?UTF-8?q?[Feat]=20=EB=A6=AC=EB=93=9C=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=A6=9D=EA=B0=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index a7f0a85..86d479a 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -577,4 +577,17 @@ export class ChatService { return lastMessageId; } + + // 리드 카운트 증가 + async updateReadCount(lastMessageId: number, channelId) { + await this.prisma.message.updateMany({ + where: { + channel_id: channelId, + id: { gt: lastMessageId }, + }, + data: { + read_count: { increment: 1 }, + }, + }); + } } From 89d76fd2fdf9e39b6cdfc48413b767678ca3f2d4 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 00:59:25 +0900 Subject: [PATCH 377/414] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20joinChannel=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EC=95=84=EC=9D=B4=EB=94=94=20=EC=A1=B0=ED=9A=8C,?= =?UTF-8?q?=20=EB=A6=AC=EB=93=9C=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 1b47ae9..4501538 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -155,6 +155,21 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 채널 참여 client.join(channelId.toString()); console.log(`유저 ${userId} 채널 ${channelId} 참여`); + + // 라스트 메세지 id 확인 + const lastMessage = await this.chatService.getLastMessageId( + userId, + channelId + ); + + if (lastMessage) { + // 라스트 메세지 데이터가 있다면 id가 큰 값(최신 메세지) 리드 카운트 증가 + await this.chatService.updateReadCount( + lastMessage.last_message_id, + channelId + ); + } + // 채널 객체 const channelData = await this.chatService.getChannel(userId, channelId); const { channel } = channelData; From 8ed1603d96c47ef014668c91b9124805f734ae59 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:38:33 +0900 Subject: [PATCH 378/414] =?UTF-8?q?[Feat]=20server=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95;=20server=20->=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 4501538..b89ea86 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -8,13 +8,13 @@ import { OnGatewayDisconnect, } from '@nestjs/websockets'; import { ChatService } from './chat.service'; -import { Server, Socket } from 'socket.io'; +import { Namespace, Socket } from 'socket.io'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor(private readonly chatService: ChatService) {} - @WebSocketServer() server: Server; + @WebSocketServer() server: Namespace; // 유저 소켓 접속 async handleConnection(client: Socket) { From ed63a54719a8e941a871a26e5be3794ad68bd926 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:38:53 +0900 Subject: [PATCH 379/414] =?UTF-8?q?[Feat]=20=EA=B7=B8=EB=A3=B9=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EA=B8=B0=EC=A1=B4=20=EC=B1=84=EB=84=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 86d479a..21daf97 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -104,7 +104,9 @@ export class ChatService { data => data._count.user_id == userIds.length )[0]; - return exist.channel_id; + if (exist) { + return exist.channel_id; + } } // 새로운 채널 생성 From f97eedcc5f84b959c5b88e31d681774f0d01265c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:40:45 +0900 Subject: [PATCH 380/414] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20rea?= =?UTF-8?q?dCount=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index b89ea86..be3edf7 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -229,6 +229,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { messageId, user, date, + readCount: messageData.readCount, }; console.log(sendData); this.server.to(data.channelId.toString()).emit('message', sendData); From cbb18eea6400a4af4ef8b1a2a09b38e249716e7e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:43:53 +0900 Subject: [PATCH 381/414] =?UTF-8?q?[Feat]=20=EB=A6=AC=EB=93=9C=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index be3edf7..6d64fc8 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -229,7 +229,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { messageId, user, date, - readCount: messageData.readCount, + readCount: messageData.read_count, }; console.log(sendData); this.server.to(data.channelId.toString()).emit('message', sendData); From 358faf66d66b63b228f1aaddc1e52f25857de9d7 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:48:42 +0900 Subject: [PATCH 382/414] =?UTF-8?q?[Feat]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A1=B0=EA=B1=B4=EB=AC=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 6d64fc8..0b2a662 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -162,13 +162,11 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { channelId ); - if (lastMessage) { - // 라스트 메세지 데이터가 있다면 id가 큰 값(최신 메세지) 리드 카운트 증가 - await this.chatService.updateReadCount( - lastMessage.last_message_id, - channelId - ); - } + // 라스트 메세지보다 id가 큰 값(최신 메세지) 리드 카운트 증가 + await this.chatService.updateReadCount( + lastMessage.last_message_id || 0, + channelId + ); // 채널 객체 const channelData = await this.chatService.getChannel(userId, channelId); From 58bf1380e67804bedf083e13822a6aaae7ceff70 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:49:51 +0900 Subject: [PATCH 383/414] =?UTF-8?q?[Feat]=20HTTP=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=97=90=20=EB=A6=AC=EB=93=9C=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 21daf97..dee8806 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -473,6 +473,7 @@ export class ChatService { content: msg.content, channelId: msg.channel_id, date: msg.created_at, + readCount: msg.read_count, user: { userId: msg.user.id, email: msg.user.email, From 7f8d434cb5a27af71948f5d31e208689a966549c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 10:51:56 +0900 Subject: [PATCH 384/414] =?UTF-8?q?[Feat]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20null=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 0b2a662..9ee790b 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -163,10 +163,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { ); // 라스트 메세지보다 id가 큰 값(최신 메세지) 리드 카운트 증가 - await this.chatService.updateReadCount( - lastMessage.last_message_id || 0, - channelId - ); + const lastMessageId = lastMessage ? lastMessage.last_message_id : 0; + await this.chatService.updateReadCount(lastMessageId, channelId); // 채널 객체 const channelData = await this.chatService.getChannel(userId, channelId); From ee5836fcaeac478728e659ca71c7b06978e7bb8b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 11:14:18 +0900 Subject: [PATCH 385/414] =?UTF-8?q?[Feat]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20DB=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A4=91=EB=B3=B5=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- src/chat/chat.service.ts | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 9ee790b..315d4b0 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -258,7 +258,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { } // 라스트 메세지 id 저장 로직 - @SubscribeMessage('disconnectChannle') + @SubscribeMessage('disconnectChannel') async handleLastMessage( @MessageBody() data: { diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index dee8806..16c5931 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -559,13 +559,24 @@ export class ChatService { } async setLastMessageId(userId, channelId, lastMessageId) { - await this.prisma.last_message_status.create({ - data: { - user_id: userId, - channel_id: channelId, - last_message_id: lastMessageId, - }, + const exist = await this.prisma.last_message_status.findFirst({ + where: { user_id: userId, channel_id: channelId }, }); + + if (exist) { + await this.prisma.last_message_status.updateMany({ + where: { user_id: userId, channel_id: channelId }, + data: { last_message_id: lastMessageId }, + }); + } else { + await this.prisma.last_message_status.create({ + data: { + user_id: userId, + channel_id: channelId, + last_message_id: lastMessageId, + }, + }); + } } // 라스트 메세지 id 조회 From f04d5e14dda789bbb36f131172c8a4be2a24a889 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 11:17:34 +0900 Subject: [PATCH 386/414] =?UTF-8?q?[Feat]=20DB=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3dffbe5..7dcb051 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -346,7 +346,7 @@ model Message { channel Channel @relation(fields: [channel_id], references: [id]) user User @relation(fields: [user_id], references: [id]) - read_count Int @default(1) + read_count Int @default(0) Last_message_status Last_message_status[] @@index([channel_id], map: "Message_channel_id_fkey") From 4f29e7c53cbcda00fac4e00a6dc545cdfc2ee5a2 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 11:24:53 +0900 Subject: [PATCH 387/414] =?UTF-8?q?[Feat]=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20readCounted=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 315d4b0..e086b48 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -252,9 +252,13 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 메세지 실시간 읽음처리 @SubscribeMessage('readMessage') - async handleReadCount(@MessageBody() data: { messageId: number }) { + async handleReadCount( + @MessageBody() data: { messageId: number }, + @ConnectedSocket() client: Socket + ) { const { messageId } = data; await this.chatService.increaseReadCount(messageId); + client.emit('readCounted', messageId); } // 라스트 메세지 id 저장 로직 From 4b8fa2d86ce6c1d765d7d82a5fe2f9301f04f54d Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 11:35:47 +0900 Subject: [PATCH 388/414] =?UTF-8?q?[Feat]=20readCounted=20emit=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EB=B3=80=EA=B2=BD;=20client=20->=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index e086b48..e3bcff7 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -253,12 +253,11 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 메세지 실시간 읽음처리 @SubscribeMessage('readMessage') async handleReadCount( - @MessageBody() data: { messageId: number }, - @ConnectedSocket() client: Socket + @MessageBody() data: { messageId: number; channelId: number } ) { const { messageId } = data; await this.chatService.increaseReadCount(messageId); - client.emit('readCounted', messageId); + this.server.to(data.channelId.toString()).emit('readCounted', messageId); } // 라스트 메세지 id 저장 로직 From 39b187e86cb8be76a494a88625b7c2177f32df9a Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 12:11:12 +0900 Subject: [PATCH 389/414] =?UTF-8?q?[Feat]=20=EB=A6=AC=EB=93=9C=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80;=20broadcastChannelJoined?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index e3bcff7..641ff5f 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -172,6 +172,9 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 클라이언트에 채널 객체 전달 client.emit('channelJoined', channel); + this.server + .to(channelId.toString()) + .emit('broadcastChannelJoined', { channelId, lastMessageId }); } // 메세지 송수신 From ce7bcd5ee669dbad138373046ef7453decf2aac3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 14:40:54 +0900 Subject: [PATCH 390/414] =?UTF-8?q?[Fix]=20=EC=A0=84=EC=B2=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index b180283..742c87d 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -33,7 +33,16 @@ export class ProjectService { const where: any = {}; if (role) where.role = role; - if (unit) where.Tags = { some: { tag: { name: unit } } }; + // Details 테이블에서 detailJobs 조건 추가 + if (unit) { + where.Details = { + some: { + detail_role: { + name: unit, // unit 값을 detail_role.name과 비교 + }, + }, + }; + } // 커서 조건 추가 if (cursor) { From be2fce9d52dfff4fd01a4e8b8d6b933d64c2e608 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 14:41:16 +0900 Subject: [PATCH 391/414] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=A6=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/docs/notification.docs.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/modules/notification/docs/notification.docs.ts diff --git a/src/modules/notification/docs/notification.docs.ts b/src/modules/notification/docs/notification.docs.ts new file mode 100644 index 0000000..5896d64 --- /dev/null +++ b/src/modules/notification/docs/notification.docs.ts @@ -0,0 +1,68 @@ +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +export const getUnReadNotificationsDocs = { + ApiOperation: ApiOperation({ + summary: '읽지 않은 알림 조회', + description: '읽지 않은 알림을 가져옵니다.', + }), + + ApiResponse: ApiResponse({ + status: 200, + description: '읽지 않은 알림 목록', + schema: { + example: { + notifications: [ + { + notificationId: 1, + userId: 10, + senderId: 5, + type: 'comment', + message: 'John님이 회원님의 게시물에 댓글을 남겼습니다.', + isRead: false, + createdAt: '2025-02-02T12:00:00.000Z', + sender: { + nickname: 'John', + profileUrl: 'https://example.com/profile/john.jpg', + }, + }, + { + notificationId: 2, + userId: 10, + senderId: 7, + type: 'like', + message: 'Jane님이 회원님의 게시물을 좋아합니다.', + isRead: false, + createdAt: '2025-02-01T11:00:00.000Z', + sender: { + nickname: 'Jane', + profileUrl: 'https://example.com/profile/jane.jpg', + }, + }, + ], + }, + }, + }), +}; + +export const patchNotificationReadDocs = { + ApiOperation: ApiOperation({ + summary: '알림 읽음 처리', + description: '특정 알림을 읽음 처리합니다.', + }), + ApiParam: ApiParam({ + name: 'notificationId', + description: '읽음 처리할 알림 ID', + type: 'number', + example: 1, + }), + ApiResponse: ApiResponse({ + status: 200, + description: '알림 읽음 처리 완료', + schema: { + example: { + notificationId: 1, // ✅ 이름 변경 + isRead: true, + }, + }, + }), +}; From 0c20a209dd6912214867344e4ef804da1bee446b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 14:41:38 +0900 Subject: [PATCH 392/414] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=A6=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=EB=93=A4=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index b5348ed..66abf38 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -4,27 +4,36 @@ import { Req, UseGuards, UseInterceptors, + Get, + Patch, + Param, } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { NotificationsService } from './notification.service'; import { SseInterceptor } from './Interceptors/notification.interceptor'; +import { ApiBearerAuth } from '@nestjs/swagger'; +import { + getUnReadNotificationsDocs, + patchNotificationReadDocs, +} from './docs/notification.docs'; @Controller('notifications') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) @UseInterceptors(SseInterceptor) export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @Sse('stream') - @UseGuards(JwtAuthGuard) - streamNotifications(@Req() req): Observable { + async streamNotifications(@Req() req): Promise> { const userId = req.user?.user_id; if (!userId) { console.error('🚨 사용자 인증 정보가 필요합니다.'); return of({ - event: 'error', // ✅ 이벤트 이름 추가 + event: 'error', data: { type: 'error', message: '사용자 인증 정보가 필요합니다.', @@ -39,11 +48,26 @@ export class NotificationsController { console.log(`❌ 사용자 ${userId}와의 SSE 연결 종료`); }); + // 🔹 SSE 연결 시 기존 읽지 않은 알림 전송 + const unreadNotifications = + await this.notificationsService.getUnreadNotifications(userId); + + // ✅ notifications 배열로 접근 + unreadNotifications.notifications.forEach(notification => { + this.notificationsService.sendRealTimeNotification(userId, { + type: notification.type, + message: notification.message, + senderNickname: notification.sender.nickname, + senderProfileUrl: notification.sender.profileUrl, + }); + }); + return this.notificationsService.notifications$.asObservable().pipe( filter(notification => notification.userId === userId), map(notification => ({ - event: 'message', // ✅ 'message' 이벤트 이름 추가 + event: 'message', // ✅ 'message' 이벤트 설정 data: { + notificationId: notification.id, type: notification.type, message: notification.message, senderNickname: notification.senderNickname, @@ -53,4 +77,29 @@ export class NotificationsController { })) ); } + + @Get('unread') + @getUnReadNotificationsDocs.ApiOperation + @getUnReadNotificationsDocs.ApiResponse + async getUnreadNotifications(@Req() req) { + const userId = req.user?.user_id; + return this.notificationsService.getUnreadNotifications(userId); + } + + // 특정 알림을 읽음 상태로 변경 + @Patch(':notificationId/read') + @patchNotificationReadDocs.ApiOperation + @patchNotificationReadDocs.ApiParam + @patchNotificationReadDocs.ApiResponse + async markNotificationAsRead( + @Req() req, + @Param('notificationId') notificationId: string + ) { + const userId = req.user?.user_id; + const numNotificationId = parseInt(notificationId, 10); + return this.notificationsService.markNotificationAsRead( + userId, + numNotificationId + ); + } } From bb4afb1bcfe1a50a4692cc02f57c3aae6111d901 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 14:41:44 +0900 Subject: [PATCH 393/414] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=A6=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=EB=93=A4=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.service.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index d949652..26895a6 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { ApiBearerAuth } from '@nestjs/swagger'; import { PrismaService } from '@src/prisma/prisma.service'; import { Subject } from 'rxjs'; @@ -41,4 +42,65 @@ export class NotificationsService { timestamp: new Date().toISOString(), // 알림 전송 시간 }); } + + async getUnreadNotifications(userId: number) { + const unreadNotifications = await this.prisma.notification.findMany({ + where: { + userId: userId, + isRead: false, + }, + include: { + sender: { + select: { + nickname: true, + profile_url: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + // 데이터를 변환하여 반환 + return { + notifications: unreadNotifications.map(notification => ({ + notificationId: notification.id, // `id`를 `notificationId`로 변경 + userId: notification.userId, + senderId: notification.senderId, + type: notification.type, + message: notification.message, + isRead: notification.isRead, + createdAt: notification.createdAt, + sender: { + nickname: notification.sender.nickname, + profileUrl: notification.sender.profile_url, // `profile_url` -> `profileUrl` + }, + })), + }; + } + + async markNotificationAsRead(userId: number, notificationId: number) { + const notification = await this.prisma.notification.findUnique({ + where: { id: notificationId }, + }); + + if (!notification) { + throw new Error('알림을 찾을 수 없습니다.'); + } + + if (notification.userId !== userId) { + throw new Error('본인의 알림만 읽음 처리할 수 있습니다.'); + } + + const updatedNotification = await this.prisma.notification.update({ + where: { id: notificationId }, + data: { isRead: true }, + }); + + return { + notificationId: updatedNotification.id, // 필드 이름 변경 + isRead: updatedNotification.isRead, + }; + } } From 6f4f482adf707eb4a9bc0f3bbdcc1563aae5e8f0 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 14:42:26 +0900 Subject: [PATCH 394/414] =?UTF-8?q?[Feat]=20readMessage=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=EC=8B=9C=EC=97=90?= =?UTF-8?q?=EB=8F=84=20lastMessageId=20=EC=84=A4=EC=A0=95=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 641ff5f..ea0954d 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -256,10 +256,16 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 메세지 실시간 읽음처리 @SubscribeMessage('readMessage') async handleReadCount( - @MessageBody() data: { messageId: number; channelId: number } + @MessageBody() + data: { + userId: number; + messageId: number; + channelId: number; + } ) { - const { messageId } = data; + const { userId, channelId, messageId } = data; await this.chatService.increaseReadCount(messageId); + await this.chatService.setLastMessageId(userId, channelId, messageId); this.server.to(data.channelId.toString()).emit('readCounted', messageId); } From 73cc294821bdac00f17cd3768bc03b9004e6a49e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:06:18 +0900 Subject: [PATCH 395/414] =?UTF-8?q?[Fix]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.service.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 26895a6..4c3d94e 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -44,6 +44,9 @@ export class NotificationsService { } async getUnreadNotifications(userId: number) { + console.log(`🔍 [getUnreadNotifications] 시작 - userId: ${userId}`); + + // 1. 읽지 않은 알림 조회 const unreadNotifications = await this.prisma.notification.findMany({ where: { userId: userId, @@ -62,9 +65,14 @@ export class NotificationsService { }, }); - // 데이터를 변환하여 반환 - return { - notifications: unreadNotifications.map(notification => ({ + console.log( + '📥 [getUnreadNotifications] DB 조회 결과:', + unreadNotifications + ); + + // 2. 데이터를 변환하여 반환 + const transformedNotifications = unreadNotifications.map(notification => { + const transformedNotification = { notificationId: notification.id, // `id`를 `notificationId`로 변경 userId: notification.userId, senderId: notification.senderId, @@ -76,7 +84,21 @@ export class NotificationsService { nickname: notification.sender.nickname, profileUrl: notification.sender.profile_url, // `profile_url` -> `profileUrl` }, - })), + }; + + console.log( + '🔧 [getUnreadNotifications] 변환된 알림:', + transformedNotification + ); + return transformedNotification; + }); + + console.log('📤 [getUnreadNotifications] 최종 반환 데이터:', { + notifications: transformedNotifications, + }); + + return { + notifications: transformedNotifications, }; } From 3852ee695cc55c28a9eb4b5066ca78111cfd0034 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 16:10:46 +0900 Subject: [PATCH 396/414] =?UTF-8?q?[Feat]=20joinChannel=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EC=97=90=20=EB=9D=BC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 13 +++++++++++++ src/chat/chat.service.ts | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index ea0954d..d649615 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -166,6 +166,19 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const lastMessageId = lastMessage ? lastMessage.last_message_id : 0; await this.chatService.updateReadCount(lastMessageId, channelId); + // 채널 라스트 메세지 조회 + const channelLastMessage = + await this.chatService.getChannelLastMessage(channelId); + + const channelLastMessageId = channelLastMessage.id; + + // 채널 입장 시 채널의 마지막 메세지 last message로 저장 + await this.chatService.setLastMessageId( + userId, + channelId, + channelLastMessageId + ); + // 채널 객체 const channelData = await this.chatService.getChannel(userId, channelId); const { channel } = channelData; diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 16c5931..7c80edb 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -604,4 +604,15 @@ export class ChatService { }, }); } + + async getChannelLastMessage(channelId: number) { + const data = await this.prisma.message.findFirst({ + where: { channel_id: channelId }, + orderBy: { id: 'desc' }, + take: 1, + select: { id: true }, + }); + + return data; + } } From e1ff4e0fce870b7aefda71eb37659b493757b546 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 16:21:45 +0900 Subject: [PATCH 397/414] =?UTF-8?q?[Refactor]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index d649615..4023cb5 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -185,9 +185,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 클라이언트에 채널 객체 전달 client.emit('channelJoined', channel); - this.server - .to(channelId.toString()) - .emit('broadcastChannelJoined', { channelId, lastMessageId }); + this.server.to(channelId.toString()).emit('broadcastChannelJoined'); } // 메세지 송수신 @@ -281,18 +279,4 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { await this.chatService.setLastMessageId(userId, channelId, messageId); this.server.to(data.channelId.toString()).emit('readCounted', messageId); } - - // 라스트 메세지 id 저장 로직 - @SubscribeMessage('disconnectChannel') - async handleLastMessage( - @MessageBody() - data: { - userId: number; - channelId: number; - lastMessageId: number; - } - ) { - const { userId, channelId, lastMessageId } = data; - await this.chatService.setLastMessageId(userId, channelId, lastMessageId); - } } From a6bad1ee278e790dc3f6eab9f506b02f59374de3 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:39:56 +0900 Subject: [PATCH 398/414] =?UTF-8?q?[Feat]=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feed/feed.service.ts | 49 +++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/feed/feed.service.ts b/src/feed/feed.service.ts index 7fbf8d0..6fa260e 100644 --- a/src/feed/feed.service.ts +++ b/src/feed/feed.service.ts @@ -376,21 +376,34 @@ export class FeedService { ); } + // 본인이 작성한 피드가 아닌 경우 알림 생성 if (feed.user_id !== userId) { const sender = await this.prisma.user.findUnique({ where: { id: userId }, select: { nickname: true, profile_url: true }, }); + if (!sender) { + throw new HttpException( + '보낸 사람 정보를 찾을 수 없습니다.', + HttpStatus.NOT_FOUND + ); + } + const message = `${sender.nickname}님이 회원님의 피드를 좋아합니다.`; - await this.notificationsService.createNotification( - feed.user_id, - userId, - 'like', - message - ); + // 알림 생성 및 notificationId 받기 + const createdNotification = + await this.notificationsService.createNotification( + feed.user_id, // 피드 작성자 ID + userId, // 좋아요 누른 사용자 ID + 'like', + message + ); + + // 생성된 알림의 notificationId를 포함하여 실시간 알림 전송 this.notificationsService.sendRealTimeNotification(feed.user_id, { + notificationId: createdNotification.notificationId, // 알림 ID 추가 type: 'like', message, senderNickname: sender.nickname, @@ -562,6 +575,8 @@ export class FeedService { async createComment(userId: number, feedId: number, commentDto: CommentDto) { try { const content = commentDto.content; + + // 댓글 생성 await this.prisma.feedComment.create({ data: { user_id: userId, @@ -570,14 +585,16 @@ export class FeedService { }, }); + // 피드 댓글 카운트 증가 await this.prisma.feedPost.update({ where: { id: feedId }, data: { comment_count: { increment: 1 } }, }); + // 피드 작성자 정보 가져오기 const feed = await this.prisma.feedPost.findUnique({ where: { id: feedId }, - include: { user: true }, + include: { user: true }, // 작성자 정보 포함 }); if (!feed) { @@ -587,6 +604,7 @@ export class FeedService { ); } + // 피드 작성자가 댓글 작성자가 아닌 경우 알림 생성 if (feed.user_id !== userId) { const sender = await this.prisma.user.findUnique({ where: { id: userId }, @@ -601,14 +619,19 @@ export class FeedService { } const message = `${sender.nickname}님이 회원님의 피드에 댓글을 남겼습니다.`; - await this.notificationsService.createNotification( - feed.user_id, - userId, - 'comment', - message - ); + // 알림 생성 및 notificationId 받기 + const createdNotification = + await this.notificationsService.createNotification( + feed.user_id, // 피드 작성자 ID + userId, // 댓글 작성자 ID + 'comment', + message + ); + + // 생성된 알림 ID를 포함하여 실시간 알림 전송 this.notificationsService.sendRealTimeNotification(feed.user_id, { + notificationId: createdNotification.notificationId, // 알림 ID 추가 type: 'comment', message, senderNickname: sender.nickname, From 452cebcceaba30797db88d180736fde3a510bbc5 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:40:28 +0900 Subject: [PATCH 399/414] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=A6=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9D=91=EB=8B=B5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 9 +++-- .../notification/notification.service.ts | 34 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 66abf38..385cd14 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -22,11 +22,11 @@ import { @Controller('notifications') @ApiBearerAuth() @UseGuards(JwtAuthGuard) -@UseInterceptors(SseInterceptor) export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @Sse('stream') + @UseInterceptors(SseInterceptor) async streamNotifications(@Req() req): Promise> { const userId = req.user?.user_id; @@ -48,13 +48,12 @@ export class NotificationsController { console.log(`❌ 사용자 ${userId}와의 SSE 연결 종료`); }); - // 🔹 SSE 연결 시 기존 읽지 않은 알림 전송 const unreadNotifications = await this.notificationsService.getUnreadNotifications(userId); - // ✅ notifications 배열로 접근 unreadNotifications.notifications.forEach(notification => { this.notificationsService.sendRealTimeNotification(userId, { + notificationId: notification.notificationId, // 포함된 notificationId type: notification.type, message: notification.message, senderNickname: notification.sender.nickname, @@ -65,9 +64,9 @@ export class NotificationsController { return this.notificationsService.notifications$.asObservable().pipe( filter(notification => notification.userId === userId), map(notification => ({ - event: 'message', // ✅ 'message' 이벤트 설정 + event: 'message', data: { - notificationId: notification.id, + notificationId: notification.notificationId, // 클라이언트에 전달 type: notification.type, message: notification.message, senderNickname: notification.senderNickname, diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 4c3d94e..c6ac9d6 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -17,7 +17,7 @@ export class NotificationsService { message: string ) { try { - return await this.prisma.notification.create({ + const createdNotification = await this.prisma.notification.create({ data: { userId, senderId, @@ -25,24 +25,19 @@ export class NotificationsService { message, }, }); + + console.log('✅ 알림 생성 완료:', createdNotification); + + return { + notificationId: createdNotification.id, // `id`를 `notificationId`로 변경 + ...createdNotification, + }; } catch (error) { console.error('알림 생성 중 오류:', error.message); throw new Error('알림 생성에 실패했습니다.'); } } - // 실시간 알림 전송 - sendRealTimeNotification(userId: number, data: any) { - this.notifications$.next({ - userId, - type: data.type || 'notification', // 이벤트 유형 - message: data.message, // 알림 메시지 - senderNickname: data.senderNickname, // 보낸 사람 닉네임 - senderProfileUrl: data.senderProfileUrl, // 보낸 사람 프로필 URL - timestamp: new Date().toISOString(), // 알림 전송 시간 - }); - } - async getUnreadNotifications(userId: number) { console.log(`🔍 [getUnreadNotifications] 시작 - userId: ${userId}`); @@ -73,7 +68,7 @@ export class NotificationsService { // 2. 데이터를 변환하여 반환 const transformedNotifications = unreadNotifications.map(notification => { const transformedNotification = { - notificationId: notification.id, // `id`를 `notificationId`로 변경 + notificationId: notification.id, // userId: notification.userId, senderId: notification.senderId, type: notification.type, @@ -125,4 +120,15 @@ export class NotificationsService { isRead: updatedNotification.isRead, }; } + sendRealTimeNotification(userId: number, data: any) { + this.notifications$.next({ + userId, + notificationId: data.notificationId, // 알림 ID 포함 + type: data.type || 'notification', // 이벤트 유형 + message: data.message, // 알림 메시지 + senderNickname: data.senderNickname, // 보낸 사람 닉네임 + senderProfileUrl: data.senderProfileUrl, // 보낸 사람 프로필 URL + timestamp: new Date().toISOString(), // 알림 전송 시간 + }); + } } From 014e95372b16383a21a88ee0029a150b983bf847 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:40:47 +0900 Subject: [PATCH 400/414] =?UTF-8?q?[Feat]=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/follow/follow.service.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/modules/follow/follow.service.ts b/src/modules/follow/follow.service.ts index 1663f88..680ab42 100644 --- a/src/modules/follow/follow.service.ts +++ b/src/modules/follow/follow.service.ts @@ -10,7 +10,6 @@ export class FollowService { ) {} async toggleFollow(userId: number, targetUserId: number) { - // 현재 팔로우 상태 확인 const existingFollow = await this.prisma.follows.findFirst({ where: { following_user_id: userId, @@ -19,7 +18,6 @@ export class FollowService { }); if (existingFollow) { - // 언팔로우 처리 await this.prisma.follows.delete({ where: { id: existingFollow.id }, }); @@ -29,7 +27,6 @@ export class FollowService { isFollowing: false, }; } else { - // 팔로우 처리 await this.prisma.follows.create({ data: { following_user_id: userId, @@ -37,7 +34,6 @@ export class FollowService { }, }); - // 상대방 정보 가져오기 const sender = await this.prisma.user.findUnique({ where: { id: userId }, }); @@ -49,23 +45,25 @@ export class FollowService { throw new Error('사용자 정보를 찾을 수 없습니다.'); } - // 알림 메시지 생성 및 전송 const message = `${sender.nickname}님이 회원님을 팔로우하기 시작했습니다.`; + + // 알림 생성 및 생성된 알림의 id 포함 + const createdNotification = + await this.notificationsService.createNotification( + targetUserId, + userId, + 'follow', + message + ); + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'follow', message, senderNickname: sender.nickname, senderProfileUrl: sender.profile_url, - type: 'follow', }; - // 알림 생성 - await this.notificationsService.createNotification( - targetUserId, - userId, - notificationData.type, - notificationData.message - ); - // SSE를 통해 실시간 알림 전송 this.notificationsService.sendRealTimeNotification( targetUserId, From f6b75cf4f6b0e1f5034f759a8eab58b65f2880c6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:40:55 +0900 Subject: [PATCH 401/414] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/project/project.service.ts | 33 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index 742c87d..88c0410 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -440,14 +440,18 @@ export class ProjectService { }); const message = `${sender.nickname}님이 회원님의 프로젝트에 지원했습니다.`; - await this.notificationsService.createNotification( - project.user_id, // 프로젝트 작성자 ID - userId, // 지원자 ID - 'application', - message - ); + // 알림 생성 및 `notificationId` 반환 + const createdNotification = + await this.notificationsService.createNotification( + project.user_id, // 프로젝트 작성자 ID + userId, // 지원자 ID + 'application', + message + ); + // 실시간 알림 전송 (notificationId 포함) this.notificationsService.sendRealTimeNotification(project.user_id, { + notificationId: createdNotification.notificationId, // 알림 ID 추가 type: 'application', message, senderNickname: sender.nickname, @@ -740,14 +744,19 @@ export class ProjectService { }); const message = `${sender.nickname}님이 회원님의 프로젝트 지원 상태를 '${status}'로 변경했습니다.`; - await this.notificationsService.createNotification( - targetUserId, // 지원자 ID - userId, // 프로젝트 작성자 ID - 'applicationStatus', - message - ); + // 알림 생성 및 notificationId 받기 + const createdNotification = + await this.notificationsService.createNotification( + targetUserId, // 지원자 ID + userId, // 프로젝트 작성자 ID + 'applicationStatus', + message + ); + + // 실시간 알림 전송 (notificationId 포함) this.notificationsService.sendRealTimeNotification(targetUserId, { + notificationId: createdNotification.notificationId, // 알림 ID 추가 type: 'applicationStatus', message, senderNickname: sender.nickname, From 7b62991050e35dbed24997a4d12f89cddb2c703e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 16:43:19 +0900 Subject: [PATCH 402/414] =?UTF-8?q?[Fix]=20=EA=B0=9C=EC=9D=B8=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 7c80edb..8b28c88 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -45,9 +45,24 @@ export class ChatService { // 채널id 리턴 (개인 채팅방) async getChannelId(userId1: number, userId2: number) { // 매핑 테이블에서 파라미터로 전달된 유저 아이디에 해당하는 데이터 찾기 + + const privateChannel = await this.prisma.channel.findMany({ + where: { type: 'private' }, + select: { id: true }, + }); + + const privateChannelIds = []; + + privateChannel.map(res => { + privateChannelIds.push(res.id); + }); + const result = await this.prisma.channel_users.groupBy({ by: ['channel_id'], where: { + channel_id: { + in: privateChannelIds, + }, user_id: { in: [userId1, userId2], }, From 5a16a84a2c07d6c2dd2703a00ffa74ae14db2f2f Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 16:50:49 +0900 Subject: [PATCH 403/414] =?UTF-8?q?[Fix]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/notification/notification.controller.ts | 7 +++++++ src/modules/notification/notification.service.ts | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 385cd14..24b3c1e 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -7,6 +7,7 @@ import { Get, Patch, Param, + BadRequestException, } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -95,7 +96,13 @@ export class NotificationsController { @Param('notificationId') notificationId: string ) { const userId = req.user?.user_id; + + // notificationId 유효성 검사 const numNotificationId = parseInt(notificationId, 10); + if (isNaN(numNotificationId)) { + throw new BadRequestException('유효하지 않은 알림 ID입니다.'); + } + return this.notificationsService.markNotificationAsRead( userId, numNotificationId diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index c6ac9d6..01a68a8 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -98,6 +98,13 @@ export class NotificationsService { } async markNotificationAsRead(userId: number, notificationId: number) { + console.log( + `🔍 [markNotificationAsRead] notificationId: ${notificationId}` + ); + if (!notificationId || isNaN(notificationId)) { + throw new Error('Invalid notificationId provided'); + } + const notification = await this.prisma.notification.findUnique({ where: { id: notificationId }, }); @@ -120,6 +127,7 @@ export class NotificationsService { isRead: updatedNotification.isRead, }; } + sendRealTimeNotification(userId: number, data: any) { this.notifications$.next({ userId, From 5ab863dbd40907d789198cefca76a4432979d42e Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 17:01:22 +0900 Subject: [PATCH 404/414] =?UTF-8?q?[Fix]=20=EC=B1=84=EB=84=90=20=EB=9D=BC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 4023cb5..ccd9493 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -170,7 +170,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channelLastMessage = await this.chatService.getChannelLastMessage(channelId); - const channelLastMessageId = channelLastMessage.id; + const channelLastMessageId = channelLastMessage?.id || 0; // 채널 입장 시 채널의 마지막 메세지 last message로 저장 await this.chatService.setLastMessageId( From 000e1e562f930e1229d1fae72e5e8af861b920ba Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 17:04:47 +0900 Subject: [PATCH 405/414] =?UTF-8?q?[Fix]=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 7 +++---- .../notification/notification.service.ts | 18 +++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 24b3c1e..dc5cb9a 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -87,23 +87,22 @@ export class NotificationsController { } // 특정 알림을 읽음 상태로 변경 - @Patch(':notificationId/read') + @Patch(':notificationId/delete') @patchNotificationReadDocs.ApiOperation @patchNotificationReadDocs.ApiParam @patchNotificationReadDocs.ApiResponse - async markNotificationAsRead( + async markNotificationAsReadAndDelete( @Req() req, @Param('notificationId') notificationId: string ) { const userId = req.user?.user_id; - // notificationId 유효성 검사 const numNotificationId = parseInt(notificationId, 10); if (isNaN(numNotificationId)) { throw new BadRequestException('유효하지 않은 알림 ID입니다.'); } - return this.notificationsService.markNotificationAsRead( + return this.notificationsService.markNotificationAsReadAndDelete( userId, numNotificationId ); diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 01a68a8..2e28bf3 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -97,10 +97,14 @@ export class NotificationsService { }; } - async markNotificationAsRead(userId: number, notificationId: number) { + async markNotificationAsReadAndDelete( + userId: number, + notificationId: number + ) { console.log( - `🔍 [markNotificationAsRead] notificationId: ${notificationId}` + `🔍 [markNotificationAsReadAndDelete] notificationId: ${notificationId}` ); + if (!notificationId || isNaN(notificationId)) { throw new Error('Invalid notificationId provided'); } @@ -114,17 +118,17 @@ export class NotificationsService { } if (notification.userId !== userId) { - throw new Error('본인의 알림만 읽음 처리할 수 있습니다.'); + throw new Error('본인의 알림만 처리할 수 있습니다.'); } - const updatedNotification = await this.prisma.notification.update({ + // 알림 삭제 + await this.prisma.notification.delete({ where: { id: notificationId }, - data: { isRead: true }, }); return { - notificationId: updatedNotification.id, // 필드 이름 변경 - isRead: updatedNotification.isRead, + message: '알림이 성공적으로 삭제되었습니다.', + notificationId, }; } From 8fa047e7df1fa0a9b147655ae042eeb7eefee900 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 17:05:03 +0900 Subject: [PATCH 406/414] =?UTF-8?q?[Fix]=20=EB=9D=BC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=99=B8=EB=9E=98=ED=82=A4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index ccd9493..c1c2c9b 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -170,15 +170,16 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { const channelLastMessage = await this.chatService.getChannelLastMessage(channelId); - const channelLastMessageId = channelLastMessage?.id || 0; - - // 채널 입장 시 채널의 마지막 메세지 last message로 저장 - await this.chatService.setLastMessageId( - userId, - channelId, - channelLastMessageId - ); + const channelLastMessageId = channelLastMessage?.id; + if (channelLastMessageId) { + // 채널 입장 시 채널의 마지막 메세지 last message로 저장 + await this.chatService.setLastMessageId( + userId, + channelId, + channelLastMessageId + ); + } // 채널 객체 const channelData = await this.chatService.getChannel(userId, channelId); const { channel } = channelData; From afdda2dced9c1955f8dbf2235293f37820508394 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 17:16:41 +0900 Subject: [PATCH 407/414] =?UTF-8?q?[Fix]=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.controller.ts | 4 ++-- .../notification/notification.service.ts | 21 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index dc5cb9a..b077c79 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -95,14 +95,14 @@ export class NotificationsController { @Req() req, @Param('notificationId') notificationId: string ) { - const userId = req.user?.user_id; + const userId = Number(req.user?.user_id); const numNotificationId = parseInt(notificationId, 10); if (isNaN(numNotificationId)) { throw new BadRequestException('유효하지 않은 알림 ID입니다.'); } - return this.notificationsService.markNotificationAsReadAndDelete( + return this.notificationsService.markNotificationAsRead( userId, numNotificationId ); diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 2e28bf3..3f68c75 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -97,18 +97,7 @@ export class NotificationsService { }; } - async markNotificationAsReadAndDelete( - userId: number, - notificationId: number - ) { - console.log( - `🔍 [markNotificationAsReadAndDelete] notificationId: ${notificationId}` - ); - - if (!notificationId || isNaN(notificationId)) { - throw new Error('Invalid notificationId provided'); - } - + async markNotificationAsRead(userId: number, notificationId: number) { const notification = await this.prisma.notification.findUnique({ where: { id: notificationId }, }); @@ -121,14 +110,14 @@ export class NotificationsService { throw new Error('본인의 알림만 처리할 수 있습니다.'); } - // 알림 삭제 - await this.prisma.notification.delete({ + const updatedNotification = await this.prisma.notification.update({ where: { id: notificationId }, + data: { isRead: true }, }); return { - message: '알림이 성공적으로 삭제되었습니다.', - notificationId, + notificationId: updatedNotification.id, + isRead: updatedNotification.isRead, }; } From 834500b1bcea91f7bc35bcf70ffc8311cd244093 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 17:28:12 +0900 Subject: [PATCH 408/414] =?UTF-8?q?[Feat]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/notification/notification.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 3f68c75..154ecad 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -98,6 +98,8 @@ export class NotificationsService { } async markNotificationAsRead(userId: number, notificationId: number) { + console.log("============ 읽음 처리 메소드 호출 =========="); + console.log('============ 읽음 처리 메소드 호출 =========='); const notification = await this.prisma.notification.findUnique({ where: { id: notificationId }, }); @@ -115,6 +117,8 @@ export class NotificationsService { data: { isRead: true }, }); + console.log('============ 읽음 처리 메소드 호출끝 =========='); + console.log('============ 읽음 처리 메소드 호출끝 =========='); return { notificationId: updatedNotification.id, isRead: updatedNotification.isRead, From 0936e9ef9c2601c7b544e5da45b79999dfa3e1c2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 2 Feb 2025 17:56:10 +0900 Subject: [PATCH 409/414] =?UTF-8?q?[Refactor]=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B9=85=20=EB=AC=B8=EA=B5=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/notification/notification.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 154ecad..3f68c75 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -98,8 +98,6 @@ export class NotificationsService { } async markNotificationAsRead(userId: number, notificationId: number) { - console.log("============ 읽음 처리 메소드 호출 =========="); - console.log('============ 읽음 처리 메소드 호출 =========='); const notification = await this.prisma.notification.findUnique({ where: { id: notificationId }, }); @@ -117,8 +115,6 @@ export class NotificationsService { data: { isRead: true }, }); - console.log('============ 읽음 처리 메소드 호출끝 =========='); - console.log('============ 읽음 처리 메소드 호출끝 =========='); return { notificationId: updatedNotification.id, isRead: updatedNotification.isRead, From 7418f77233d72d0405264c4f2c6ff5e4833ee46b Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 19:39:52 +0900 Subject: [PATCH 410/414] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=9E=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=9E=84=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 8 +++++++- src/chat/chat.module.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index c1c2c9b..5db2bd5 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -9,10 +9,14 @@ import { } from '@nestjs/websockets'; import { ChatService } from './chat.service'; import { Namespace, Socket } from 'socket.io'; +import { NotificationsService } from '@src/modules/notification/notification.service'; @WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { - constructor(private readonly chatService: ChatService) {} + constructor( + private readonly chatService: ChatService, + private readonly notificationService: NotificationsService + ) {} @WebSocketServer() server: Namespace; @@ -81,6 +85,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 유저2의 채널 리스트에 해당 채널 추가 userSocket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); + + const createdNotification = await this.no; } else { // 오프라인 일때 console.log(`User ${userId2} is not connected.`); diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 1c0595c..10ee31e 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -4,9 +4,10 @@ import { PrismaModule } from '@src/prisma/prisma.module'; import { JwtModule } from '@nestjs/jwt'; import { ChatController } from './chat.controller'; import { S3Module } from '@src/s3/s3.module'; +import { NotificationModule } from '@src/modules/notification/notification.module'; @Module({ - imports: [PrismaModule, JwtModule, S3Module], + imports: [PrismaModule, JwtModule, S3Module, NotificationModule], providers: [ChatService], exports: [ChatService], controllers: [ChatController], From 01a6f6dc7ead6d05e01714b995e8d9c004332574 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 19:46:35 +0900 Subject: [PATCH 411/414] =?UTF-8?q?[Feat]=20=EA=B0=9C=EC=9D=B8=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 5db2bd5..d38fc22 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -86,7 +86,34 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { userSocket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); - const createdNotification = await this.no; + // 알람 기능 + const sender = await this.chatService.getSenderProfile(userId1); + + const message = `${sender.nickname}님과의 개인 채팅방이 생성되었습니다.`; + + // 알람 DB에 저장 + const createdNotification = + await this.notificationService.createNotification( + userId2, + userId1, + 'privateChat', + message + ); + + // 전송할 알림 데이터 객체 + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'follow', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profileUrl, + }; + + // SSE를 통해 실시간 알림 전송 + this.notificationService.sendRealTimeNotification( + userId2, + notificationData + ); } else { // 오프라인 일때 console.log(`User ${userId2} is not connected.`); From a1e1f7e99bf1f0f90410775b95b4db0b4b73b11c Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 19:54:19 +0900 Subject: [PATCH 412/414] =?UTF-8?q?[Feat]=20=EA=B7=B8=EB=A3=B9=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index d38fc22..140861d 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -103,7 +103,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 전송할 알림 데이터 객체 const notificationData = { notificationId: createdNotification.notificationId, // 포함된 notificationId - type: 'follow', + type: 'privateChat', message, senderNickname: sender.nickname, senderProfileUrl: sender.profileUrl, @@ -164,9 +164,35 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { targetSockets.includes(socket.id) ); + const sender = await this.chatService.getSenderProfile(userId); + const message = `${sender.nickname}님이 단체 채팅방을 생성했습니다.`; + // 각 멤버들의 채널 리스트에 해당 채널 추가 - userSockets.forEach(socket => { + userSockets.forEach(async socket => { socket.emit('channelAdded', channel); + + const createdNotification = + await this.notificationService.createNotification( + socket.data.userId, + userId, + 'groupChat', + message + ); + + // 전송할 알림 데이터 객체 + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'groupChat', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profileUrl, + }; + + // SSE를 통해 실시간 알림 전송 + this.notificationService.sendRealTimeNotification( + socket.data.userId, + notificationData + ); }); } else { // 오프라인 일때 From c3df724c7839e4839214e3c52c54db69c3214fd2 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 20:36:55 +0900 Subject: [PATCH 413/414] =?UTF-8?q?[Feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=95=8C=EB=A6=BC=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=EC=8B=9C=EC=97=90=EB=8F=84?= =?UTF-8?q?=20=EC=9E=91=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 110 ++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 140861d..2a6d176 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -85,39 +85,39 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { // 유저2의 채널 리스트에 해당 채널 추가 userSocket.emit('channelAdded', channel); console.log(`channel ${channelId} added in ${userId2} channel list`); + } else { + // 오프라인 일때 + console.log(`User ${userId2} is not connected.`); + } - // 알람 기능 - const sender = await this.chatService.getSenderProfile(userId1); + // 알람 기능 + const sender = await this.chatService.getSenderProfile(userId1); - const message = `${sender.nickname}님과의 개인 채팅방이 생성되었습니다.`; + const message = `${sender.nickname}님과의 개인 채팅방이 생성되었습니다.`; - // 알람 DB에 저장 - const createdNotification = - await this.notificationService.createNotification( - userId2, - userId1, - 'privateChat', - message - ); - - // 전송할 알림 데이터 객체 - const notificationData = { - notificationId: createdNotification.notificationId, // 포함된 notificationId - type: 'privateChat', - message, - senderNickname: sender.nickname, - senderProfileUrl: sender.profileUrl, - }; - - // SSE를 통해 실시간 알림 전송 - this.notificationService.sendRealTimeNotification( + // 알람 DB에 저장 + const createdNotification = + await this.notificationService.createNotification( userId2, - notificationData + userId1, + 'privateChat', + message ); - } else { - // 오프라인 일때 - console.log(`User ${userId2} is not connected.`); - } + + // 전송할 알림 데이터 객체 + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'privateChat', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profileUrl, + }; + + // SSE를 통해 실시간 알림 전송 + this.notificationService.sendRealTimeNotification( + userId2, + notificationData + ); // 클라이언트에 채널id 전달 client.emit('channelCreated', channel); @@ -164,41 +164,43 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { targetSockets.includes(socket.id) ); - const sender = await this.chatService.getSenderProfile(userId); - const message = `${sender.nickname}님이 단체 채팅방을 생성했습니다.`; - // 각 멤버들의 채널 리스트에 해당 채널 추가 userSockets.forEach(async socket => { socket.emit('channelAdded', channel); - - const createdNotification = - await this.notificationService.createNotification( - socket.data.userId, - userId, - 'groupChat', - message - ); - - // 전송할 알림 데이터 객체 - const notificationData = { - notificationId: createdNotification.notificationId, // 포함된 notificationId - type: 'groupChat', - message, - senderNickname: sender.nickname, - senderProfileUrl: sender.profileUrl, - }; - - // SSE를 통해 실시간 알림 전송 - this.notificationService.sendRealTimeNotification( - socket.data.userId, - notificationData - ); }); } else { // 오프라인 일때 console.log('모든 유저가 오프라인 상태입니다.'); } + const sender = await this.chatService.getSenderProfile(userId); + const message = `${sender.nickname}님이 단체 채팅방을 생성했습니다.`; + + groupMemberIds.forEach(async memberId => { + const createdNotification = + await this.notificationService.createNotification( + memberId, + userId, + 'groupChat', + message + ); + + // 전송할 알림 데이터 객체 + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'groupChat', + message, + senderNickname: sender.nickname, + senderProfileUrl: sender.profileUrl, + }; + + // SSE를 통해 실시간 알림 전송 + this.notificationService.sendRealTimeNotification( + memberId, + notificationData + ); + }); + // 클라이언트에 채널id 전달 client.emit('groupCreated', channel); } From 741efcaec70cb44df41535f250d7fbd4a4e62752 Mon Sep 17 00:00:00 2001 From: Dong Hyun Date: Sun, 2 Feb 2025 21:10:18 +0900 Subject: [PATCH 414/414] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=BC=93=20=EC=98=A4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EC=8B=9C=20=EC=95=8C=EB=9E=8C=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat.gateway.ts | 30 ++++++++++++++++++++++++++++++ src/chat/chat.service.ts | 17 +++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 2a6d176..3e6dcb2 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -304,6 +304,36 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { readCount: messageData.read_count, }; console.log(sendData); + + // 오프라인 유저들에게 알람 + const offlineUsers = await this.chatService.getChannelOfflineUsers( + data.channelId + ); + + if (offlineUsers.length) { + const message = '새로운 메세지가 있습니다.'; + + offlineUsers.forEach(async id => { + const createdNotification = + await this.notificationService.createNotification( + id, + userId, + 'groupChat', + message + ); + + const notificationData = { + notificationId: createdNotification.notificationId, // 포함된 notificationId + type: 'groupChat', + message, + senderNickname: user.nickname, + senderProfileUrl: user.profileUrl, + }; + + this.notificationService.sendRealTimeNotification(id, notificationData); + }); + } + this.server.to(data.channelId.toString()).emit('message', sendData); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index 8b28c88..54cf5ca 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -630,4 +630,21 @@ export class ChatService { return data; } + + async getChannelOfflineUsers(channelId: number) { + const userData = await this.prisma.channel_users.findMany({ + where: { channel_id: channelId }, + select: { user_id: true }, + }); + const userIds = userData.map(user => user.user_id); + + const onlineData = await this.prisma.online_users.findMany({ + select: { user_id: true }, + }); + const onlineIds = onlineData.map(user => user.user_id); + + const result = userIds.filter(id => !onlineIds.includes(id)); + + return result; + } }