From 68451b70596681c044e837a1108295424284c1a3 Mon Sep 17 00:00:00 2001
From: Raymond <akdfhr2@gmail.com>
Date: Sun, 31 Dec 2023 14:02:20 +0900
Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?=
 =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EC=A0=9C=ED=95=9C=20=EB=B0=94=EC=9D=B4?=
 =?UTF-8?q?=ED=8A=B8=EB=A1=9C=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/users/dtos/set-nickname.dto.ts            |  5 ++-
 .../validator/max-length-byte.validator.ts    | 40 +++++++++++++++++++
 2 files changed, 43 insertions(+), 2 deletions(-)
 create mode 100644 src/users/validator/max-length-byte.validator.ts

diff --git a/src/users/dtos/set-nickname.dto.ts b/src/users/dtos/set-nickname.dto.ts
index 73d1eef..667307e 100644
--- a/src/users/dtos/set-nickname.dto.ts
+++ b/src/users/dtos/set-nickname.dto.ts
@@ -1,9 +1,10 @@
 import { ApiProperty } from '@nestjs/swagger';
-import { IsString, MaxLength } from 'class-validator';
+import { IsString } from 'class-validator';
+import { MaxByteLength } from '../validator/max-length-byte.validator';
 
 export class ChangeNicknameDTO {
   @ApiProperty({ description: '번경할 닉네임', default: '츄츄' })
   @IsString()
-  @MaxLength(6)
+  @MaxByteLength(6)
   nickname: string;
 }
diff --git a/src/users/validator/max-length-byte.validator.ts b/src/users/validator/max-length-byte.validator.ts
new file mode 100644
index 0000000..43cc5f0
--- /dev/null
+++ b/src/users/validator/max-length-byte.validator.ts
@@ -0,0 +1,40 @@
+import { registerDecorator, ValidationOptions } from 'class-validator';
+
+const LINE_FEED = 10; // '\n'
+
+export function getByteLength(decimal: number) {
+  return decimal >> 7 || LINE_FEED === decimal ? 2 : 1;
+}
+
+export function getLimitedByteText(inputText: string, maxByte: number) {
+  const characters = inputText.split('');
+
+  return (
+    characters.reduce((acc, cur) => {
+      const decimal = cur.charCodeAt(0);
+      const byte = getByteLength(decimal); // 글자 한 개가 몇 바이트 길이인지 구해주기
+      return acc + byte;
+    }, 0) <= maxByte
+  );
+}
+
+export function MaxByteLength(
+  max: number,
+  validationOptions?: ValidationOptions,
+) {
+  return function (object: object, propertyName: string) {
+    registerDecorator({
+      name: 'MaxByteLength',
+      target: object.constructor,
+      async: false,
+      propertyName: propertyName,
+      constraints: [], // * 아래 validate 의 args.contraints로 넘어감
+      options: validationOptions,
+      validator: {
+        validate(value: any): boolean {
+          return getLimitedByteText(value, max);
+        },
+      },
+    });
+  };
+}

From 5b2d5290e09e2bad1f96c14f84717362ec7956ad Mon Sep 17 00:00:00 2001
From: Raymond <akdfhr2@gmail.com>
Date: Sun, 31 Dec 2023 14:05:39 +0900
Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?=
 =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EC=B2=B4=ED=81=AC=20=ED=8C=8C=EB=9D=BC?=
 =?UTF-8?q?=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/users/dtos/set-nickname.dto.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/users/dtos/set-nickname.dto.ts b/src/users/dtos/set-nickname.dto.ts
index 667307e..fa6ef33 100644
--- a/src/users/dtos/set-nickname.dto.ts
+++ b/src/users/dtos/set-nickname.dto.ts
@@ -5,6 +5,8 @@ import { MaxByteLength } from '../validator/max-length-byte.validator';
 export class ChangeNicknameDTO {
   @ApiProperty({ description: '번경할 닉네임', default: '츄츄' })
   @IsString()
-  @MaxByteLength(6)
+  @MaxByteLength(12, {
+    message: '닉네임이 너무 길어요.',
+  })
   nickname: string;
 }

From 748d81fa689d2ed2de585b13ee2caa9154410cac Mon Sep 17 00:00:00 2001
From: Raymond <akdfhr2@gmail.com>
Date: Sun, 31 Dec 2023 17:13:49 +0900
Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?=
 =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 global.d.ts                            |  2 +
 src/app.module.ts                      |  2 +
 src/oauth/dtos/service-provider.dto.ts |  1 +
 src/oauth/oauth.controller.ts          | 16 ++++++-
 src/oauth/oauth.service.ts             | 58 ++++++++++++++++++++++++++
 5 files changed, 77 insertions(+), 2 deletions(-)

diff --git a/global.d.ts b/global.d.ts
index 64be943..8b3dc67 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -10,6 +10,8 @@ declare namespace NodeJS {
     readonly GOOGLE_AUTH_CLIENT_ID: string;
     readonly GOOGLE_REDIRECT_URL: string;
     readonly OOGLE_AUTH_CLIENT_SECRET: string;
+    readonly KAKAO_CLIENT_ID: string;
+    readonly KAKAO_REDIRECT_URL: string;
     readonly SALT: string;
     readonly ROUND: string;
     readonly EXPIRESTOKEN: string;
diff --git a/src/app.module.ts b/src/app.module.ts
index 22b5ea2..59b7142 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -22,6 +22,8 @@ import { MailsModule } from './mails/mails.module';
         GOOGLE_AUTH_CLIENT_ID: Joi.string().required(),
         GOOGLE_AUTH_CLIENT_SECRET: Joi.string().required(),
         GOOGLE_REDIRECT_URL: Joi.string().required(),
+        KAKAO_CLIENT_ID: Joi.string().required(),
+        KAKAO_REDIRECT_URL: Joi.string().required(),
         DB_HOST: Joi.string().required(),
         DB_PORT: Joi.string().required(),
         DB_USER: Joi.string().required(),
diff --git a/src/oauth/dtos/service-provider.dto.ts b/src/oauth/dtos/service-provider.dto.ts
index b0f3186..9d5a993 100644
--- a/src/oauth/dtos/service-provider.dto.ts
+++ b/src/oauth/dtos/service-provider.dto.ts
@@ -1,3 +1,4 @@
 export enum ServiceProvider {
   GOOGLE = 'google',
+  KAKAO = 'kakao',
 }
diff --git a/src/oauth/oauth.controller.ts b/src/oauth/oauth.controller.ts
index 0031488..9123f1c 100644
--- a/src/oauth/oauth.controller.ts
+++ b/src/oauth/oauth.controller.ts
@@ -1,4 +1,12 @@
-import { Controller, Get, Param, Post, Query, Res } from '@nestjs/common';
+import {
+  BadRequestException,
+  Controller,
+  Get,
+  Param,
+  Post,
+  Query,
+  Res,
+} from '@nestjs/common';
 import { OauthService } from './oauth.service';
 import { Response } from 'express';
 import {
@@ -40,11 +48,15 @@ export class OauthController {
     @Param('serviceName', new ParseExplicitEnumPipe(ServiceProvider))
     serviceName: ServiceProvider,
     @Query('code') code: string,
-    // @Query('state') state: string,
   ): Promise<JWT> {
     switch (serviceName) {
       case ServiceProvider.GOOGLE:
         return await this.oauthService.userFromGoogle(code);
+      case ServiceProvider.KAKAO:
+        return await this.oauthService.userFromKakao(code);
+      default:
+        break;
     }
+    throw new BadRequestException();
   }
 }
diff --git a/src/oauth/oauth.service.ts b/src/oauth/oauth.service.ts
index 0305b43..1f55872 100644
--- a/src/oauth/oauth.service.ts
+++ b/src/oauth/oauth.service.ts
@@ -35,6 +35,15 @@ export class OauthService {
         );
         url.searchParams.set('access_type', 'offline');
         return url;
+      case ServiceProvider.KAKAO:
+        const kakaoURL = new URL('https://kauth.kakao.com/oauth/authorize');
+        kakaoURL.searchParams.set('client_id', process.env.KAKAO_CLIENT_ID);
+        kakaoURL.searchParams.set('response_type', 'code');
+        kakaoURL.searchParams.set(
+          'redirect_uri',
+          process.env.KAKAO_REDIRECT_URL,
+        );
+        return kakaoURL;
       default:
         break;
     }
@@ -79,4 +88,53 @@ export class OauthService {
       throw new BadRequestException('invalid request: ' + err?.message || '');
     }
   }
+  async userFromKakao(code: string): Promise<JWT> {
+    try {
+      const form = new FormData();
+      form.append('client_id', process.env.KAKAO_CLIENT_ID);
+      form.append('redirect_uri', process.env.KAKAO_REDIRECT_URL);
+      form.append('grant_type', 'authorization_code');
+      form.append('code', code);
+      const response = await axios.post<{
+        access_token: string;
+        token_type: string;
+        refresh_token: string;
+        expires_in: number;
+        scope: string;
+        refresh_token_expires_in: number;
+      }>('https://kauth.kakao.com/oauth/token	', form, {
+        headers: {
+          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+        },
+      });
+
+      if (!response.data['access_token']) {
+        throw new BadRequestException('Access-Token을 받아오지 못 했습니다.');
+      }
+      // 회원정보 가져오기
+      const userUrl = 'https://kapi.kakao.com/v2/user/me';
+      const userResponse = await axios.get(userUrl, {
+        params: {
+          access_token: response.data['access_token'],
+        },
+        headers: {
+          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+        },
+      });
+
+      // const googleUesr = userResponse.data as GoogleUserInfo;
+      return userResponse.data;
+      // let user = await this.userService.findByUserEmail(googleUesr.email);
+      // if (!user) {
+      //   user = await this.userService.create({ email: googleUesr.email });
+      // }
+      // const token = this.authService.sign(user.id);
+      // user.refresh = token.refresh;
+      // await this.dataSource.getRepository(User).save(user);
+      // return new JWT(token);
+    } catch (err) {
+      this.logger.error(err.message);
+      throw new BadRequestException('invalid request: ' + err?.message || '');
+    }
+  }
 }

From 4782dc1bf3376eb991b2dc2196df67201b5e2aa8 Mon Sep 17 00:00:00 2001
From: Raymond <akdfhr2@gmail.com>
Date: Sun, 31 Dec 2023 17:25:05 +0900
Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?=
 =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/oauth/oauth.service.ts | 87 ++++++++++++++++++++++++++------------
 1 file changed, 59 insertions(+), 28 deletions(-)

diff --git a/src/oauth/oauth.service.ts b/src/oauth/oauth.service.ts
index 1f55872..404b23e 100644
--- a/src/oauth/oauth.service.ts
+++ b/src/oauth/oauth.service.ts
@@ -1,14 +1,39 @@
-import { AuthService } from './../auth/auth.service';
-import { UserService } from './../users/user.service';
 import { BadRequestException, Injectable, Logger } from '@nestjs/common';
-import axios from 'axios';
-import { GoogleUserInfo } from './dtos/google.dto';
-import { URL } from 'url';
 import { InjectDataSource } from '@nestjs/typeorm';
-import { DataSource } from 'typeorm';
+import axios from 'axios';
+import { JWT } from 'src/auth/dtos/jwt.dto';
 import { User } from 'src/users/entities/user.entity';
+import { DataSource } from 'typeorm';
+import { URL } from 'url';
+import { AuthService } from './../auth/auth.service';
+import { UserService } from './../users/user.service';
+import { GoogleUserInfo } from './dtos/google.dto';
 import { ServiceProvider } from './dtos/service-provider.dto';
-import { JWT } from 'src/auth/dtos/jwt.dto';
+
+export interface KakaoUser {
+  id: number;
+  connected_at: string;
+  properties: Properties;
+  kakao_account: KakaoAccount;
+}
+
+export interface Properties {
+  nickname: string;
+}
+
+export interface KakaoAccount {
+  profile_nickname_needs_agreement: boolean;
+  profile: Profile;
+  has_email: boolean;
+  email_needs_agreement: boolean;
+  is_email_valid: boolean;
+  is_email_verified: boolean;
+  email: string;
+}
+
+export interface Profile {
+  nickname: string;
+}
 
 @Injectable()
 export class OauthService {
@@ -90,11 +115,6 @@ export class OauthService {
   }
   async userFromKakao(code: string): Promise<JWT> {
     try {
-      const form = new FormData();
-      form.append('client_id', process.env.KAKAO_CLIENT_ID);
-      form.append('redirect_uri', process.env.KAKAO_REDIRECT_URL);
-      form.append('grant_type', 'authorization_code');
-      form.append('code', code);
       const response = await axios.post<{
         access_token: string;
         token_type: string;
@@ -102,18 +122,27 @@ export class OauthService {
         expires_in: number;
         scope: string;
         refresh_token_expires_in: number;
-      }>('https://kauth.kakao.com/oauth/token	', form, {
-        headers: {
-          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+      }>(
+        'https://kauth.kakao.com/oauth/token	',
+        {
+          code,
+          grant_type: 'authorization_code',
+          client_id: process.env.KAKAO_CLIENT_ID,
+          redirect_uri: process.env.KAKAO_REDIRECT_URL,
         },
-      });
+        {
+          headers: {
+            'Content-Type': 'application/x-www-form-urlencoded',
+          },
+        },
+      );
 
       if (!response.data['access_token']) {
         throw new BadRequestException('Access-Token을 받아오지 못 했습니다.');
       }
       // 회원정보 가져오기
       const userUrl = 'https://kapi.kakao.com/v2/user/me';
-      const userResponse = await axios.get(userUrl, {
+      const userResponse = await axios.get<KakaoUser>(userUrl, {
         params: {
           access_token: response.data['access_token'],
         },
@@ -121,17 +150,19 @@ export class OauthService {
           'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
         },
       });
-
-      // const googleUesr = userResponse.data as GoogleUserInfo;
-      return userResponse.data;
-      // let user = await this.userService.findByUserEmail(googleUesr.email);
-      // if (!user) {
-      //   user = await this.userService.create({ email: googleUesr.email });
-      // }
-      // const token = this.authService.sign(user.id);
-      // user.refresh = token.refresh;
-      // await this.dataSource.getRepository(User).save(user);
-      // return new JWT(token);
+      const kakaoUser = userResponse.data;
+      let user = await this.userService.findByUserEmail(
+        kakaoUser.kakao_account.email,
+      );
+      if (!user) {
+        user = await this.userService.create({
+          email: kakaoUser.kakao_account.email,
+        });
+      }
+      const token = this.authService.sign(user.id);
+      user.refresh = token.refresh;
+      await this.dataSource.getRepository(User).save(user);
+      return new JWT(token);
     } catch (err) {
       this.logger.error(err.message);
       throw new BadRequestException('invalid request: ' + err?.message || '');

From 10d31e2d04392ae4a57bb693fb5865f6f84759be Mon Sep 17 00:00:00 2001
From: Raymond <akdfhr2@gmail.com>
Date: Sun, 31 Dec 2023 17:30:14 +0900
Subject: [PATCH 5/5] 0.2.5

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 28bc77d..4ad8601 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "shinnyang",
-  "version": "0.2.4",
+  "version": "0.2.5",
   "description": "",
   "author": {
     "name": "Medici",