44,000원
다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
socket io 미 연결 문제 (nest & flutter)
안녕하세요! socket관련한 서비스를 진행해보고 싶어서 제로초님의 강의를 들은 수강생입니다.현재 nest & flutter를 이용하고 있는데, flutter에서의 연결 및 다른 tool에서 socket io 연결이 되지 않으며 “외부 사이트로는 접근이 불가능한 오류”가 생겨서 조심스럽게 여쭤봅니다. 현재 로직은 채팅을 생성시, 채팅을 보여주는 리스트가 실시간으로 새로고침이 되는 부분을 작업중입니다.하지만, postman, httpie, hoppscotch의 부분에서 연결이 되지 않는 문제가 발생합니다.많은 방법을 찾아봤지만, 터미널에서 socket io cli를 통해서 로그는 볼 수 있지만, 다른 tool에서는 이용이 불가능한 방법에 대해서 알고 싶어서 질문드립니다!Socket io를 통해서 local, dev서버 연결 완료 하지만 postman의 socket io기능을 통해서 테스트를 진행하려고 할 때,postman으로 연결local에서는 문제가 없이 연길이 되지만, dev서버에서는 이러한 에러가 발생합니다. 또한 flutter 앱에서 연결을 하려면 다음과 같은 에러가 발생합니다.오류 메시지 "WebSocketException: Connection to 'http://~~~~.com:81/socket.io/?EIO=4&transport=websocket#' was not upgraded to websocket"는 클라이언트가 WebSocket 연결을 시도하였으나, 서버가 해당 연결을 WebSocket 프로토콜로 업그레이드하지 않았다는 것을 의미합니다. 이는 여러 가지 원인에 의해 발생할 수 있습니다: upgrade가 되지 않았다고 나와서 ,ngnix의 socket 부분에서 upgrade부분도 잘 넣어줬는데, 오류가 해결되지 않아서... 고민 끝에 질문 올립니다.Ngnix 설정부터 2주정도 시간을 들였지만, 해결이 되지 않아서…여쭤봅니다.방화벽도 해제가 되어 있는데 연결이 안되고 있습니다..다음은 nest에서 작성한 코드 입니다![chat.gateway.ts]import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, ConnectedSocket, MessageBody, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @WebSocketGateway() export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; afterInit(server: Server) { console.log('WebSocket initialized'); } handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); // 수정: client 객체 직접 출력 대신 id 출력 } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('sendMessage') handleMessage( @ConnectedSocket() client: Socket, @MessageBody() data: { message: string } ): void { console.log(`Received message from ${client.id}: ${data.message}`); this.server.emit('newMessage', data); // 모든 클라이언트에게 메시지 전송 console.log(`Received message: ${data.message}`); } } [main.ts]import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { join } from 'path'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from './common/exceptions/http-exception.filter'; import { SuccessInterceptor } from './common/interceptors/success.interceptor'; import { IoAdapter } from '@nestjs/platform-socket.io'; import { CustomIoAdapter } from './adapters/custom-io.adapter'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); app.useWebSocketAdapter(new CustomIoAdapter(app)); const configService = app.get(ConfigService); const port = configService.get('server.port'); const mongoUrl = configService.get('DB.MONGO_URL'); console.log('MongoDB URL:', mongoUrl); app.enableCors({ origin: true, credentials: true, }); app.useStaticAssets(join(__dirname, '..', 'client'), { prefix: '/api/v1/client', }); app.useGlobalInterceptors(new SuccessInterceptor()); app.useGlobalFilters(new HttpExceptionFilter()); app.setGlobalPrefix('api/v1'); const swagger_options = new DocumentBuilder() .setTitle('Nyam-Docs') .setDescription('API description') .setVersion('2.0.1') .addApiKey( { type: 'apiKey', name: 'x-token', in: 'header', description: 'Enter token', }, 'x-token', ) .addApiKey( { type: 'apiKey', name: 'x-type', in: 'header', description: 'Enter type', }, 'x-type', ) .build(); const document = SwaggerModule.createDocument(app, swagger_options); SwaggerModule.setup('api-docs', app, document); await app.listen(port, '0.0.0.0'); console.log(`Application Listening on Port : ${port}`); } bootstrap();다음은 custom한 io입니다[custom.io.adpter.ts]import { IoAdapter } from '@nestjs/platform-socket.io'; import { INestApplication, Injectable } from '@nestjs/common'; import { ServerOptions } from 'socket.io'; @Injectable() export class CustomIoAdapter extends IoAdapter { constructor(app: INestApplication) { super(app); } createIOServer(port: number, options?: ServerOptions): any { const serverOptions: ServerOptions = { ...options, cors: { origin: '*', // 모든 도메인에서 접근 허용 methods: ['GET', 'POST', 'PUT', 'DELETE'], credentials: true }, transports: ['websocket', 'polling'], //pooling 없으면 연결 안 됨(socket) allowEIO3: true // Engine.IO 3.x 버전 클라이언트 허용 }; return super.createIOServer(port, serverOptions); } } 혹시 해결방법을 아시거나, 도움을 주실만한 정보가 있으시다면 알려주시면 정말 감사하겠습니다!
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
no elements in sequence 에러 관해서 질문이 있습니다.
안녕하세요. nestjs를 사용해서 백엔드를 구성하고 있는데 RxJS의 no elements in sequence라는 에러에 대해서 질문이 있습니다.const findObject = await this.respository.findOne({ where: { data } }) if (findObject) { throw new HttpException("중복 접수 되었습니다.", HttpStatus.CONFLICT); }DB에서 찾은 객체가 있을 경우, Exception으로 에러를 내면서 종료를 시키는 로직을 생각하고 작성을 했습니다. 그런데 정작 오류는 no elements in sequence 와 함께 500 응답코드가 반환되는데요.반환 할 값이 없을경우 나는 에러로 알고있는데, throw만 하고 리턴하는 값이 없어서 발생 하는 것 같습니다.그리고 발생하는 두 오류 중, no elements in sequence가 먼저 반환되어 종료되기때문에 409에러가 묻히는 것으로 추측을 하고있는데 409에러를 먼저 내는 방법은 없을까요?
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
NextJS와 NestJS 소켓IO 연결
안녕하세요 제로초님! 강의 잘 듣고 있습니다.제가 현재 프론트엔드를 NextJS 14.xx(app router)로 작성하고 있고, 백엔드는 제로초님 강의 따라서 NestJS로 만들고 있는데 현재 연결은 로컬에서 HTTP로 하고 있습니다. 그런데 소켓을 연결하는 과정에서 계속 'WebSocket connection to 'ws://localhost:8080/socket.io/?EIO=4&transport=websocket' failed: WebSocket is closed before the connection is established.' 위와 같은 에러가 뜹니다. 이틀간 계속 검색해봤는데 도대체 왜 연결이 안되는지 모르겠습니다... ㅠㅠ 프론트는 현재 아래와 같이 되어있구요.localhost:3000으로 돌리고 있습니다. "socket.io-client": "^4.7.4",import io from 'socket.io-client'; useEffect(() => { const options = { auth: `${getCookie('accessToken')}`, extraHeaders: { id: chatRoomId }, }; const socket = io(`http://localhost:8080/chats`, options); console.log(socket); socket.on('connect', () => { console.log('WebSocket connected.'); }); return () => { socket.off(); }; }, []); 백엔드는 아래와 같이 되어있습니다. import { Socket } from 'socket.io'; import { ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, } from '@nestjs/websockets'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Message } from './entity/message.entity'; import { Logger } from '@nestjs/common'; @WebSocketGateway({ namespace: 'chats', cors: true, }) export class ChatsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { private logger = new Logger('CHAT-SERVICE'); constructor( @InjectRepository(Message) private readonly messageRepository: Repository<Message>, ) { this.logger.log('constructor'); } afterInit() { this.logger.log('init'); } async handleConnection(@ConnectedSocket() socket: Socket) { this.logger.log(`connected: ${socket.id} ${socket.nsp.name}`); } async handleDisconnect(@ConnectedSocket() socket: Socket) { this.logger.log(`disconnected: ${socket.id} ${socket.nsp.name}`); } @SubscribeMessage('send-message') async handleSubmitChat( @MessageBody() message: string, @ConnectedSocket() socket: Socket, ) {} } chat-service의 main.ts는 아래와 같습니다. import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const port = 8080; await app.listen(port); console.log(`CHAT-SERVICE listening on ${port}!`); } bootstrap(); app.module.ts는 아래와 같습니다. import { Module } from '@nestjs/common'; import { ChatModule } from './chat/chat.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import jwtConfig from './config/jwt.config'; import mysqlConfig from './config/mysql.config'; import sentryConfig from './config/sentry.config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [jwtConfig, mysqlConfig, sentryConfig], }), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: async (configService: ConfigService) => { let object: TypeOrmModuleOptions = { type: 'mysql', host: configService.get('mysql.host'), port: configService.get('mysql.port'), database: configService.get('mysql.database'), username: configService.get('mysql.username'), password: configService.get('mysql.password'), autoLoadEntities: true, synchronize: true, }; if (configService.get('STAGE') === 'LOCAL') { object = Object.assign(object, { logging: true, }); } return object; }, }), ChatModule, ], controllers: [], providers: [], }) export class AppModule {} 소켓 객체는 프론트에서 찍어보면 다음과 같이 나옵니다.@WebSocketGateway({ namespace: 'chats', cors: true, }) 에서 cors를 http://localhost:3000으로 설정하고 main에서도 다 열었을때도 안되는걸로 보아 cors 문제는 아닌듯합니다.
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
start:dev-backup으로 돌리면 핫 리로딩이 되요 정상인가요?
"start:dev-backup": "nest start --watch", "start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js",npm run start:dev로 실행 하였을떈 console.log을 바꾸면 작동하지 않는데 start:dev-backup으로 실행할땐 랏 리로딩이 됍니다. 정상인가요?`webpack-hmr.config.js`이 a-nest 파일 안에 있는게 맞겠죠? 영상에선 구분이 잘 가지 않아서const nodeExternals = require('webpack-node-externals'); const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); module.exports = function (options, webpack) { return { ...options, entry: ['webpack/hot/poll?100', options.entry], externals: [ nodeExternals({ allowlist: ['webpack/hot/poll?100'], }), ], plugins: [ ...options.plugins, new webpack.HotModuleReplacementPlugin(), new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/], }), new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }), ], }; };처음엔 실행이 안되길래 공식 문서에서 npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack으로 설치 해주었습니다.
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
getUser 관련
안녕하세요 제로초님! 다음과 같이 로그인후 바로 me를 호출하도록했는데 undefined이 찍혀서 왜 유저값이 들어있지 않은지 잘 모르겠습니다. axios에서 withCredential tue 넣었고 쿠키에 세션값이 저장되고 보낼때 header에 들어간것까지 확인했습니다. 제가 놓친 부분이나 누락한 부분이 있을까요? deserializeUser가 실행이 안되는거 같은데 이유를 모르겠네요
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
회원가입시에 로그인
안녕하세요 제로초님!passport 를 nest에 붙이는 법에 대해 이해하기 쉽게 알려주셔서 감사합니다! 하나 질문이 있는데요! 보통 서비스에서는 회원가입을 완료하면 cookie에 session정보(유저정보)를 넣어줘서 로그인후와 같은 경험을 하도록하는데요. 혹시 그 부분은 어떻게 구현해야할까요?passport에서 session정보를 cookie에 담아 보내는 로직을 모두 포함하다보니 방법이 잘 생각나지 않네요 ㅠ
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
ChunkLoadError: Loading chunk 552 failed.
안녕하세요. 저는 nextjs와 nestjs를 사용하고 있습니다. nextjs는 13버전을 사용하고 있는데요.코드를 원하는대로 다 작성해서 배포를 해봤는데, 개발 환경과 프로덕션 환경이 달라서 그런지 에러가 많아서 고쳐보고 있는 중입니다.그런 와중에 버튼을 클릭했을때 링크된 곳으로 사용자를 보내주는 역할을 하는 부분에서 ChunkLoadError: Loading chunk 552 failed. 이런 에러 문구가 콘솔에 뜨면서 화면이 렌더링 되지 않고 있습니다. <Link href="http://<host 서버 주소>/counsel" className="nav-link"> 상담 접수하러가기 </Link>빌드한 결과물에 이상이 있나 싶어서, dist폴더도 삭제했다가 다시 빌드해봤는데 같은 증상이 계속 나옵니다 ㅠㅠ nginx 설정에 문제가 있는걸까요? nextjs를 사용하고 있어서 따로 nginx 설정파일에 정적파일을 명시해두지는 않고 proxypass로 서버 주소를 넣어놓기만 했습니다. 구글링 해서 조치해봐도 딱히 해결책이 나오지 않아서 여쭤봅니다 ㅜㅜ
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
ValidationPipe DTO 변환 대신 JS Object 로 변환되는 이유
DTO 변환이 잘 되다가 다른 테스트 케이스 작성중에 아래 처럼 SignInUserRequest DTO 로 변환이 안되는 경우를 겪었습니다. ValidationPipe 옵션 transform: true 임에도 불구하고, 이런 현상이 일어나는 이유를 알 수 있을까요?아래는 잘되는 경우입니다.읽어주셔서 감사합니다.
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
Exception 포스트맨으로 확인 시 undefined 에러
에러[Nest] 48206 - 01/24/2024, 4:13:26 PM ERROR [ExceptionsHandler] You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable. at Object.createInvalidObservableTypeError (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/util/throwUnobservableError.js:5:12) at Object.innerFrom (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/observable/innerFrom.js:93:36) at doInnerSub (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/operators/mergeInternals.js:22:21) at outerNext (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/operators/mergeInternals.js:17:70) at OperatorSubscriber._this._next (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/operators/OperatorSubscriber.js:33:21) at Subscriber.next (/Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/Subscriber.js:51:18) at /Users/kwonyeji/Documents/nestStudy/a-nest/node_modules/rxjs/dist/cjs/internal/observable/innerFrom.js:120:28 at process.processTicksAndRejections (node:internal/process/task_queues:95:5)2.의심 코드<main.ts>import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { HttpExceptionFilter } from './httpException.filter'; declare const module: any; async function bootstrap() { const app = await NestFactory.create(AppModule); const port = process.env.PORT || 3000; app.useGlobalFilters(new HttpExceptionFilter()); const config = new DocumentBuilder() .setTitle('Sleact API') .setDescription('Sleact 개발을 위한 API 문서입니다.') .setVersion('1.0') .addTag('connect.sid') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); await app.listen(port); console.log(`listening on port ${port}`) if (module.hot) { module.hot.accept(); module.hot.dispose(() => app.close()); } } bootstrap(); 에러상황회원가입 구현 후 에러 확인 하려고 포스트맨 으로 요청 시 200성공이 떨어지는게 아니라 해당 에러가 발생합니다. 값을 모두 넣어도 같은 에러가 발생하고 console 찍었을떄 아예 controller에 접근하지도 못했습니다. 설정이 잘못된 건지 잘 모르겠습니다 ㅠ
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
class interface 질문
안녕하세요 제로초님 다른 비슷한 질문에서 다음과 같은 답변을 하셨어요 런타임에 있어서 런타임에도 타입체크를 수행하길 원하면 class로 선언하면 되고, 런타임에는 없길 원하면 interface를 쓰시면 됩니다. 런타임이 실제 코드가 실행될때라고 이해하고 있는데 런타임에 타입체크등 코드가 남아있으면 좋은점이 와닿지 않아서요!실제로 어떤 경우에 런타임에 코드가 남아있서서 좋은지, 타입체크가 되면 좋은지 여쭤봐도 될까요?
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
빌드 후 /dist 디렉토리가 강사님과 다른데 괜찮을까요
npm run build 후npm run start:prod 했더니 main.js를 찾을 수 없다는 에러 메세지가 나와 디렉토리를 확인해보니 아래와 같았습니다.그리고 main.js는 /dist/src 아래에 있었고요. 일단 packakge.json에서 start:prod의 실행 경로를 'dist/src/main.js'로 변경했고,npm run start:prod해서 서비스 가동되는것 확인했습니다. vsc 종료해도 로컬 호스트로 백엔드 서버 접근과 api 사용까지 이상 없이 되는 것 확인했으니 이대로 aws에 올리고 루트 디렉토리에 /dist 폴더, .env, .gitignore, package-lock.json, package.json만 업로드 해주면 되는걸까요?
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
Exception Filter편에서 POST할 때 EntityMetadataNotFoundError에러가 납니다.
// users.module.ts import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Users } from '../entities/Users'; @Module({ imports: [TypeOrmModule.forFeature([Users])], providers: [UsersService], controllers: [UsersController], }) export class UsersModule {}// app.module.ts import { MiddlewareConsumer, Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { LoggerMiddleware } from './middlewares/logger.middleware'; import { UsersModule } from './users/users.module'; import { WorkspacesModule } from './workspaces/workspaces.module'; import { ChannelsModule } from './channels/channels.module'; import { DmsModule } from './dms/dms.module'; import { UsersService } from './users/users.service'; import * as process from 'process'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Users } from './entities/Users'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), UsersModule, WorkspacesModule, ChannelsModule, DmsModule, TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: process.env.DB_USER, password: process.env.DB_PW, database: process.env.DB_NAME, entities: ['./entities/*.{js,ts}'], synchronize: false, logging: true, keepConnectionAlive: true, migrations: [__dirname + '/src/migrations/*.ts'], }), TypeOrmModule.forFeature([Users]), ], controllers: [AppController], providers: [AppService, UsersService], }) export class AppModule { configure(consumer: MiddlewareConsumer): void { consumer.apply(LoggerMiddleware).forRoutes('*'); } }// users.controller.ts import { Body, Controller, Post, Get, Req, Res, UseInterceptors, } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { JoinRequestDto } from './dto/join.request.dto'; import { UsersService } from './users.service'; import { UserDto } from '../common/dto/user.dto'; import { User } from '../common/decorators/user.decorator'; import { UndefinedToNullInterceptor } from '../common/interceptors/undefinedToNull.interceptor'; @UseInterceptors(UndefinedToNullInterceptor) @ApiTags('USER') @Controller('api/users') export class UsersController { constructor(private usersService: UsersService) {} //..... @ApiOperation({ summary: '회원가입' }) @Post() async join(@Body() body: JoinRequestDto) { await this.usersService.join(body.email, body.nickname, body.password); }import { HttpException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Users } from '../entities/Users'; import bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor() {} @InjectRepository(Users) private usersRepository: Repository<Users>; getUser() {} async join(email: string, nickname: string, password: string) { const user = this.usersRepository.findOne({ where: { email } }); if (!email) { throw new HttpException('이메일이 없습니다.', 400); } if (!nickname) { throw new HttpException('닉네임이 없습니다.', 400); } if (!password) { throw new HttpException('비밀번호가 없습니다.', 400); } if (user) { throw new HttpException('이미 존재하는 사용자입니다.', 400); } const hashedPassword = await bcrypt.hash(password, 12); await this.usersRepository.save({ email, nickname, password: hashedPassword, }); } }// a-nest/src/entities/Users.ts import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn, } from "typeorm"; import { Channelchats } from "./Channelchats"; import { Channelmembers } from "./Channelmembers"; import { Dms } from "./Dms"; import { Mentions } from "./Mentions"; import { Workspacemembers } from "./Workspacemembers"; import { Workspaces } from "./Workspaces"; @Index("email", ["email"], { unique: true }) @Entity("users", { schema: "sleact" }) export class Users { @PrimaryGeneratedColumn({ type: "int", name: "id" }) id: number; @Column("varchar", { name: "email", unique: true, length: 30 }) email: string; @Column("varchar", { name: "nickname", length: 30 }) nickname: string; @Column("varchar", { name: "password", length: 100 }) password: string; @Column("datetime", { name: "createdAt" }) createdAt: Date; @Column("datetime", { name: "updatedAt" }) updatedAt: Date; @Column("datetime", { name: "deletedAt", nullable: true }) deletedAt: Date | null; @OneToMany(() => Channelchats, (channelchats) => channelchats.user) channelchats: Channelchats[]; @OneToMany(() => Channelmembers, (channelmembers) => channelmembers.user) channelmembers: Channelmembers[]; @OneToMany(() => Dms, (dms) => dms.sender) dms: Dms[]; @OneToMany(() => Dms, (dms) => dms.receiver) dms2: Dms[]; @OneToMany(() => Mentions, (mentions) => mentions.sender) mentions: Mentions[]; @OneToMany(() => Mentions, (mentions) => mentions.receiver) mentions2: Mentions[]; @OneToMany( () => Workspacemembers, (workspacemembers) => workspacemembers.user ) workspacemembers: Workspacemembers[]; @OneToMany(() => Workspaces, (workspaces) => workspaces.owner) workspaces: Workspaces[]; } 서버 실행 후 http://localhost:3002/api/users URL로 빈 값을 POST로 보내면 콘솔창에 400 이메일이 없습니다. 라는 문구가 뜬 이후에C:\Users\xxx\Documents\Project\NestProject\a-nest\src\data-source\DataSource.ts:448 if (!metadata) throw new EntityMetadataNotFoundError(target) ^ EntityMetadataNotFoundError: No metadata for "Users" was found. at DataSource.getMetadata (C:\Users\xxx\Documents\Project\NestProject\a-nest\src\data-source\DataSource.ts:448:30) at Repository.get metadata [as metadata] (C:\Users\xxx\Documents\Project\NestProject\a-nest\src\repository\Repository.ts:53:40) at Repository.findOne (C:\Users\xxx\Documents\Project\NestProject\a-nest\src\repository\Repository.ts:597:42) at UsersService.join (C:\Users\xxx\Documents\Project\NestProject\a-nest\src\users\users.service.ts:13:39) at UsersController.join (C:\Users\xxx\Documents\Project\NestProject\a-nest\src\users\users.controller.ts:37:29) at C:\Users\xxx\Documents\Project\NestProject\a-nest\node_modules\@nestjs\core\router\router-execution-context.js:38:29 at processTicksAndRejections (node:internal/process/task_queues:95:5)위와 같이 EntityMetadataNotFoundError에러가 발생합니다. DB 커넥션 부분도 확인해보고, entities 부분이 문제인가 싶어 아래와 같이 변경도 해보았지만TypeOrmModule.forRoot({ entities: ['./entities/*.{js,ts}'], ],해결되지 않아 질문드립니다.추가적으로 \data-source\DataSource.ts 부분과\repository\Repository.ts부분은 실제 디렉토리에 없는 것들인데 왜 뜨는지도 모르겠습니다.해결 방법이 있을까요?
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
하위 테이블이 되는 엔티티에서 외래키 컬럼을 별도로 작성하는 이유가 궁금합니다
강의에서 OneToMany, ManyToOne 관계인 엔티티들을 보면/entity/Users.ts// ... @Entity({ schema: 'sleact', name: 'users' }) export class Users { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) id: number; // ... @OneToMany( () => WorkspaceMembers, (workspacemembers) => workspacemembers.User, ) WorkspaceMembers: WorkspaceMembers[]; // ... }/entity/WorkspaceMembers.ts@Entity('workspacemembers', { schema: 'sleact' }) export class WorkspaceMembers { // ... @Column('int', { primary: true, name: 'UserId' }) UserId: number; // ... @ManyToOne(() => Users, (users) => users.WorkspaceMembers, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) @JoinColumn([{ name: 'UserId', referencedColumnName: 'id' }]) User: Users; // ... }WorkspaceMembers 엔티티에서 UserId 컬럼을 별도로 작성하신 뒤@JoinColumn 데코레이터에서 Users 엔티티에서 가져온 'id'컬럼을 본 엔티티에서 'UserId' 컬럼으로 사용하겠다고 지정하셨는데요 이외에도 다른 일대다, 다대일 관계인 엔티티 중 다대일 파일들에 모두 '(상위테이블)Id' 이런 식으로 컬럼을 직접 설정하셨더라구요.이 이유가 무엇인지에 대해서 질문드리고 싶어서 글 작성합니다. 개인적으로 연습하면서 TypeORM 공식문서나 깃북을 확인했을때는 관계에 대해서는 지정을 하되,하위 테이블에선 별도로 외래키에 대한 컬럼까지는 작성하지 않았는데요.제가 작업중인 환경에서 테스트할 때도user.entity.ts// ... @Entity({ name: 'Users' }) export class Users { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) id: number; // .. @OneToMany(() => Posts, (post: Posts) => post.user, { cascade: true, }) post: Posts[]; }posts.entity.ts// ... @Entity({ name: 'Posts' }) export class Posts { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) id: number; // @Column({ type: 'int' }) // userId: number; // ... @ManyToOne(() => Users, (user: Users) => user.post, { onDelete: 'CASCADE', }) // 외래키 정보 // @JoinColumn([ // { // referencedColumnName: 'id', // 상대방 컬럼 // name: 'userId', // 여기서 쓸 컬럼 // }, // ]) user: Users[]; }이 상태에서 DB와 테이블을 생성했을때 외래키가 되는 'userId'가 생성되는 것 확인하였습니다.단 이 상태에서는 서비스 단에서 리포지터리로 데이터를 입력할 수가 없었습니다. 이 부분은 JoinColumn 데코레이터가 있어도 결과는 같았습니다. 혹시 자동적으로 만들어지는 외래키 컬럼에는 서비스에서 typeorm 사용해도직접적으로 데이터를 집어 넣지 못하기 때문에,외래키 컬럼을 직접 만들어주고 JoinColumn 데코레이터 사용해서 명시적으로 지정을 해줘야만 한다 라고 생각하면 될까요? 제가 제대로 이해한 것이 맞을까요?
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
AWS-RDS 사용중인데 createdAt 시간이 실제 시간과 맞지 않습니다.
왼쪽은 엣지 inprivate, 오른쪽은 크롬 시크릿 모드 브라우저입니다.서로의 채팅 입력 시간이 9시간 전으로 표시되고 있습니다. 실제로 서버의 createdAt은 한국 표준시라고는 되어있지만 실제 시간보다 9시간 느린 시간이 저장되었고 entities/channelChats.ts의 설정 내용은 강사님의 깃헙 내용과 동일했습니다. aws - rds에서 확인해본 DB도지역이 ap-northeast-2a, 서울로 잘 지정되어 있었습니다. 혹시 작업 중에 이런 경우 보신 적 있으신가요?어디를 더 살펴봐야 서버에 제대로 된 현재 시간이 저장될 수 있을지 조언 부탁드립니다.
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
마이크로 서비스를 사용하려고 하는데 질문이 있습니다.
tcp통신을 이용해서 마이크로 서비스를 만들고 있습니다.마이크로 서비스는 3000번으로 포트를 켜놨고,게이트웨이의 포트는 4000번으로 실행을 하게끔 해놨습니다.제가 우선 테스트를 위해서 user의 레포지토리에서 데이터를 전체 긁어오게끔 간단한 로직을 짜놨는데요. // 게이트웨이 컨트롤러 @Get("/users") @UseGuards() async GetUsers() { return this.organizationService.getUsers(); } // 게이트웨이 서비스 getUsers() { return this.clients.send("get:getUsers", {}); } // 마이크로 서비스 @MessagePattern({ cmd: "get:getUsers" }) async getUsers() { console.log("걸림"); return pipe( await this.userRepository.find({ relations: [ /* 관계 설정 */], }), (users) => ({ success: true, users: { users: users.map((user) => ( // 유저 정보들 ), }, }), ); }localhost:3000/api/users 경로로 요청 결과는 500에러가 나오고 콘솔에는[Nest] 3096 - 2023. 11. 16. 오후 2:45:20 ERROR [ExceptionsHandler] There is no matching message handler defined in the remote service.이렇게 메시지 핸들러가 일치하지 않다는데 대체 뭐가 잘못된건지를 모르겠습니다.메시지 패턴 이름도 같게 해놨는데 말이죠.
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
passport 관련해서 여쭙니다.
프론트쪽에서 워크스페이스 유저 초대를 하면서버쪽에서는 해당 기능을 요청한 유저가 워크스페이스 오너인지 체크하려고 합니다..AuthGuard를 확장한 OwnerAuthGuard에서 워크스페이스 레포지터리를 임포트해 db조회해서요청자가 owner인지 아닌 지 체크하려고 하는 데 passport의 패턴에 어긋나거나 성능상의 문제가 있을까요?공식문서나 구글링을 해보면 전부 jwt나 아이디, 비밀번호 체크하는 것만 있어서 확실치 않아 질문 드립니다.@Injectable() export class OwnerAuthGuard extends AuthGuard('owner') { async canActivate(context: ExecutionContext): Promise<boolean> { const can = await super.canActivate(context); console.log('LocalAuthGuard can:' + can); if (can) { const request = context.switchToHttp().getRequest(); //여기서 세션에 저장된 id로 workspace테이블을 조회 } return true; } }
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
.env 파일 추가 후 빈 화면이 뜹니다.
ConfigModule 사용하기(dotenv 진화판) 부분에서 4분 쯤에 .env 추가하셔서 설정하시는 부분에서 막혔습니다. @nestjs/config모듈은 설치된 상태입니다.import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ConfigModule.forRoot()], controllers: [AppController], providers: [AppService], }) export class AppModule {} app.controller.tsimport { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello() { return this.appService.getHello(); } } .envSECRET=안녕하세요packge.json{ "name": "a-nest", "version": "0.0.1", "description": "", "author": "", "private": true, "license": "UNLICENSED", "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev-backup": "nest start --watch", "start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "dotenv": "^16.3.1", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", "run-script-webpack-plugin": "^0.2.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3", "webpack": "^5.89.0", "webpack-node-externals": "^3.0.0" }, "jest": { "moduleFileExtensions": [ "js", "json", "ts" ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", "testEnvironment": "node" } } 코드 작성 후 서버를 재실행 시켰고, 빈 하얀 화면이 출력되는 상태입니다. 제가 놓친 부분이 있을까요?
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
절대경로 사용시 문제점 질문
직접 생성한 모듈 파일을 불러올 때 절대 경로를 사용한다면 node_modules에서 찾기 때문에 유효하지 않다고 했는데, 이게 제스트로 실행할 때만 문제가 되고, 노드로 그냥 실행시킬 때에는 문제가 되지 않는 이유는 뭔가요? 노드로 그냥 실행시킬 때에도 node_modules에서 해당 모듈 파일을 찾아서 에러가 발생해야 하는 것이 아닌가요?
- 미해결Slack 클론 코딩[백엔드 with NestJS + TypeORM]
유저 컨트롤러 코드 질문
깃허브 코드를 보면 @ApiOperation({ summary: '회원가입' }) @UseGuards(NotLoggedInGuard) @Post() async join(@Body() data: JoinRequestDto) { const user = this.usersService.findByEmail(data.email); if (!user) { throw new NotFoundException(); } const result = await this.usersService.join( data.email, data.nickname, data.password, ); if (result) { return 'ok'; } else { throw new ForbiddenException(); } } 이런데 여기서 user가 없는데 왜 NotFoundException()을 날리는 건가요? user가 없으면 그대로 join하여 사용자 등록을 해야 하는 것이 아닌가요?
- 해결됨Slack 클론 코딩[백엔드 with NestJS + TypeORM]
package.json / "scripts" / schema:drop, schma:sync에 `-d ./dataSource.ts` 옵션을 넣지 않으신 이유가 있으신가요?
테이블 삭제도 시도해보고 싶어서 터미널에$ npm run schema:drop 을 입력하였더니 아래와 같은 내용이 나왔습니다.Drops all tables in the database on your default dataSource. To drop table of a concrete connection's database use -c option. 옵션: -h, --help 도움말 표시 [불리언] -d, --dataSource Path to the file where your DataSource instance is defined. [필수] -v, --version 버전 표시 [불리언] 필수 인수가 주어지지 않았습니다: dataSource dataSource가 주어지지 않았다고 해서 살펴보다package.json/scripts/schema에 다른 명령어들과는 다르게 경로가 지정되어 있지 않는것 같아아래와 같이 수정했고, { "scripts": { "seed": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d ./dataSource.ts", "schema:drop": "ts-node ./node_modules/typeorm/cli.js schema:drop -d ./dataSource.ts", "schema:sync": "ts-node ./node_modules/typeorm/cli.js schema:sync", } } 테이블 삭제에 성공했습니다.$ npm run schema:drop > a-nest@0.0.1 schema:drop > ts-node ./node_modules/typeorm/cli.js schema:drop -d ./dataSource.ts query: SELECT VERSION() AS `version` query: SELECT * FROM `INFORMATION_SCHEMA`.`SCHEMATA` WHERE `SCHEMA_NAME` = '00_nestjs_typeorm' query: START TRANSACTION query: SELECT concat('DROP VIEW IF EXISTS `', table_schema, '`.`', table_name, '`') AS `query` FROM `INFORMATION_SCHEMA`.`VIEWS` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' query: SELECT concat('DROP TABLE IF EXISTS `', table_schema, '`.`', table_name, '`') AS `query` FROM ` `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channelchats` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channelmembers` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channels` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`dms` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`mentions` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`migrations` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`users` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`workspacemembers` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`workspaces` query: SET FOREIGN_KEY_CHECKS = 1; query: COMMIT Database schema has been successfully dropped. 혹시 schema:drop, schema:sync 명령어에 경로를 지정하지 않으신 이유가 있으신지,경로를 빼놓은 상태로 유지하다가, 정말 필요할 때에만 경로 지정하고 명령어를 사용하는게 좋은지가 궁금합니다. 실수로 입력해서 데이터 손실이 발생하는 것을 막기 위해서일까요?