Argument of type 'FindOptions<User>' is not assignable to parameter of type 'FindOneOptions<User>'. Types of property 'comment' are incompatible. Type 'unknown' is not assignable to type 'string | undefined'.

 

원인은 FindOptions<User>FindOneOptions<User>의 하위 타입이 아니기 때문에 발생했으며, FindOneOptions<User> 타입은 findOne() 메서드에 전달되는 옵션 객체의 타입이란다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOneOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}
//거의 다 똑같고 위의 import 셋째줄에서 FindOptions를 FindOneOptions로 바꿔주면 끝이다.

checkDuplicateEmail() 메서드 내에서 검색 조건으로 이메일 값을 사용하도록 수정한 것이다. FindOneOptions<User> 타입을 사용하여 검색 옵션 객체를 정의하고, 해당 객체를 findOne() 메서드에 전달하여 중복된 이메일을 확인한다.

 

그리고 위처럼 고치니,

Object literal may only specify known properties, and 'email' does not exist in type 'FindOptionsWhere<User> | FindOptionsWhere<User>[]'.

The expected type comes from property 'where' which is declared here on type 'FindOneOptions<User>

두 개의 오류가 동시에...ㅋㅋㅋ 차분하게 수정해보자.

 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOneOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}

?? 뭐가 다른지 모르겠다. 첫번째 코드랑 두번째 코드랑 다른게 없는데 1번째는 오류가 2개나 나고 2번째는 멀쩡하다 왜??

맞왜틀?? 오류의 원인은 FindOneOptions<User>where 속성에 정의되지 않은 속성인 'email'을 사용하려고 하기 때문에 발생합니다. 이 오류를 해결하기 위해서는 where 속성을 올바르게 정의해야 한다고 한다. 1번도 올바르게 정의 됐는데?? 진짜 이해 할 수가 없다..

 

5편에서 계속..

import { ApiProperty } from '@nestjs/swagger';
import { UserGender, UserType } from '../../../commons/enums';
import { SignUpCommand } from '../../../domains/ports/in/command/user.command';

export class SignUpRequest extends SignUpCommand {
  /**
   * 유저 가입 이메일
   * @example testUser@livey.kr
   */
  @ApiProperty()
  userEmail: string;

  /**
   * @example testPW
   */
  @ApiProperty()
  userPW: string;

  /**
   * @example tester
   */
  @ApiProperty()
  userName: string;

  /**
   *
   */
  userGender: UserGender;

  userType: UserType;

  /**
   * @example 010-1234-5678
   */
  @ApiProperty()
  userPhoneNum: string;

  
/**
* 이메일 유효성 검사 API 추가 예시 
*/
@IsEmail() // <- 여기에 이메일 유효성을 확인하는 데코레이터를 추가합니다.
@ApiProperty({
    example: '테스트용 이메일',
    description: '유저 가입 이메일',
})
userEmail:string;
  

  
/**
* 여기까지 예시입니다. 나머지 코드는 그대로 사용하시면 됩니다.
위의 코드에서 SignUpRequest 클래스의 userEmail 프로퍼티에
@IsEmail() 데코레이터를 추가하여 이메일의 유효성을 검사할 수 있습니다. 
해당 데코레이터는 NestJS에서 제공하는 class-validator 패키지의 일부입니다.

또한, @ApiProperty() 데코레이터를 사용하여 Swagger 문서화에 필요한 정보를 추가할 수 있습니다. 
예를 들어, example 속성을 사용하여 예시 값을 제공하고, description 속성을 사용하여 
프로퍼티에 대한 설명을 추가할 수 있습니다.

추가된 코드 부분은 주석으로 표시되어 있으며,
위와 같은 방식으로 기존 코드에 이메일 유효성 검사 API를 추가할 수 있습니다.
*/

}

생략된 내용이 굉장히 많은데, 아무래도 프로젝트 내용 상 철저히 보안이 유지되어야 하기 때문에 코드 공유가 어려워서, 예시 코드 내용 올린 점.. 양해 바란다.

아무튼 대충 위와 같이 유저 리퀘스트 DTO에 이메일 유효성 검사 API를 추가해주고, 실행하려는데..

 

Object literal may only specify known properties, and 'email' does not exist in type 'FindOptionsWhere<User> | FindOptionsWhere<User>[]'.

하.. 이번엔 또 뭐가..

알고 보니 FindOptionsWhere<User> 타입에 'email' 속성이 존재하지 않기 때문에 발생했다. 이 오류를 해결하기 위해서는 FindOptionsWhere<User> 타입에 'email' 속성을 추가해야 한다.

import { User } from '../../domains/entities/User.entity';
import { FindConditions } from 'typeorm';

// ...

const conditions: FindConditions<User> = {
  email: 'testUser@livey.kr',
};

// 사용 예시:
const user = await userRepository.findOne(conditions);

//위의 코드에서는 FindConditions<User>를 사용하여 검색 조건을 정의하고 있습니다.
해당 타입은 TypeORM에서 제공하는 타입으로, email과 같은 필드로 검색 조건을 지정할 수 있도록 합니다.

userRepository.findOne() 메서드를 호출할 때 검색 조건으로 conditions 객체를 전달하여 
사용할 수 있습니다. 이렇게 하면 원하는 이메일 값을 가진 유저를 찾을 수 있습니다.

프로젝트의 실제 구조와 TypeORM 설정에 따라 코드가 달라질 수 있으므로, 
필요한 경우 해당 프로젝트의 구조와 설정에 맞게 코드를 수정하셔야 합니다.

처음에 이렇게 했다가, 유저 레포지토리가 있길래 

import { User } from '../../domains/entities/User.entity';
import { FindConditions, Repository } from 'typeorm';

class UserRepository extends Repository<User> {
  // ...

  async findUserByEmail(email: string): Promise<User | undefined> {
    const conditions: FindConditions<User> = {
      email: email,
    };

    return this.findOne(conditions);
  }

  // ...
}
//위의 코드에서는 findUserByEmail() 메서드를 추가하여 이메일로 유저를 조회하고 있습니다.
해당 메서드 내에서 검색 조건으로 주어진 이메일 값을 사용하고 있습니다.

위와 같이 바꿔주었다.

 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}
//위의 코드는 checkDuplicateEmail() 메서드 내에서 
검색 조건으로 이메일 값을 사용하도록 수정한 것입니다. 
FindOptions<User> 타입을 사용하여 검색 조건 객체를 정의하고, 
해당 객체를 findOne() 메서드에 전달하여 중복된 이메일을 확인합니다.
**typeorm에서는 FindConditions 대신 FindOptions를 사용해야 합니다.
typeorm에서는 FindOptions를 사용하여 검색 조건을 정의합니다**

이렇게 바꿨는데, 또 오류가 발생했다... 나머지는 4편에서.

아마 한 6~7편까지 나올 듯 하다..ㅋㅋ

import { ApiProperty } from '@nestjs/swagger';
import { UserGender, UserType } from '../../../commons/enums';
import { SignUpCommand } from '../../../domains/ports/in/command/user.command';

export class SignUpRequest extends SignUpCommand {
  /**
   * 유저 가입 이메일
   * @example testUser@livey.kr
   */
  @ApiProperty()
  userEmail: string;

  /**
   * @example testPW
   */
  @ApiProperty()
  userPW: string;

  /**
   * @example tester
   */
  @ApiProperty()
  userName: string;

  /**
   *
   */
  userGender: UserGender;

  userType: UserType;

  /**
   * @example 010-1234-5678
   */
  @ApiProperty()
  userPhoneNum: string;

  
/**
* 이메일 유효성 검사 API 추가 예시 
*/
@IsEmail() // <- 여기에 이메일 유효성을 확인하는 데코레이터를 추가합니다.
@ApiProperty({
    example: '테스트용 이메일',
    description: '유저 가입 이메일',
})
userEmail:string;
  

  
/**
* 여기까지 예시입니다. 나머지 코드는 그대로 사용하시면 됩니다.
*/

}

대충 위와 같이 유저 리퀘스트 DTO에 이메일 유효성 검사 API를 추가했고..

 

개발을 할 때 간과한 것이 있었다. 상대 경로라는 것이다.

상대 경로는 현재 파일의 위치를 기준으로 연결하려는 파일의 상대적인 경로로 나타낸 것을 의미한다. 상대 경로는 주소나 프로젝트 디렉토리 위치가 바뀌어도 내부 구조만 그대로라면 수정없이 그대로 사용할 수 있다는 장점을 가지고 있다. 그러나 자기 자신이 기준이기 때문에 자기 자신의 위치가 바뀌는 것에 취약하다는 단점이 있다(이거 때문에 컨트롤러 통합할때 애먹었다.. ㅂㄷㅂㄷ)
상대 경로는 보통 다음과 같이 명시한 것들을 의미한다.

./src/compnents/Counter.js
../../img/logo.jpg

기호의 의미는,

/ root
./ 현재 위치
../ 상위 경로

/만 사용되면 root, 즉 가장 토대가 되는 경로가 선택된다.
./는 현재 위치를 나타낸다. 현재 위치 ./는 보통 생략.
../는 상위 경로를 나타낸다. 상위 경로는 현재 폴더가 속한 폴더를 가리킨다.

이걸 몰라서,

import { User } from './src/entities/User.entity';
import { User } from './entities/User.entity';
import { User } from '../entities/User.entity';
import { User } from '../../entities/User.entity';
import { User } from '../../domains/entities/User.entity';

장장 5번의 삽질을 거쳐서야 드디어 오류가 멈췄다.. 후 ㅂㄷㅂㄷ!!!

여기서 끝이 아니다... 삽질기는 계속된다 ㅋㅋ

본격적으로 포트포워딩해보자.

예를 들어보겠다.

ssh -L 3306:localhost:3306 사용자명@원격 서버(ip)

SSH 포트 포워딩을 설정하는 것을 의미한다. 

SSH 포트 포워딩은 로컬 컴퓨터와 원격 서버 간의 특정 포트를 전달하거나 중계하는 기능을 한다. 위의 명령어에서 -L 옵션은 로컬 포트 포워딩을 설정하라는 의미이며, 3306:localhost:3306은 로컬 컴퓨터의 3306번(MySQL) 포트와 원격 서버의 localhost(원격 서버 자체)의 3306번 포트를 연결한다는 의미이다.

이 경우, 로컬 컴퓨터의 3306번 포트에 접속하는 모든 요청은 SSH 연결을 통해 원격 서버로 전달되고, 원격 서버에서 다시 localhost(자기 자신)의 3306번 포트로 전달되며, 일반적으로 이러한 방식으로 SSH 포트 포워딩을 사용하여 데이터베이스 등의 서비스에 보안 연결을 설정하거나, 네트워크 상에서 접근이 제한된 리소스에 접근하기 위해 사용한다.

위 예시에서 사용자명@ 원격 서버(ip) 는 SSH 접속할 때 사용되는 호스트 주소와 계정 정보이다.

즉 위 명령어는 로컬 컴퓨터의 3306번 포트를 통해 SSH 연결을 수립하고, 해당 연결로부터 데이터베이스 등 원격 리소스에 접근할 수 있도록 해준다.

 

따라서, 개발 자체는 SSH 서버에 접속해서 이루어져야 하지만(테스트도 마찬가지, 애초에 SSH 서버에 접속하지 않으면 실행 자체가 되지 않는다...연결 시간 초과가 뜨거나 연결 실패가 뜬다. 그게 그거다..) MySQL과 같이 로컬 서버에 있는 경우 이를 포트포워딩해서 SSH 서버에서도 로컬 서버에 있는 DB 리소스에 접근할 수 있다고 보면 된다!

 

++ ssh [사용자명]@[호스트주소]는 SSH를 사용하여 원격 서버에 접속하기 위한 명령어이다.

실제로 사용할 때는 [사용자명]과 [호스트주소]를 실제 값으로 대체해야 하며, 여기서 [사용자명]은 원격 서버에 접속할 때 사용되는 계정 이름이고, [호스트주소]는 접속하려는 원격 서버의 IP 주소나 도메인 이름이다.

예를 들어, ssh myuser@example.com와 같이 명령어를 실행하면 myuser라는 계정으로 example.com 호스트에 SSH로 접속하게 된다. 이때 myuser와 example.com은 실제로 사용하는 계정 이름과 호스트 주소로 대체되어야 한다.

SSH 명령을 실행하기 전에 해당 호스트에 SSH 서비스가 구성되어 있고, 액세스 권한이 있는지 확인해야 하며, 기본적으로 SSH 포트인 22번을 사용하지만 다른 포트 번호가 설정되어 있다면 -p [포트번호] 옵션을 추가하여 명시해야 한다. 따라서 실제 환경에서는 [사용자명]@[호스트주소], 즉 유효한 계정과 호스트 주소를 사용하여 SSH 접속을 시도해야 한다. ++

 

개발 중 테스트를 위해 개발 중인 서버에 접속하려면?

개발 중인 서버의 홈페이지에 접속하려면 일반적으로 웹 브라우저의 주소창에 해당 서버의 IP 주소 또는 도메인 이름을 입력하면 된다. 예를 들어, 개발 중인 서버의 IP 주소가 123.456.789.123이라면, 웹 브라우저의 주소창에 http://123.456.789.123을 입력하여 접속할 수 있다.

만약 개발 중인 서버에 도메인 이름이 연결되어 있다면, 해당 도메인 이름을 사용하여 접속할 수도 있습니다. 예를 들어, 개발 중인 서버의 도메인 이름이 dev.example.com이라면, 웹 브라우저의 주소창에 http://dev.example.com을 입력하여 접속할 수 있다. 생각보다 매우 간단하다.

단, 실제로 접속 가능한지 확인하기 위해서는 해당 서버가 실행 중이고 네트워크 설정 및 방화벽 등이 올바르게 구성되어 있는지 확인해야 한다. 또한, 개발 중인 서비스가 HTTPS를 사용하는 경우 https:// 프로토콜을 사용하여 보안 연결로 접속해야 할 수도 있다. 따라서 실제 환경에서는 개발 중인 서버의 IP 주소나 도메인 이름을 확인하고 이를 바탕으로 적절한 URL을 입력하여 웹 페이지에 접속해야 한다.

 

+++

SSH 터널링 명령어는 터미널 또는 명령 프롬프트에서 실행해야 합니다. 아래는 각 운영체제별로 명령어를 실행하는 방법이다.(포트포워딩을 터널링이라고도 한다)

Windows:

  1. 시작 메뉴에서 "명령 프롬프트" 또는 "PowerShell"을 검색하여 실행합니다.
  2. 명령 프롬프트 또는 PowerShell 창이 열리면, 주어진 SSH 터널링 명령어를 입력합니다.
  3. Enter 키를 눌러 명령을 실행합니다.

macOS/Linux:

  1. 애플리케이션 폴더에서 "터미널"을 찾아 실행합니다.
  2. 터미널 창이 열리면, 주어진 SSH 터널링 명령어를 입력합니다.
  3. Enter 키를 눌러 명령을 실행합니다.

위 단계를 따라 하시면 SSH 터널링이 설정되고, 로컬 포트 3306가 원격 MariaDB에 연결된다.

SSH 터널링 설정 후에 DBeaver나 다른 MySQL/MariaDB 클라이언트 등에서 로컬호스트(localhost)의 3306 포트로 연결하면 원격 MariaDB에 접근할 수 있다.

주의: 실제 IP 주소와 사용자 이름은 제공된 정보에 따라 변경되어야 한다. 또한, 해당 계정의 비밀번호도 입력해야 한다.

https://jizard.tistory.com/339 이 블로그에 가면 자세히 나와있다!

'CS > 네트워크' 카테고리의 다른 글

면접 질문 요약(1)  (0) 2024.01.01
HTTP 상태코드  (0) 2024.01.01
HTTP&HTTPS  (0) 2024.01.01
HTTP 상태 코드(HTTP Status code) 정리표  (2) 2023.10.26
포트포워딩(1)  (0) 2023.10.22

프로젝트에 합류하고 나서, 포트포워딩의 개념을 잘 몰라 DB연결부터 애를 먹었던 기억이 있다.

들어본 적은 있는데, 정작 써먹어 본 적은 없다. 근데 내가 하고 있던 게 포트포워딩이었고(ssh 접속) 이게 잘 안되서 DB접속이 되지 않았던 것이다. 이참에 확실히 포트포워딩에 대해 이해하고 넘어가려 한다.

 

DATABASE_MARIADB_URL 환경 변수 설정을 보면, 다음과 같은 형식으로 MySQL 데이터베이스에 연결하기 위한 URL이 지정되어 있었는데, 이는 일반 ip로는 접속이 안된다. 실제 서비스를 목적으로 한 프로젝트이다 보니 보안이 매우 중요하기도 하고, 웬만해선 다들 이런식으로 개발하는 것 같다. 그래서 어찌되었건 ssh로 접속을 해야만 DB에 연결이 된다.

(학교 와이파이 등으로 접속하면 보안 문제상 안 되는 경우가 많은데, 우리 학교도 그런 줄 알았으나 접속이 잘 되는거 보면 우리 학교는 예외인가 보다.. 휴 다행)

 

포트 포워딩에 대해 알아보기 전에 포트의 개념에 대해 알아보자

하나의 서버가 다양한 역할을 수행할 때, 가령 웹사이트를 전달해주는 역할, 그리고 파일을 요청하는 역할이 있다고 한다면, 클라이언트가 서버에 요청을 보냈을 때 웹사이트 요청인지 파일 요청인지 구분할 수 있는 방법이 필요하게 됩니다. 이때 사용하는 것이 포트이다.

 

포트는 숫자로 표현하게 되어 있으며 65535번까지 존재하며 아래와 같이 3종류로 표현이 된다.

  • 0번 ~ 1023번: 잘 알려진 포트 (well-known port)
  • 1024번 ~ 49151번: 등록된 포트 (registered port)
  • 49152번 ~ 65535번: 동적 포트 (dynamic port)

잘 알려진 포트 번호의 대표적 예는 다음과 같습니다.

  • 번호 프로토콜 통신 프로토콜 설명
    80 HTTP TCP 웹 서버 접속
    443 HTTPS TCP 웹 서버 접속(SSL)
    110 POP3 TCP 메일 읽기
    25 SMTP TCP 메일 서버간 메일 전송
    22 SSH TCP 컴퓨터 원격 로그인
    53 DNS UDP DNS 질의
    123 NTP TCP 시간 동기화
    20 FTP TCP 데이터 전송
    21 FTP TCP FTP 제어

 

잘 알려진 포트 번호를 본다면 파일 요청은 일반적으로 21번 포트, 그리고 http요청은 80 포트를 이용하게 된다. 따라서 이제 서버는 21번 포트로 오는 요청은 파일 요청임으로 받아들이고 80 포트로 오는 요청은 웹사이트를 요청하는 것으로 이해하게 되어 여러 가지 역할을 할 수 있게 되는 것이다. 정리하자면 포트란 컴퓨터의 Lan선은 하나인데 통신을 필요로 하는 프로그램이 다수일 때 이 다수의 프로그램을 구별할 수 있는 번호가이다.

 

라고 한다. 특히 나의 경우 SSH(Secure Shell)로 포트포워딩해서 쓰기 때문에 22번이 익숙했다.

SSH로 포트포워딩(원격 서버 접속) 하기 위해서는

ssh [사용자명]@[호스트주소]

 

실제로 해당 호스트가 SSH 서버로 구성되어 있고, 액세스 권한이 있다면 위의 명령어를 사용하여 접속할 수 있다. 만약 액세스 권한이 없거나 정확한 계정 정보가 필요하다면 관리자 또는 시스템 운영팀과 상담하여 정확한 접속 정보와 계정 정보를 확인해야 한다.(개발에 참여하는 멤버들만이 접속할 수 있는 일련의 암호 같은 느낌이라 생각하면 쉽다.)

또한, SSH 포트 번호가 기본값인 22번이 아닌 다른 포트 번호로 설정되어 있다면 -p 옵션을 사용하여 포트 번호도 지정해주어야 합니다. 예를 들면 'ssh -p 2222 호스트주소' 와 같이 명령어를 실행한다.

참고: 실제 환경에서의 도메인 및 SSH 접속 방법은 시스템 구성 및 관리자 정책에 따라 다르므로, 상황에 맞게 정확한 정보를 확인하는 것이 중요하다.

 

그렇다면 포트 포워딩이란?

포트 포워딩이란 컴퓨터 네트워크에서 패킷이 라우터나 방화벽 같은 네트워크 게이트웨이를 통과하는 동안 네트워크 주소를 변환해주는 것을 의미합니다. 쉽게 말해 외부에서 접속이 가능하도록 하는 것입니다.

 

예를들어 보겠습니다. 공유기를 설치하게 되면 공유기와 연결된 PC들은 192.168~로 시작하는 IP를 공유기로부터 부여받게 됩니다. 그리고 공유기는 ISP 업체로부터 할당받은 IP를 가지게 됩니다.

 

공유기를 기점으로 공유기 뒤에 있는 PC들은 내부 IP라고 부르고, 공유기를 외부 IP라고 부르게 됩니다. 그런데 만약 다른 영역에 있는 PC에서 내부 IP에 있는 192.168.0.20 PC에 접속하고자 하는 요청이 들어왔을 때 공유기는 어느 PC로 연결을 해주어야 할지 모르는 상태가 됩니다.

 

이러한 상황에서 공유기에게 해당 포트로 요청이 오면 192.168.0.20 PC로 연결하라는 이정표를 달아주는 것을 포트 포워딩이라고 합니다.

출처: https://ooeunz.tistory.com/104

 

무슨 말인지 이해하기 어렵다면, 본디 외부에서 접속이 불가능한(관리자나 시스템 운영자 외) 네트워크 주소를, 관리자와 시스템 운영자의 허락을 받은 사람에 한해(접속할 수 있는 스크립트를 공유받고) 접속할 수 있도록 해준다고 생각하면 이해가 쉬울 듯 하다. 

 

다음부터는 포트포워딩을 구체적으로 어떻게 하는지에 대해 적어보겠다.

'CS > 네트워크' 카테고리의 다른 글

면접 질문 요약(1)  (0) 2024.01.01
HTTP 상태코드  (0) 2024.01.01
HTTP&HTTPS  (0) 2024.01.01
HTTP 상태 코드(HTTP Status code) 정리표  (2) 2023.10.26
포트포워딩(2)  (0) 2023.10.22

정말 말 많고 탈 많던 회고가 될 것으로 생각된다.

우선 처음에는 이메일 컨트롤러를 만들었다. 아래와 같이

import { Controller, Get, Query } from '@nestjs/common';
import { EmailService } from './email.service';

@Controller('email')
export class EmailController {
  constructor(private readonly emailService: EmailService) {}

  @Get('validate')
  async validateEmail(@Query('email') email: string): Promise<boolean> {
    const isValidFormat = this.emailService.validateEmailFormat(email);
    if (!isValidFormat) {
      return false;
    }

    const isDuplicate = await this.emailService.checkDuplicateEmail(email);
    if (isDuplicate) {
      return false;
    }

    return true;
  }
}
//위의 코드에서 validateEmail 메서드는 email 쿼리 파라미터로 전달받은 이메일 주소의 유효성을 검사합니다.
먼저 validateEmailFormat 메서드로 이메일 주소의 형식이 올바른지 확인하고, 
그 다음에 checkDuplicateEmail 메서드로 중복 여부를 확인합니다. 모든 검사가 통과되면 true를 반환하고, 
그렇지 않으면 false를 반환합니다.

 

다음으로, 해당 컨트롤러에서 사용할 서비스 클래스인 EmailService도 생성해주었다.

이 서비스 클래스에서 실제로 유효성 검사와 중복 여부 확인 로직을 구현한다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class EmailService {
  validateEmailFormat(email: string): boolean {
    // 여기에 이메일 형식 검증 로직을 구현하세요.
    // 예시: 정규식을 사용하여 형식이 맞는지 확인하는 방법
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    // 여기에 중복 여부 확인 로직을 구현하세요.
    // 예시: 데이터베이스에서 이미 등록된 이메일인지 조회하는 방법
    const existingUser = await User.findOne({ where: { email } });
    
     return !!existingUser; // 이미 등록된 경우 true 반환
   }
}

위의 코드에서는 각각 validateEmailFormat 메서드와 checkDuplicateEmail 메서드가 있다.

첫 번째 메서드(validateEmailFormat)는 정규식을 사용하여 입력된 이메일 주소가 올바른 형식인지 판별하고,

두 번째 메서드(checkDuplicateEmail)는 데이터베이스나 다른 저장소에서 이미 등록된 이메일인지 조회하여 중복 여부를 판별한다.

마지막으로, 해당 컨트롤러와 서비스 클래스를 Nest.js 앱 모듈에 등록해 주면 API 제작이 끝난다

import { Module } from '@nestjs/common';
import { EmailController } from './email.controller';
import { EmailService } from './email.service';

@Module({
  controllers: [EmailController],
  providers: [EmailService],
})
export class AppModule {}
// 대략 이런 식으로

그런데, 알고 보니 유저 컨트롤러에 이미 회원가입(당연히 이메일 관련 내용도 포함) 로직이 있던 것이다. 

이럴 경우, 이메일 컨트롤러를 만들 필요 없이 유저 컨트롤러에 추가적으로 로직을 구현하면 되는 일이었다. 서비스가 늘어날수록 컨트롤러도 다양해질 것인데, 그럴 경우를 미리 대비해서 조금이라도 컨트롤러의 개수를 줄이려고 노력해보았다. 

그래서 결국 이메일 컨트롤러를 지우고 유저 컨트롤러에 병합을 시도했다.

문제는 다음부터였다...ㅂㄷㅂㄷ

이걸 왜 그동안 몰랐을까? 사실 이걸 알고 있었다면 전작과 같이 start:local 하나 때문에 대대적인 패키지 스크립트 수정 따위 하지 않아도 되었다. 

 

경위는 이러하다. 내가 git clone 받아 온 브랜치는 main 브랜치이고, 이 브랜치는 정상적으로 작동이 되지 않는(...) 브랜치이다. 실제 개발 환경 테스트가 가능한 브랜치는 develope 브랜치라고 따로 존재하였다. 

애시당초 develope 브랜치에서 서버 가동을 했더라면 아까 스크립트 변경 삽질기는 없었을 것인데.... 뭐 그래도 하나 배워갔다고 생각하겠다. 그리고 그동안 git은 clone, add, commit, push, PR, merge 이런 것 밖에 안 해봐서 브랜치를 딱히 변경할 필요성을 느끼지 못했는데, 이번 기회에 새로 배웠다. 

 

내가 있는 현재 브랜치 (main) 에서 (develope) 브랜치로 이동하려면 먼저 git status 명령어로 현재 디렉토리의 상태를 확인한다.

다음으로 git checkout <branch-name> 명령어를 입력한다. 나의 경우는 <>란의 develope가 되겠다. 이러면 main -> develope로 브랜치가 변경되었다. 파일들도 상당히 달라졌고 무엇보다 서버 가동이 전혀 문제없이 잘 되었다

...가 아니라, 아까 삽질했던 것 때문에 파일의 내용들이 상당히 달라졌고 이를 위해서는 변경 사항을 commit 하거나 stash해야 한다는 것이다. commit할 이유가 전혀 없으니 git stash 명령어를 사용하여 현재 작업 트리의 변경 사항을 stash했다.

 

브랜치 변경도 이번이 처음이고 당연히 stash도 처음 사용해봤는데 이 stash가 브랜치를 이동할 때 숱하게 쓰인다고 한다. stash의 의미는 티스토리로 따지면 임시저장과 비슷한 기능을 수행한다. 글을 쓰다가 싫증나서 글 쓰기 페이지를 벗어나려고 할 때 그냥 쓰던 글을 날리거나, 임시저장을 해야 나갈 수 있는 것처럼, git은 날리는건(...) 안되고 커밋해서 변경사항을 확실하게 저장하거나, stash로 임시저장을 하고 나중에 git stash pop로 변경 사항을 복원하든 해야 한다. 

보통 NestJS는 서버 가동 시 npm (run) start나 npm run dev를 쓰는 게 일반적이라고 알고 있다.

그런데 우리 프로젝트는 특이하게도 npm run start:local이라는 스트립트로 서버를 가동한다. 

코드를 입력하고 가동을 하려는데? 

npm ERR! Missing script: "start:local"
npm ERR!
npm ERR! To see a list of scripts, run:
npm ERR!   npm run
npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\LG\AppData\Local\npm-cache_logs\2023-10-16T03_47_01_257Z-debug-0.log

npm ERR! Missing script: "start:local" 오류는 "start:local"이라는 스크립트가 package.json 파일에 정의되지 않았기 때문에 발생한다. 따라서  package.json 파일의 "scripts" 섹션 내에 "start:local" 스크립트를 추가해야 한다.

"scripts": {
    // 이전 스크립트들...
    // ...
    
    // 여기에 start:local 스크립트 추가
    // index.js 파일을 직접 실행합니다.
    
   	"start:local": "node index.js"
}

이런식으로 수정해주면 된다. 개발 환경에서 로컬 서버를 실행하기 위해 이렇게 실행하는 것이다.

그런데 문제는 이것 뿐이 아니었다. 

 

package.json 파일을 살펴보니 "dependencies" 섹션에서 "-" 패키지를 썼는데 이는 유효하지 않다. "g"패키지도 마찬가지. 또한, "devDependencies" 섹션에서 "@nestjs/cli", "@nestjs/schematics", 그리고 "@typescript-eslint/eslint-plugin"과 관련된 버전들은 최신 버전으로 업데이트되지 않았다. 마지막으로 제일 중요한 cross-env 패키지를 설치해야 하는데 하지 않았다. 이는 "start:local" 스크립트에서 사용되므로 꼭 필요하다.

{
  "name": ㅇㅇㅇㅇㅇ
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "MIT",
  "scripts": {
    // 스크립트들...
    // ...
    // 여기에 start:local 스크립트 추가
    // cross-env를 사용하여 NODE_ENV 환경 변수를 설정합니다.
   	"start:local": "cross-env NODE_ENV=local nest start --watch",
    
    // start:local-pm2, start:dev, start:prod 등의 스크립트도 그대로 두세요.
    
  },
  
  	// dependencies와 devDependencies는 여기에 있어야 합니다.
  
  	"dependencies": {
    	"@adminjs/express": "^5.0.1",
    	"@adminjs/nestjs": "^5.0.1",
    	"@adminjs/typeorm": "^4.0.",
        ... (다른 종속성들)
   },
   
   "devDependencies":{
       "@nestjs/cli":"^9.,8,.7,"
       "@nestjs/schematics":"^9.,8,.7,"
       ... (다른 개발 종속성들)
       
       // cross-env 추가
     	"cross-env":"^7.,0,.3"
   },
   
   // jest 설정 등 다른 부분은 그대로 두세요.
}

이렇게 변경해주고,

npm install -g cross-env

로 설치해 줘야 비로소 start:local 스크립트를 쓸 수 있게 된다.

[Nest] 36104 - 2023. 10. 16. 오후 12:44:21 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (5)... Error: connect ECONNREFUSED ::1:3306 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1300:16)

 

초장부터 이런 오류가... NestJS에서 데이터베이스에 연결 할 수 없고,  "ECONNREFUSED" 오류는 로컬 호스트(localhost)의 3306 포트(MySQL, MariaDB)에 대한 연결이 거부되었음을 의미한다. 처음엔 학교 와이파이에 연결한 상태에서 개발을 진행 중이었기 때문에 학교측에서 와이파이에 포트 제한을 걸어놓은 줄 알고 있었다(실제로 학교 선배도 그렇게 말했었다. 그래서 어찌저찌 우회해서 개발했다고...) 

그래서 별짓 다 했다. 방화벽 문제인가 싶어서 인바운드 편집도 해보고... 프로젝트 DB로 MariaDB를 쓰고 있었기 때문에 3306 포트 접속 허용하고... 암튼 별 걸 다 해봤다. 그런데도 계속 똑같은 오류가 떠서 원인이 뭘까 곰곰히 생각해 보았다.

 

결국, 다음날에 문제를 해결할 수 있었다. 문제의 원인은 학교 와이파이도, 방화벽도 문제가 아니었다. 문제는 그냥 SSH 터널링이 안 되어서 그런 것이었다. 사실 이를 전혀 간과하지 못했던 것이, 나는 이미 파워쉘을 통해 SSH 터널링 해 놓은 상태였고, 그 상태에서 접속을 시작했는데 안 되었던 거라서(프로젝트가 그냥 일회성 토이프로젝트가 아닌, 실제로 창업팀이 서비스할 프로덕트를 만드는 것이라 보안이 그 무엇보다 중요해서 SSH 서버를 반드시 사용해야 한다) 전혀 터널링이 안 되어서 문제가 되었을 것이라는 생각을 안 해 봤다.

 

그런데, 내가 쓰고 있는 IDE는 VSCode인데, 여기서 터미널을 제공해주고 있다. 여기서 ssh 접속 코드를 입력해주면 되는 거였는데 따로 윈도우 터미널로 접속하면 그게 IDE까지 연동이 되지는 않는 모양이다.(왜 그런지는 모르겠지만 잠재적으로 내린 결론이다) 결국 윈도우 터미널이 아닌 IDE에 내장된 터미널로 SSH 터널링 했고, 그 후 접속 시도해 봤는데 그동안의 삽질이 무색하게도 바로 성공해버렸다...

 

결론: 개발 시, SSH 서버에 접속할 때는 반드시 IDE에 내장된 터미널을 통해 터널링을 시도하자.

1.What is Generative AI Studio?
 
(1) A machine learning model that is trained on text only.
 
(2) A technology that lets you code programming languages without learning them.
 
(3) A type of artificial intelligence that writes emails for you.
 
(4) A tool that helps you use Generative AI capabilities in your application. 정답
 
2.How does generative AI generate new content?
 
(1) It is programmed based on predetermined algorithms that can not be altered.
 
(2) It learns from a massive amount of existing content. 정답
 
(3) It is a random process.
 
(4) The training leads to a foundation model that cannot be further tuned with a new dataset.
 
3.What is a prompt?
 
(1) A prompt is a long piece of text that explains how a large language model generates text.
 
(2) A prompt is a piece of text that is used to evaluate a large language model.
 
(3) A prompt is a short piece of text that is used to guide a large language model to generate content. 정답
 
(4) A prompt is a piece of text that is used to train a large language model.
 
4. Which of the following is a type of prompt that allows a large language model to perform a task with only a few examples? 틀림;
 
(1) Few-shot prompt
 
(2) One-shot prompt
 
(3) Zero-shot prompt
 
(4) Unsupervised prompt
 
5. Which of the following is the best way to generate more creative or unexpected content by adjusting the model parameters in Generative AI Studio?
 
(1) Setting the top K to 1
 
(2) Setting the temperature to a high value 정답
 
(3) Setting the top P to 25%
 
(4) Setting the temperature to a low value

 

+ Recent posts