반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/yjhIxpR_-gE

연재 마지막 순서로 첫화면에 이미지 슬라이드를 같이 개발합니다.

 

우선 관리자에서 이미지를 5개 정도 업로드합니다.

이미지 슬라이드는 Bootstrap-vue의 Carousel 컴퍼넌트를 사용합니다. 

https://bootstrap-vue.org/docs/components/carousel

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

Carousel 소스를 복사하여 다음과 같이 수정합니다.

pages/index.vue

<template>
  <div>
    <b-carousel
      id="carousel-1"
      v-model="slide"
      :interval="4000"
      controls
      indicators
      background="#ababab"
      img-width="1024"
      img-height="480"
      style="text-shadow: 1px 1px 2px #333;"
      @sliding-start="onSlideStart"
      @sliding-end="onSlideEnd"
    >
      <!-- Text slides with image -->
      <div v-for="slider in sliders" v-bind:key="slider.id">
        <b-carousel-slide
          caption="First slide"
          text="Nulla vitae elit libero, a pharetra augue mollis interdum."
          :img-src="slider.imageUrl"
        ></b-carousel-slide>
      </div>
      
    </b-carousel>

    <p class="mt-4">
      Slide #: {{ slide }}<br>
      Sliding: {{ sliding }}
    </p>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  data() {
    return {
      sliders: '',
      slide: 0,
      sliding: null
    }
  },
  computed: {
    accessToken() {
      return this.$store.state.accessToken;
    }
  },
  mounted() {
    this.getSliders();
  },
  methods: {
    async getSliders(){
      const options = {
          headers: {
              Authorization: `Bearer ${this.accessToken}`
          }
      }
      const path = '/slider';
      await this.$axios.get(
        path,
        options
      ).then(response=> {
        this.sliders = response.data;
        console.log(this.sliders);
      }).catch(error => {
        console.log(error);
      })
    },
    onSlideStart(slide) {
      this.sliding = true
    },
    onSlideEnd(slide) {
      this.sliding = false
    }
  }
}
</script>

서버를 실행하면 다음과 같이 화면이 스크롤 됩니다.

이렇게 해서 이미지 슬라이드가 완성되었습니다.

 

Vue와 Nestjs를 공부하시는 분들에게 도움이 되었으면 좋겠습니다.

 

수고하셨습니다^^

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/_bmxSSTzQsQ

지난 시간에 S3와 DB에 업로드한 파일을 조회하고 삭제하는 기능을 구현해 봅니다.


이미지 조회 기능 구현

slider-api 프로젝트에서 slider를 조회하는 api를 다음과 같이 추가합니다.

/src/slider/slider.controller.ts

@Get('')
async getSlider(): Promise<Slider[]> {
    return await this.sliderService.getSliders();
}

/src/slider/slider.service.ts

async getSliders(): Promise<Slider[]> {
    return await this.sliderRepository.find();
}

slider-front 프로젝트에 조회 기능을 추가합니다.

/pages/admin/index.vue

data()에 sliders 변수를 추가합니다.

data() {
    return {
        file1: null,
        bucketName: this.$env.S3_BUCKET,
        bucketRegion: this.$env.S3_REGION,
        identityPoolId: this.$env.S3_IDENTITY_POOL_ID,
        sliders: ''
    }
},

methods: 아래

async getSliders(){
    const options = {
        headers: {
            Authorization: `Bearer ${this.accessToken}`
        }
    }
    const path = '/slider';
    await this.$axios.get(
        path,
        options
    ).then(response => {
        this.sliders = response.data;
        console.log(this.sliders);
    }).catch(error => {
        console.log(error);
    })
},

이미지를 보여주기 위해서 bootstrap-vue의 card를 사용합니다.

https://bootstrap-vue.org/docs/components/card 

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

card component 소스를 복사하여 다음과 같이 수정합니다.

/pages/admin/index.vue

<div>
    <b-row>
        <b-col>
            <div v-for="slider in sliders" v-bind:key="slider.id">
                <div>
                    <b-card
                        :img-src="slider.imageUrl"
                        img-alt="slider"
                        img-top
                        tag="article"
                        style="max-width: 20rem;"
                        class="mb-2"
                    >
                        <b-card-text>
                        </b-card-text>
                        <b-button variant="primary" @click="deleteSlider(slider.id)">삭제</b-button>
                    </b-card>
                    </div>
            </div>
        </b-col>
    </b-row>
</div>

조회 테스트를 합니다.

이미지가 보이지 않습니다.

원인은 이미지 파일이 잘못 올라갔기 때문이므로, 업로드 소스를 "Body: this.file1" 으로 수정합니다.

await s3.upload({
    ACL: 'public-read',
    Body: this.file1,
    Key: this.genDateTime()+'_'+this.file1.name
}

이미지를 다시 업로드 하면 다음과 같이 표시됩니다.

 

이미지 삭제 기능 구현

slider-api에 삭제 api를 추가합니다.

/src/slider/slider.controller.ts

@Delete('/:id')
async deleteSlider(@Param('id') id, @Request() requestAnimationFrame, @Response() res): Promise<any> {
    return await this.sliderService.deleteSlider(id);
}

/src/slider/slider.service.ts

import * as AWS from 'aws-sdk';

async getSlider(id: number): Promise<Slider> {
    const slider = await this.sliderRepository.findOne({where: {id}});
    if(slider){
        return slider;
    }else{
        throw new NotFoundException(`Slider with ID ${id} not found!`);
    }
}

async deleteSlider(id): Promise<any> {
    const slider = await this.getSlider(id);
    if(slider){
        const bucketName = process.env.S3_BUCKET;
        const region = process.env.S3_REGION;
        const IdentityPoolId = process.env.S3_IDENTITY_POOL_ID;
        AWS.config.update({
            region,
            credentials: new AWS.CognitoIdentityCredentials({
                IdentityPoolId
            })
        });
        const s3 = new AWS.S3({
            apiVersion: '2006-03-01',
            params: {
                Bucket: bucketName
            }
        });
        //s3 파일 삭제
        const uriName = decodeURI(slider.imageUrl);
        const fileNamePos = uriName.indexOf('/images/');
        if(fileNamePos === -1){
            throw new NotFoundException();
        }
        const fileName = uriName.substring(fileNamePos + 8);
        s3.deleteObject({
            Bucket: bucketName,
            Key: 'images/'+fileName
        }), (err) => {
            if(err) {
                console.log(err);
                throw new InternalServerErrorException(err);
            }
        }
        //table에서 삭제
        return await this.sliderRepository.delete(id);
    }
}

터미널에서 다음 명령어로 aws-sdk 패키지를 추가합니다.

yarn add aws-sdk

환경 설정 파일 .env에 다음 내용을 추가합니다.
.env

S3_BUCKET=codegear-slider-bucket
S3_REGION=ap-northeast-2
S3-IDENTITY_POOL_ID=ap-northeast-2:*****************************

slider-front 프로젝트에 delete method를 추가합니다.
/pages/admin/index.vue

<b-button variant="primary" @click="deleteSlider(slider.id)">삭제</b-button>
.....
async deleteSlider(id){
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.accessToken}`
    }
    await this.$axios.delete(
        `/slider/${id}`,
        {headers}
    ).then(async response =>{
        await this.getSliders();
    }).catch(error=> {
        console.log(error);
    })
}

삭제 버튼을 클릭하면 이미지가 삭제되는 것을 확인할 수 있습니다.

 

반응형
반응형

Frontend에서 S3 버킷에 파일을 업로드 하는 것을 알아보겠습니다.

 

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/7FsycVjRq8Q

 

slider-front 프로젝트를 열고 admin 페이지를 생성합니다.

내용은 index.vue를 복사하여 만듭니다.

/pages/admin/index.vue

<template>
  <div>
    <div>관리자 페이지</div>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

관리자 메뉴 링크 클릭시 위 페이지로 이동하도록 아래와 같이 변경합니다.

/layouts/default.vue

async admin() {
  await this.$router.replace('/admin/');
}

 

관리자 메뉴의 링크가 잘 작동하는지 테스트를 합니다.

 

dotenv와 aws-sdk 패키지를 추가합니다.

yarn add @nuxtjs/dotenv dotenv aws-sdk

aws 설정을 위해 plugins 폴더를 추가하고 env-var.js 파일을 아래와 같이 추가합니다.

export default ({
    app, context
}, inject) => {
    const env = {}
    env.S3_BUCKET = 'codegear-slider-bucket'; //버킷명 입력
    env.S3_REGION = 'ap-northeast-2'; //리전명 입력
    env.S3_IDENTITY_POOL_ID = '설정한 IDENTITY_POOL_ID값 입력';
    inject('env', env)
}

 

파일 업로드를 위해 Bootstrap-Vue의 Form File Component를 추가합니다.

https://bootstrap-vue.org/docs/components/form-file

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

Admin 페이지를 다음과 같이 수정합니다.

/admin/index.vue

<template>
    <div>
        <b-form-file
            ref="file-input"
            v-model="file1"
            :state="Boolean(file1)"
            placeholder="Choose a file or drop it here..."
            drop-placeholder="Drop file here..."
            accept="image/*"
        ></b-form-file>
        <div class="mt-3">Selected file: {{ file1 ? file1.name : '' }}</div>
        <b-button @click="clearFiles" class="mr-2">Reset</b-button>
        <b-button @click="upload" class="mr-1">Upload</b-button>
    </div>
</template>

<script>
import AWS from 'aws-sdk';

export default {
    name: 'IndexPage',
    data() {
        return {
            file1: null,
            bucketName: this.$env.S3_BUCKET,
            bucketRegion: this.$env.S3_REGION,
            IdentityPoolId: this.$env.S3_IDENTITY_POOL_ID,
        }
    },
    computed: {
        accessToken() {
            return this.$store.state.accessToken;
        }
    },
    methods: {
        clearFiles() {
            this.$refs['file-input'].reset()
        },
        async upload() {
            if(!this.file1) {
                alert('File을 선택해주세요.')
            }else{
                console.log(this.file1);
                console.log(`${this.bucketRegion} ${this.bucketName}`);
                AWS.config.region = this.bucketRegion;
                AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                    IdentityPoolId: this.IdentityPoolId,
                });
                const s3 = new AWS.S3({
                    apiVersion: '2006-03-01',
                    params: {
                        Bucket: this.bucketName+'/images'
                    }
                });
                await s3.upload({
                    ACL: 'public-read',
                    Body: this.file1,
                    Key: this.file1.name
                }, (error)=>{
                    if(error){
                        this.errorHandler(error);
                        // return alert('There was an error uploading your photo: ', error.message);
                    }
                }).promise().then((data)=>{
                    console.log('File uploaded!!!')
                    console.log(data);
                })
            };
        },
    }
}
</script>

파일 업로드 테스트를 해봅니다.

위와 같이 정상적으로 업로드가 안되고 AccessDenied가 나올 경우,
AWS의 IAM - 역할 메뉴에서 다음 항목을 검색합니다.

Cognito_SliderS3AccessUnauth_Role

해당항목을 클릭하고 들어가면,
아래와 같은 정책 이름이 보입니다.

"oneClick_Cognito_SliderS3AccessUnauth_Role_..." 항목을 클릭합니다.

JSON을 선택하여 아래와 같이 작성 후 정책 검토를 한 후 저장합니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::codegear-slider-bucket/*",
                "arn:aws:s3:::codegear-slider-bucket"
            ]
        }
    ]
}

 

 

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/A8FcYGEe2i4

이번 시간은 아마존에서 제공하는 Storage Service인 S3에 파일을 업로드 하는 것을 알아봅니다.

S3는 Simple Storage Service의 각 단어 첫번째 글자가 S가 3개라는 의미입니다.

S3를 이용하면 이미지 파일을 저장할 수 있고, 저장된 이미지를 웹에서 볼 수 있습니다.

 

순서는 다음과 같습니다.

  • AWS S3에 버킷 생성
  • Cognito Identity Pool(자격 증명 풀) 생성
  • IAM에서 자격 증명 풀에 S3 권한 추가

AWS에서 S3 버킷 생성 

우선 AWS에 로그인 한 후 "S3"를 검색하여 서비스로 이동합니다.

AWS S3

 

S3를 사용하기 위해서는 아래와 같이 Bucket을 생성해 주어야 합니다.

  • 왼쪽 메뉴의 "버킷"을 클릭
  • "버킷만들기" 버튼을 클릭버킷의 이름은 S3 서비스를 통틀어 유일해야만 함.

  • 리전을 선택한 후
  • 버킷 선택을 클릭

  • 퍼블릭 액세스 차단 설정에서 "모든 퍼블릭 엑세스 차단"의 체크 박스를 해제.
    • 현재는 샘플 프로젝트이므로 보안 작업 없이 진행합니다.
    • 운영 환경에서는 권한을 반드시 설정하여 사용하시기 바랍니다.

  • "모든 퍼블릭 액세스 차단을 비활성화하면..." 아래의 체크박스를 체크.

  • "버킷 만들기" 클릭

  • 버킷 생성이 완료되면 생성된 버킷명을 클릭하고, "권한" 탭을 클릭

 

  • CORS 항목에서 "편집"을 클릭한 후 다음 내용을 입력합니다.
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]

Cognito Identity Pool(자격 증명 풀) 생성

  • aws에서 cognito 서비스를 검색하여 이동합니다.

  • "자격 증명 풀 관리"를 클릭.

  • "새 자격 증명 풀 만들기"를 클릭.

  • 자격 증명 풀 이름 입력(예: SliderS3Access)
  • "인증되지 않은 자격 증명에 대한 액세스 활성화" 클릭

  • "풀 생성" 버튼 클릭

  • "허용" 버튼 클릭

  • 샘플 코드의 플랫폼에서 "JavaScript"를 선택하면 아래와 같은 코드가 나타납니다.

  • 이 코드를 소스에 추가하면 업로드가 가능해집니다.

IAM에서 권한 추가

  • AWS의 IAM 서비스로 이동합니다.

  • "역할" 메뉴에서 "slider"로 검색.
  • 생성된 Role을 클릭.(예 : Cognito_SliderS3AccessAuth_Role)

  • "권한 추가" 클릭 후 "정책 연결"을 선택.

  • 기타 권한 정책에서 "s3"로 검색 후 "AmazonS3FullAccess"를 선택.
  • "정책 연결" 버튼을 클릭.

이렇게 하면 S3에 업로드 할 수 있는 권한 생성 및 설정이 완료되었습니다.

반응형
반응형

아래는 이 글의 동영상 강좌입니다.

https://youtu.be/QO1NunXmDuU

 

Jwt Token을 이용하여 frontend에서 로그인을 하는 방법을 알아보겠습니다.

 

Store 기능 사용하기

로그인 정보 저장을 위해 store를 사용하도록 합니다.

store 폴더 아래에 index.js 를 추가하고 다음 코드를 입력합니다.

/store/index.js

export const state = () => ({
    accessToken: {}
});

export const mutations = {
    accessToken (state, data) {
        state.accessToken = data;
    }
}

 

Navbar 만들기

Navbar를 만드는 다양한 방법이 있습니다. 이번엔 bootstrap vue를 이용하도록 합니다.

Nuxt 프로젝트에 bootstrap vue를 사용하는 방법은 다음 사이트를 참조하세요. https://bootstrap-vue.org/docs 

아래 명령으로 slider-front 프로젝트에 설치를 합니다.

yarn add bootstrap-vue

nuxt-config.js에 다음 설정을 추가합니다.

modules = {
  'bootstrap-vue/nuxt'
}

프로젝트에 layouts 폴더를 만들고 default.vue를 만듭니다.

bootstrap vue 사이트의 navbar 샘플을 복사하여 default.vue의 template에 붙여놓고 아래와 같이 변경합니다.

<template>
  <div>
  <b-navbar toggleable="lg" type="dark" variant="info">
    <b-navbar-brand href="#">Slider</b-navbar-brand>

    <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

    <b-collapse id="nav-collapse" is-nav>
      <b-navbar-nav class="ml-auto" v-if="!accessToken">
        <b-nav-item href="/login" right>로그인</b-nav-item>
      </b-navbar-nav>
      <b-navbar-nav class="ml-auto" v-if="accessToken && accessToken!==''">
        <b-nav-item @click="logout" right>로그아웃</b-nav-item>
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
  <nuxt/>
  </div>
</template>
<script>
export default {
  name: 'Layout',
  methods: {
    async logout() {
      await this.$store.commit('accessToken', '');
      await this.$router.push('/');
    }
  },
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

 

Login

기존 index.vue를 login.vue로 변경합니다.

index.vue를 만들고 다음과 같이 입력합니다.

index 페이지에서 로그인이 잘 되었는지를 확인하기 위해 accessToken을 보여주는 예제입니다.

/pages/index.vue

<template>
  <div>
    <div>Welcome!</div>
    <div v-if="accessToken">{{accessToken}}</div>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

로그인 API 호출 로직 수정

pages/kakao-callback.vue를 다음과 같이 수정합니다.

<template>
  <div>로그인중....</div>
</template>

<script>
  export default {
    async mounted() {
      try{
        if(!this.$route.query.code){
          return this.$router.push('/')
        }
        console.log(`code : ${this.$route.query.code}`)
        const body = {
          code: this.$route.query.code,
          domain: window.location.origin
        }
        const response = await this.$axios.$post(
            '/auth/login', body, {}
        )
        if(response){
          console.log(response.accessToken);
          this.$store.commit('accessToken', response.accessToken);
        }
        return this.$router.push('/')
      }catch(error){
        console.log(error)
      }
    }
  }
</script>

프로젝트 실행화면

프로젝트를 실행하면 다음과 같은 화면을 보실 수 있습니다.

 

첫화면

 

로그인 화면

 

로그인 성공 후 화면

 

반응형
반응형

이번 시간은 JWT 인증 기능을 추가해 보겠습니다.

 

다음은 이 강의의 동영상입니다.

https://youtu.be/SClJMs2BlWk

 

 

JWT에 관한 자세한 사항은 제가 이전에 작성한 글을 참조해 주세요.

https://codegear.tistory.com/72

 

[Nodejs 프레임워크]NestJS - 12. JWT 토큰 생성

다음은 이글의 동영상 강의 입니다. https://youtu.be/H4VS-Osylvo 토큰을 이용한 API 호출 방법 REST API 호출시 토큰을 이용하면 사용자 인증과 권한을 체크할 수 있습니다. 다음 그림은 토큰의 생성과 호

codegear.tistory.com

 

JWT 관련 패키지 추가

jwt 인증을 위해서는 다음과 같은 패키지가 필요합니다.

- @nestjs/jwt

- @nestjs/passport

- passport

- passport-jwt

아래와 같이 패키지를 추가합니다.

yarn add @nestjs/jwt @nestjs/passport passport passport-jwt

 

Payload 인터페이스 생성

jwt인증시엔 payload interface가 필요합니다.

아래와 같이 같이 Payload interface를 만듭니다.(참조 : https://jwt.io/)

/src/auth/security/payload.interface.ts

export interface Payload {
    id: number;
    name: string;
    authorities?: any[];
}

JwtStrategy 를 아래와 같이 작성합니다.

/src/auth/security/passport.jwt.stategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { AuthService } from "../auth.service";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
    constructor(private authService: AuthService){
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: true,
            secretOrKey: process.env.JWT_SECRET
        })
    }
}

 

로그인 및 회원 가입

카카오 로그인 후에 회원 로그인 및 회원 가입을 처리합니다.

- 회원 데이터가 없을 경우는 회원 가입을 처리합니다.

- 회원 데이터가 있을 경우는 로그인 처리를 합니다.

 

우선 auth.controller에서 카카오 로그인을 한 후 다음 코드를 추가합니다.

/src/auth/auth.controller.ts

			console.log(`kakaoUserInfo : ${JSON.stringify(kakao)}`);
            if (!kakao.id) {
                throw new BadRequestException('카카오 로그인 실패!');
            }

            // 로그인 처리 - 회원 가입이 안되어 있을 경우 가입 처리
            const jwt =  await this.authService.login(kakao);
            console.log(`jwt.accessToken : ${jwt.accessToken}`);
            res.send({
                accessToken: jwt.accessToken,
                message: 'success'
            });

authService에서 고객을 조회하고, 로그인을 처리합니다.

/src/auth/auth.service.ts

async login(kakao): Promise<{accessToken: string} | undefined> {
	let userFind: User = await this.userService.findByFields({
            where: { kakaoId: kakao.id }
        });
        if(!userFind) {
            isFirstLogin = true;
            // 회원 가입
            const user = new User();
            user.kakaoId = kakao.id;
            user.email = kakao.kakao_account.email;
            user.name = kakao.kakao_account.name;
            
            userFind = await this.registerUser(user);
        }

        const payload: Payload = {
            id: userFind.id,
            name: userFind.name,
            authorities: userFind.authorities
        };
        return {
            accessToken: this.jwtService.sign(payload)
        };
}

userService에  findByFields, registerUser를 추가합니다.

/src/auth/user.service.ts

   constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
        @InjectRepository(UserAuthority)
        private userAuthorityRepository: Repository<UserAuthority>,
    ){}
    
    async findByFields(options: FindOneOptions<User>): Promise<User | undefined> {
        return await this.userRepository.findOne(options);
    }
    
    async registerUser(newUser: User): Promise<User> {
        let userFind: User = await this.findByFields({
            where: { kakaoId: newUser.kakaoId }
        });
        if(userFind) {
            throw new HttpException('Username aleady used!', HttpStatus.BAD_REQUEST);
        }
        const registeredUser = await this.userService.save(newUser);
        if(registeredUser){
            await this.saveAuthority(registeredUser.id);
        }else {
            throw new HttpException('Username register error!', HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return registeredUser;
    }
    
    async save(user: User): Promise<User | undefined> {
        return await this.userRepository.save(user);
    }
    
    async saveAuthority(userId: number): Promise<UserAuthority | undefined> {
        let userAuth = new UserAuthority();
        userAuth.userId = userId;
        userAuth.authorityName = RoleType.USER;
        return await this.userAuthorityRepository.save(userAuth);
    }

권한 Type을 선언한 RoleType을 만들어줍니다.

/src/auth/role-type.ts

export enum RoleType {
    USER = 'ROLE_USER',
    ADMIN = 'ROLE_ADMIN',
}

 

auth.module에 jwtModule을 추가하고 import 합니다.

/src/auth/auth.module.ts

import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './security/passport.jwt.strategy';
...
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: {expiresIn: '300s'},
    }),
    PassportModule
  ],
  exports: [TypeOrmModule, UserService],
  controllers: [AuthController],
  providers: [AuthService, UserService, JwtStrategy]
})
export class AuthModule {}

 

프론트에서 로그인을 하면 브라우저의 콘솔에서 아래와 같이 jwt token을 확인할 수 있습니다.

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/7sT3G9ZwyzM

 

카카오 로그인 기능을 만들어봅니다.

 

진행 순서는 다음과 같습니다.

  1. 카카오 developer 사이트에서 application 생성
  2. 프론트 개발
  3. 백엔드 개발

보다 자세한 설명은 이전 영상을 보시면 됩니다.

https://www.youtube.com/watch?v=E-a-wQqybbE 

 

카카오 developer 사이트에서 application 생성

  • 우선 카카오 로그인을 사용하기 위해 https://developers.kakao.com/ 에서 로그인을 합니다.
  • 상단의 내 애플리케이션 메뉴를 클릭 합니다.

  • 애플리케이션 추가하기를 클릭합니다.

  • 애플리케이션 정보를 입력합니다.

  • 애플리케이션 목록에서 새로 만든 slider를 확인할 수 있습니다.

  • slider 앱을 클릭하면 앱에 대한 정보를 확인할 수 있습니다.

  • 플랫폼 -> 플랫폼 설정하기를 클릭합니다.

  • 우리는 Web으로 개발을 할 것이므로 web을 클릭합니다. 사이트 도메인에 http://localhost:3000 를 입력한 후 저장합니다.
    *http://localhost:3000 는 로컬에서 프론트를 개발하여 테스트 하기 위한 정보입니다.

  • 저장이 완료되면 다음과 같이 변경된것을 확인할 수 있습니다. 여기서 Redirect URI 등록하러 가기를 클릭합니다.

  • 활성화 설정은 ON 으로 변경하고, Redirect URI 등록을 클릭합니다. 

  • Redirect URI에 http://localhost:3000/kakao-callback 을 입력합니다.

  • Left 메뉴에서 카카오로그인 -> 동의 항목을 클릭합니다.

  •  동의 항목은 카카오 로그인시 가져올 수 있는 고객의 정보를 선택하는 화면입니다.

  • 설정 항목을 클릭한 후 필수 또는 선택 동의로 변경하고, 저장합니다.
    * 필수 동의(검수 필요)가 disable되어 있는 경우는 비즈니스 채널을 신청해야만 선택할 수 있습니다.

  • 카카오 애플리케이션 설정이 완료되었습니다.

프론트 개발

  • 다운로드 받은 이미지를 프로젝트의 /static/images/kakao_login.png로 복사합니다.
  • 카카오 로그인 모듈을 사용하기 위해 
    /nuxt.config.js 의 head 부분에 다음을 추가합니다.
script: [
  {
    src: 'https://developers.kakao.com/sdk/js/kakao.js'
  }
]
  • index page를 수정합니다.
    /pages/index.vue
<template>
  <a @click="loginWithKakao">
    <img src="/images/kakao_login.png"/>
  </a>
</template>

<script>
export default {
  name: 'IndexPage',
  methods: {
    kakaoInit () {
      Kakao.init('329a6a74...')// KaKao client key
      Kakao.isInitialized()
    },
    async loginWithKakao () {
      await Kakao.Auth.authorize({
        redirectUri: `${window.location.origin}/kakao-callback`
      })
    }
  },
  mounted () {
    this.kakaoInit()
  }
}
</script>
  • callback 페이지를 추가합니다.
    /pages/kakao-callback.vue
<template>
  <div>
    로그인 중 입니다...
  </div>
</template>

<script>
export default {
  async mounted () {
    try {
      // code 유무 체크
      if (!this.$route.query.code) {
        return this.$router.push('/')
      }
      console.log(`code : ${this.$route.query.code}`)
      // 서버 로그인 요청(kakao 인증 코드 전달)
      const body = {
        code: this.$route.query.code,
        domain: window.location.origin
      }
      const response = await this.$axios.$post(
        '/auth/login', body, {}
      )
      console.log(response)
    } catch (error) {
      console.log(error)
    }
  }
}
</script>
  • 이제 프론트를 실행하고 테스트 해봅니다.
yarn dev
  • 카카오 로그인 버튼을 클릭합니다.

  • 동의 화면이 나오고 동의 체크 후

  • 크롬의 콘솔을 실행해보면 code 값이 찍힌 것을 확인할 수 있습니다.

백엔드 개발

  • slider-api 프로젝트를 open 합니다.
  • 포트 변경 - frontend에서 3000번 포트를 사용하고 있으므로, backend 포트를 3003으로 바꿔주겠습니다.
    /src/main.ts
  await app.listen(3003);
  • front의 호출 url도 http://localhost:3003으로 변경합니다.
    /nuxt.config.js
axios: {
	baseURL: 'http://localhost:3003'
},
  • 카카오 api 호출을 위해 axios 패키지를 추가합니다.
yarn add axios
  • generator로 auth module/controller/service를 생성합니다.
nest g module auth
nest g controller auth
nest g service auth
  • authController에 다음을 추가합니다.
    src/auth/auth.controller.ts
import { Controller, Post, Body, Response, BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
    constructor(
        private authService: AuthService
    ){}
    @Post('/login')
    async login(@Body() body: any, @Response() res): Promise<any> {
        try {
        // 카카오 토큰 조회 후 계정 정보 가져오기
        const { code, domain } = body;
        if (!code || !domain) {
            throw new BadRequestException('카카오 정보가 없습니다.');
        }
        const kakao = await this.authService.kakaoLogin({ code, domain });

        console.log(`kakaoUserInfo : ${JSON.stringify(kakao)}`);
        if (!kakao.id) {
            throw new BadRequestException('카카오 정보가 없습니다.');
        }

        res.send({
            user: kakao,
            message: 'success',
        });
        } catch (e) {
            console.log(e);
            throw new UnauthorizedException();
        }
    }
}
  • authService에 다음을 추가합니다.
    /src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import axios from 'axios';
import * as qs from 'qs';

@Injectable()
export class AuthService {
    async kakaoLogin(options: { code: string; domain: string }): Promise<any> {
        const { code, domain } = options;
        const kakaoKey = '87073966cb41...';
        const kakaoTokenUrl = 'https://kauth.kakao.com/oauth/token';
        const kakaoUserInfoUrl = 'https://kapi.kakao.com/v2/user/me';
        const body = {
          grant_type: 'authorization_code',
          client_id: kakaoKey,
          redirect_uri: `${domain}/kakao-callback`,
          code,
        };
        const headers = {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
        };
        try {
          const response = await axios({
            method: 'POST',
            url: kakaoTokenUrl,
            timeout: 30000,
            headers,
            data: qs.stringify(body),
          });
          if (response.status === 200) {
            console.log(`kakaoToken : ${JSON.stringify(response.data)}`);
            // Token 을 가져왔을 경우 사용자 정보 조회
            const headerUserInfo = {
              'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
              Authorization: 'Bearer ' + response.data.access_token,
            };
            console.log(`url : ${kakaoTokenUrl}`);
            console.log(`headers : ${JSON.stringify(headerUserInfo)}`);
            const responseUserInfo = await axios({
              method: 'GET',
              url: kakaoUserInfoUrl,
              timeout: 30000,
              headers: headerUserInfo,
            });
            console.log(`responseUserInfo.status : ${responseUserInfo.status}`);
            if (responseUserInfo.status === 200) {
              console.log(
                `kakaoUserInfo : ${JSON.stringify(responseUserInfo.data)}`,
              );
              return responseUserInfo.data;
            } else {
              throw new UnauthorizedException();
            }
          } else {
            throw new UnauthorizedException();
          }
        } catch (error) {
          console.log(error);
          throw new UnauthorizedException();
        }
      }
}

* kakaoKey는 kakao developers 사이트에서 생성한 RestAPI 키를 입력합니다.

  • 브라우저에서 http://localhost:3000을 입력하고 로그인을 해보면 다음과 같이 CORS 오류를 만나게 됩니다.

  • CORS를 해결하기 위해 main.ts에 설정을 추가합니다.
    /src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const whitelist = [
    'http://localhost:3000'
  ];
  app.enableCors({
    origin: function (origin, callback) {
      if (!origin || whitelist.indexOf(origin) !== -1) {
        callback(null, true)
      } else {
        callback(new Error('Not allowed by CORS'))
      }
    },
    allowedHeaders: '*',
    methods: "GET,PUT,PATCH,POST,DELETE,UPDATE,OPTIONS",
    credentials: true,
  });
  await app.listen(3003);
}
bootstrap();
  • 다시 브라우저에서 로그인을 시도합니다.
    로그인이 완료되면 크롬 콘솔에서 다음과 같은 로그를 확인할 수 있습니다.

카카오 로그인이 완료되었습니다.

 

다음 시간에는 로그인에 JWT 인증을 추가하는 것을 진행합니다.

반응형
반응형

이 글의 동영상 강좌

https://youtu.be/P9fvgGHd_Jg

Backend의 Nestjs에서 Database(MySQL)를 연결하기 위해 TypeORM을 설정해봅니다.

 

진행 순서는 다음과 같습니다.

MySQL Schema와 User 생성

MySQL Workbench를 이용해서 Schema와 User를 생성합니다.

- Schema : slider

- User : slider

- Password : 1234

자세한 내용은 이전에 작성한 내용을 참조하시기 바랍니다.

https://codegear.tistory.com/32

 

데이터베이스기초-02.MySQL-Schema, User, Table 생성하기

이 글의 동영상 강의입니다. https://youtu.be/hOOiqk8Z1gQ MySQL Workbench를 이용하여 가장 기본적인 Schema와 User를 생성하고, User에게 Schema의 권한을 주는법과, Schema에 Table을 생성하는 법을 알아보겠..

codegear.tistory.com

 

패키지 설치

TypeORM과 MySQL 패키지를 설치합니다.

yarn add @nestjs/typeorm typeorm mysql2

 

TypeORM 설정

/src/orm.config.ts 파일을 아래와 같이 생성합니다.

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

function ormConfig(): TypeOrmModuleOptions {
  const commonConf = {
    SYNCRONIZE: false,
    ENTITIES: [__dirname + '/domain/*.entity{.ts,.js}'],
    MIGRATIONS: [__dirname + '/migrations/**/*{.ts,.js}'],
    MIGRATIONS_RUN: false,
  };

  return {
    name: 'default',
    type: 'mysql',
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
    logging: true,
    synchronize: commonConf.SYNCRONIZE,
    entities: commonConf.ENTITIES,
    migrations: commonConf.MIGRATIONS,
    migrationsRun: commonConf.MIGRATIONS_RUN,
  };
}

export { ormConfig };

 

Main Module에 typeorm config 설정 추가

/src/app.module.ts에서 @Module -> imports 아래에 다음 내용을 추가합니다.

....
import { TypeOrmModule } from '@nestjs/typeorm';
import { ormConfig } from './orm.config';

@Module({
    imports: [
    	TypeOrmModule.forRootAsync({ useFactory: ormConfig }),
    ]
....

 

환경 설정 파일(.env) 생성

orm.config.ts에서 process.env.DB_NAME과 같은 형태로 환경변수를 참조하도록 했습니다.

/.env 파일을 아래와 같이 생성합니다.(.env는 dotenv라고 읽습니다.)

NODE_ENV=local
DB_HOST=localhost
DB_PORT=3306
DB_USER=slider
DB_PASS=1234
DB_NAME=slider

- local db에 설정한데로 값을 입력하면 됩니다.

 

@nestjs/config 모듈 추가

.env 파일을 사용하기 위해서는 dotenv모듈이 필요합니다.
nestjs에서는 @nestjs/config 모듈을 설치하면 dotenv모듈을 사용할 수 있습니다.

아래와 같이 설치를 합니다.

yarn add @nestjs/config

아래와 같이 main module에 ConfigModule 관련 설정을 추가합니다.

/src/app.module.ts

import { ConfigModule } from '@nestjs/config';
...

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    ....]
...

 

로그 확인

이제 application을 실행해봅니다.

yarn start

application이 정상적으로 실행되면 아래와 같은 로그를 확인할 수 있습니다.

 

Entity 생성

위에서 typeorm 설정 파일의 entity 파일 위치를 아래와 같이 설정했습니다.

ENTITIES: [__dirname + '/domain/*.entity{.ts,.js}'],

따라서 src/domain이라는 폴더를 만들고 entity 파일들을 만들면 됩니다.

 

/src/domain/user.entity.ts 를 아래와 같이 만듭니다.

import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import {UserAuthority} from "./user-authority.entity";

@Entity('user', { schema: 'slider' })
export class User {
    @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
    id: number;

    @Column('varchar', { name: 'kakao_id', length: 45 })
    kakaoId: string;

    @Column('varchar', { name: 'email', length: 100 })
    email: string;

    @Column('varchar', { name: 'name', nullable: true, length: 45 })
    name: string | null;

    @Column('varchar', { name: 'gender', length: 10 })
    gender: string;

    @Column('varchar', { name: 'phone', length: 20 })
    phone: string;

    @Column('varchar', { name: 'birth', length: 10 })
    birth: string;

    @Column('varchar', { name: 'profile_image', nullable: true, length: 200 })
    profileImage: string | null;

    @Column('timestamp', {
        name: 'created_at',
        nullable: true,
        default: () => 'CURRENT_TIMESTAMP',
    })
    createdAt: Date | null;

    @Column('timestamp', {
        name: 'updated_at',
        nullable: true,
        default: () => 'CURRENT_TIMESTAMP',
    })
    updatedAt: Date | null;

    @OneToMany(() => UserAuthority, (userAuthority) => userAuthority.user, {
        eager: true,
    })
    authorities?: any[];
}

/src/domain/user-authority.entity.ts를 다음과 같이 만듭니다.

import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';

@Entity('user_authority')
export class UserAuthority {
  @PrimaryGeneratedColumn()
  id: number;

  @Column('int', { name: 'user_id' })
  userId: number;

  @Column('varchar',{name: 'authority_name'})
  authorityName: string;

  @ManyToOne(() => User, (user) => user.authorities)
  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
  user: User;
}

 

테이블 자동 생성

orm.config.ts 파일에서 syncronize를 true로 변경합니다.

SYNCRONIZE: true,

 

Application을 실행합니다.

yarn start

MySQL에 테이블이 생성된 것을 확인할 수 있습니다.

 

이상으로 TypeORM 설정이 완료되었습니다.

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/9KbnFR_1_Jg

 

 

 

nest project 생성(Backend)

nestjs.com에 가시면 프로젝트 생성하는 법을 확인할 수 있습니다.

https://docs.nestjs.com/first-steps

- project명은 slider-api로 생성합니다.

nest new slider-api

 

nuxt project 생성(Frontend)

nuxt.org에 가시면 프로젝트 생성하는 법을 확인할 수 있습니다.

https://nuxtjs.org/docs/get-started/installation

 

Installation

Here, you will find information on setting up and running a Nuxt project in 4 steps.

nuxtjs.org

- project명은 slider-front로 생성합니다.

- yarn create nuxt-app <project-name> 을 사용합니다.

 

yarn create nuxt-app slider-front

 

Repository 생성

github.com에 접속해서 Repository를 생성합니다.

  • github.com -> Repositories -> New를 선택합니다.
  • Backend Repository는 slider-api로 생성합니다.
  • Frontend Repository는 slider-front로 생성합니다.

.gitignore 파일 생성

  • 두개 프로젝트의 root 폴더에 .gitignore 파일을 생성합니다.
  • 이 파일은 git 서버에 올라가지 않아도 되는 파일들을 작성해줍니다.
/node_modules
/dist

.DS_Store
.idea

yarn.lock

 

Repository 연결

  • github desktop을 이용해서 소스를 clone합니다.
  • clone한 소스에 생성한 프로젝트 소스를 이동합니다.
  • 소스를 commit하고 push 합니다.

 

반응형
반응형

그동안 배웠던 내용들을 이용해서 사이트를 만드는 강좌를 진행합니다.

제목은 Nodejs와 Vuejs로 사이트 만들기 입니다.

 

이 글의 동영상 강좌입니다.

https://youtu.be/9RBeK3U8Z5Y

개발 스펙은 다음과 같습니다.

  • Backend는Nodejs (Nestjs 프레임워크를 사용합니다. TypeORM을 통해 DB를 연동합니다.)
  • Frontend는 Vuejs (Nuxtjs 프레임워크를 사용합니다.)
  • Database는 MySQL을 사용합니다. (MySQL Workbench의 Model 기능을 사용합니다.)

인프라는 다음과 같이 구성합니다.

  • Version Control(코드 저장소)은 Github을 사용합니다.
  • Production(운영) Server는 AWS의 EC2를 사용합니다.
  • Image가 배포될 Server는 AWS의 S3를 사용합니다.

기타

  • 배포는 Github Actions를 이용해서 자동배포가 되도록 구성을 합니다.
  • 로그인 및 회원 가입은 카카오 로그인을 이용해서 처리합니다.

기능

  • 사이트의 첫화면에 Slideshow가 나타나며 자동 스크롤이 됩니다.
  • 이 이미지는 관리자를 통해 관리되도록 합니다.

사이트를 직접 만들면서 각종 기술들이 어떻게 쓰이는지를 알아 갈 수 있도록 하는것이 이 강좌의 목적입니다.

많이 기대해주세요^^

반응형

+ Recent posts