From 2dc017273dfd7d2fdf5f84db28ffa973d0a60aec Mon Sep 17 00:00:00 2001
From: lsh23 <shseoul14@gmail.com>
Date: Fri, 8 Dec 2023 01:16:45 +0900
Subject: [PATCH 1/5] =?UTF-8?q?[BE]=20feat:=20avatar=20holder=EB=A5=BC=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 BE/src/auth/application/avatar.holder.spec.ts | 23 +++++++++++++++++++
 BE/src/auth/application/avatar.holder.ts      | 20 ++++++++++++++++
 BE/src/auth/auth.module.ts                    |  2 ++
 3 files changed, 45 insertions(+)
 create mode 100644 BE/src/auth/application/avatar.holder.spec.ts
 create mode 100644 BE/src/auth/application/avatar.holder.ts

diff --git a/BE/src/auth/application/avatar.holder.spec.ts b/BE/src/auth/application/avatar.holder.spec.ts
new file mode 100644
index 00000000..e1d90202
--- /dev/null
+++ b/BE/src/auth/application/avatar.holder.spec.ts
@@ -0,0 +1,23 @@
+import { ConfigService } from '@nestjs/config';
+import { AvatarHolder } from './avatar.holder';
+
+describe('AvatarHolder Test', () => {
+  test('기본 avatarUrl을 환경변수로 부터 가져온다.', () => {
+    //given
+    //when
+    const configService = new ConfigService({
+      USER_AVATAR_URLS: 'url1,url2,url3,url4,url5',
+    });
+    const avatarHolder: AvatarHolder = new AvatarHolder(configService);
+
+    //then
+    expect(avatarHolder.getUrls()).toEqual([
+      'url1',
+      'url2',
+      'url3',
+      'url4',
+      'url5',
+    ]);
+    expect(avatarHolder.getUrls()).toContain(avatarHolder.getUrl());
+  });
+});
diff --git a/BE/src/auth/application/avatar.holder.ts b/BE/src/auth/application/avatar.holder.ts
new file mode 100644
index 00000000..005340d2
--- /dev/null
+++ b/BE/src/auth/application/avatar.holder.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+
+@Injectable()
+export class AvatarHolder {
+  private readonly defaultAvatarUrls: string[];
+  constructor(configService: ConfigService) {
+    const rawAvatarUrls = configService.get<string>('USER_AVATAR_URLS');
+    this.defaultAvatarUrls = rawAvatarUrls.split(',');
+  }
+
+  getUrls() {
+    return this.defaultAvatarUrls;
+  }
+
+  getUrl() {
+    const idx = Math.floor(Math.random() * this.defaultAvatarUrls.length);
+    return this.defaultAvatarUrls[idx];
+  }
+}
diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts
index 77c4865d..2551092d 100644
--- a/BE/src/auth/auth.module.ts
+++ b/BE/src/auth/auth.module.ts
@@ -14,6 +14,7 @@ import { AccessTokenGuard } from './guard/access-token.guard';
 import { CacheModule } from '@nestjs/cache-manager';
 import { redisModuleOptions } from '../config/redis';
 import type { RedisClientOptions } from 'redis';
+import { AvatarHolder } from './application/avatar.holder';
 
 @Global()
 @Module({
@@ -32,6 +33,7 @@ import type { RedisClientOptions } from 'redis';
     JwtUtils,
     UserCodeGenerator,
     AccessTokenGuard,
+    AvatarHolder,
   ],
   exports: [JwtUtils, AccessTokenGuard],
 })

From 84d9526220bf6fbdb5e9b0eac4f385d66cb4926b Mon Sep 17 00:00:00 2001
From: lsh23 <shseoul14@gmail.com>
Date: Fri, 8 Dec 2023 01:18:40 +0900
Subject: [PATCH 2/5] =?UTF-8?q?[BE]=20feat:=20=ED=9A=8C=EC=9B=90=EC=9D=B4?=
 =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=EB=90=98=EB=8A=94=20=EC=8B=9C=EC=A0=90?=
 =?UTF-8?q?=EC=97=90=20avatar=EC=9D=B4=20=EC=9E=90=EB=8F=99=EC=9C=BC?=
 =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EC=A0=95=EB=90=9C=EB=8B=A4.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 BE/src/auth/application/auth.service.spec.ts | 4 ++++
 BE/src/auth/application/auth.service.ts      | 3 +++
 BE/src/config/config/index.ts                | 1 +
 BE/src/users/domain/user.domain.ts           | 4 ++++
 4 files changed, 12 insertions(+)

diff --git a/BE/src/auth/application/auth.service.spec.ts b/BE/src/auth/application/auth.service.spec.ts
index 80f39dca..7b50d9a0 100644
--- a/BE/src/auth/application/auth.service.spec.ts
+++ b/BE/src/auth/application/auth.service.spec.ts
@@ -20,6 +20,7 @@ import { Cache } from 'cache-manager';
 import { RefreshTokenNotFoundException } from '../exception/refresh-token-not-found.exception';
 import { AuthTestModule } from '../../../test/auth/auth-test.module';
 import { anyString, instance, mock, when } from 'ts-mockito';
+import { AvatarHolder } from './avatar.holder';
 
 describe('AuthService', () => {
   let authService: AuthService;
@@ -41,6 +42,7 @@ describe('AuthService', () => {
       ],
       providers: [
         AuthService,
+        AvatarHolder,
         OauthHandler,
         OauthRequester,
         JwtUtils,
@@ -76,6 +78,8 @@ describe('AuthService', () => {
       response.user.userCode,
     );
     expect(response.user).toBeDefined();
+    expect(response.user.userCode).toBeDefined();
+    expect(response.user.avatarUrl).toBeDefined();
     expect(response.accessToken).toBeDefined();
     expect(response.refreshToken).toBeDefined();
     expect(response.refreshToken).toBeDefined();
diff --git a/BE/src/auth/application/auth.service.ts b/BE/src/auth/application/auth.service.ts
index d00796d9..ee2d8955 100644
--- a/BE/src/auth/application/auth.service.ts
+++ b/BE/src/auth/application/auth.service.ts
@@ -14,6 +14,7 @@ import { RefreshAuthResponseDto } from '../dto/refresh-auth-response.dto';
 import { RefreshTokenNotFoundException } from '../exception/refresh-token-not-found.exception';
 import { CACHE_MANAGER } from '@nestjs/cache-manager';
 import { Cache } from 'cache-manager';
+import { AvatarHolder } from './avatar.holder';
 
 @Injectable()
 export class AuthService {
@@ -23,6 +24,7 @@ export class AuthService {
     private readonly oauthHandler: OauthHandler,
     private readonly userCodeGenerator: UserCodeGenerator,
     private readonly jwtUtils: JwtUtils,
+    private readonly avatarHolder: AvatarHolder,
   ) {}
 
   @Transactional()
@@ -56,6 +58,7 @@ export class AuthService {
     const newUser = User.from(userIdentifier);
     const userCode = await this.userCodeGenerator.generate();
     newUser.assignUserCode(userCode);
+    newUser.assignAvatar(this.avatarHolder.getUrl());
     return await this.usersRepository.saveUser(newUser);
   }
 
diff --git a/BE/src/config/config/index.ts b/BE/src/config/config/index.ts
index e3784680..336e483b 100644
--- a/BE/src/config/config/index.ts
+++ b/BE/src/config/config/index.ts
@@ -50,6 +50,7 @@ export const configServiceModuleOptions = {
     }),
 
     GROUP_AVATAR_URLS: Joi.string().required(),
+    USER_AVATAR_URLS: Joi.string().required(),
 
     REDIS_HOST: Joi.number().when('NODE_ENV', {
       is: 'production',
diff --git a/BE/src/users/domain/user.domain.ts b/BE/src/users/domain/user.domain.ts
index 5ac751de..027f25d7 100644
--- a/BE/src/users/domain/user.domain.ts
+++ b/BE/src/users/domain/user.domain.ts
@@ -20,4 +20,8 @@ export class User {
   assignUserCode(userCode: string) {
     this.userCode = userCode;
   }
+
+  assignAvatar(avatarUrl: string) {
+    this.avatarUrl = avatarUrl;
+  }
 }

From 7bc4d7e457c28cb4a1137d62c1998bb821e04344 Mon Sep 17 00:00:00 2001
From: lsh23 <shseoul14@gmail.com>
Date: Fri, 8 Dec 2023 01:19:19 +0900
Subject: [PATCH 3/5] =?UTF-8?q?[BE]=20feat:=20=EA=B7=B8=EB=A3=B9=20?=
 =?UTF-8?q?=EB=8B=AC=EC=84=B1=20=EA=B8=B0=EB=A1=9D=20=EB=A6=AC=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=EC=9D=98=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=9C=A0?=
 =?UTF-8?q?=EC=A0=80=EC=97=90=20=EB=8C=80=ED=95=9C=20avatar=20url=EC=9D=84?=
 =?UTF-8?q?=20=ED=8F=AC=ED=95=A8=ED=95=9C=EB=8B=A4.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/group-achievement.service.ts  |  4 +--
 .../group-achievement.controller.spec.ts      | 30 +++++++++++++++----
 .../dto/group-achievement-response.ts         | 11 +++----
 .../group-achievement.repository.spec.ts      |  3 +-
 .../entities/group-achievement.repository.ts  |  2 +-
 BE/src/group/achievement/index.ts             | 10 +++++++
 6 files changed, 44 insertions(+), 16 deletions(-)

diff --git a/BE/src/group/achievement/application/group-achievement.service.ts b/BE/src/group/achievement/application/group-achievement.service.ts
index 3ec5e11f..4763bacc 100644
--- a/BE/src/group/achievement/application/group-achievement.service.ts
+++ b/BE/src/group/achievement/application/group-achievement.service.ts
@@ -95,9 +95,7 @@ export class GroupAchievementService {
     );
     return new PaginateGroupAchievementResponse(
       paginateGroupAchievementRequest,
-      achievements.map((achievement) =>
-        GroupAchievementResponse.from(achievement),
-      ),
+      achievements,
     );
   }
 
diff --git a/BE/src/group/achievement/controller/group-achievement.controller.spec.ts b/BE/src/group/achievement/controller/group-achievement.controller.spec.ts
index 640dfe5f..1ae335d6 100644
--- a/BE/src/group/achievement/controller/group-achievement.controller.spec.ts
+++ b/BE/src/group/achievement/controller/group-achievement.controller.spec.ts
@@ -349,21 +349,30 @@ describe('GroupAchievementController', () => {
             {
               id: 6,
               title: 'test6',
-              userCode: 'ABCDEFG',
+              user: {
+                userCode: 'ABCDEF3',
+                avatarUrl: 'avatarUrl1',
+              },
               categoryId: 1,
               thumbnailUrl: 'thumbnail_url6',
             },
             {
               id: 3,
               title: 'test3',
-              userCode: 'ABCDEFG',
+              user: {
+                userCode: 'ABCDEF2',
+                avatarUrl: 'avatarUrl2',
+              },
               categoryId: 1,
               thumbnailUrl: 'thumbnail_url3',
             },
             {
               id: 2,
               title: 'test2',
-              userCode: 'ABCDEFG',
+              user: {
+                userCode: 'ABCDEF1',
+                avatarUrl: 'avatarUrl3',
+              },
               categoryId: 1,
               thumbnailUrl: 'thumbnail_url2',
             },
@@ -394,21 +403,30 @@ describe('GroupAchievementController', () => {
                 id: 6,
                 thumbnailUrl: 'thumbnail_url6',
                 title: 'test6',
-                userCode: 'ABCDEFG',
+                user: {
+                  avatarUrl: 'avatarUrl1',
+                  userCode: 'ABCDEF3',
+                },
               },
               {
                 categoryId: 1,
                 id: 3,
                 thumbnailUrl: 'thumbnail_url3',
                 title: 'test3',
-                userCode: 'ABCDEFG',
+                user: {
+                  avatarUrl: 'avatarUrl2',
+                  userCode: 'ABCDEF2',
+                },
               },
               {
                 categoryId: 1,
                 id: 2,
                 thumbnailUrl: 'thumbnail_url2',
                 title: 'test2',
-                userCode: 'ABCDEFG',
+                user: {
+                  avatarUrl: 'avatarUrl3',
+                  userCode: 'ABCDEF1',
+                },
               },
             ],
             next: {
diff --git a/BE/src/group/achievement/dto/group-achievement-response.ts b/BE/src/group/achievement/dto/group-achievement-response.ts
index f1fe0e78..975c122c 100644
--- a/BE/src/group/achievement/dto/group-achievement-response.ts
+++ b/BE/src/group/achievement/dto/group-achievement-response.ts
@@ -1,5 +1,5 @@
 import { ApiProperty } from '@nestjs/swagger';
-import { IGroupAchievementListDetail } from '../index';
+import { IGroupAchievementListDetail, UserInfo } from '../index';
 
 export class GroupAchievementResponse {
   @ApiProperty({ description: 'id' })
@@ -10,20 +10,20 @@ export class GroupAchievementResponse {
   title: string;
   @ApiProperty({ description: 'categoryId' })
   categoryId: number;
-  @ApiProperty({ description: 'userCode' })
-  userCode: string;
-
+  @ApiProperty({ description: 'user', type: UserInfo })
+  user: UserInfo;
   constructor(
     id: number,
     thumbnailUrl: string,
     title: string,
     userCode: string,
+    avatarUrl: string,
     categoryId: number | null,
   ) {
     this.id = id;
     this.thumbnailUrl = thumbnailUrl;
     this.title = title;
-    this.userCode = userCode;
+    this.user = new UserInfo(userCode, avatarUrl);
     this.categoryId = categoryId ? categoryId : -1;
   }
 
@@ -33,6 +33,7 @@ export class GroupAchievementResponse {
       groupAchievementListDetail.thumbnailUrl,
       groupAchievementListDetail.title,
       groupAchievementListDetail.userCode,
+      groupAchievementListDetail.avatarUrl,
       groupAchievementListDetail.categoryId,
     );
   }
diff --git a/BE/src/group/achievement/entities/group-achievement.repository.spec.ts b/BE/src/group/achievement/entities/group-achievement.repository.spec.ts
index 71f5b0f0..270654d1 100644
--- a/BE/src/group/achievement/entities/group-achievement.repository.spec.ts
+++ b/BE/src/group/achievement/entities/group-achievement.repository.spec.ts
@@ -470,7 +470,8 @@ describe('GroupRepository Test', () => {
       // then
       expect(findAll.length).toEqual(30);
       expect(findAll[0].id).toEqual(last.id);
-      expect(findAll[0].userCode).toEqual(last.user.userCode);
+      expect(findAll[0].user.userCode).toEqual(last.user.userCode);
+      expect(findAll[0].user.avatarUrl).toEqual(last.user.avatarUrl);
       expect(findAll[0].title).toEqual(last.title);
       expect(findAll[0].categoryId).toEqual(last.groupCategory.id);
       expect(findAll[0].thumbnailUrl).toEqual(last.image.thumbnailUrl);
diff --git a/BE/src/group/achievement/entities/group-achievement.repository.ts b/BE/src/group/achievement/entities/group-achievement.repository.ts
index 5a2737a2..c261ece8 100644
--- a/BE/src/group/achievement/entities/group-achievement.repository.ts
+++ b/BE/src/group/achievement/entities/group-achievement.repository.ts
@@ -125,7 +125,7 @@ export class GroupAchievementRepository extends TransactionalRepository<GroupAch
         'user',
         'group_achievement.user_id = user.id',
       )
-      .addSelect(['user.user_code as userCode'])
+      .addSelect(['user.user_code as userCode', 'user.avatar_url as avatarUrl'])
 
       .leftJoin('group_achievement.groupCategory', 'category')
       .addSelect('category.id as categoryId')
diff --git a/BE/src/group/achievement/index.ts b/BE/src/group/achievement/index.ts
index 494894fe..2ec9499b 100644
--- a/BE/src/group/achievement/index.ts
+++ b/BE/src/group/achievement/index.ts
@@ -11,6 +11,7 @@ export interface IGroupAchievementListDetail {
   title: string;
   thumbnailUrl: string;
   userCode: string;
+  avatarUrl: string;
   categoryId: number;
 }
 
@@ -22,3 +23,12 @@ export interface GroupAchievementUpdate {
   imageUrl?: string;
   thumbnailUrl?: string;
 }
+
+export class UserInfo {
+  userCode: string;
+  avatarUrl: string;
+  constructor(userCode: string, avatarUrl: string) {
+    this.userCode = userCode;
+    this.avatarUrl = avatarUrl;
+  }
+}

From 48efb52060b146c0fbb1f54cffda67ffacd1161d Mon Sep 17 00:00:00 2001
From: lsh23 <shseoul14@gmail.com>
Date: Fri, 8 Dec 2023 01:19:47 +0900
Subject: [PATCH 4/5] =?UTF-8?q?[BE]=20fix:=20REDIS=20HOST=20=ED=99=98?=
 =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=EC=97=90=20=EB=8C=80=ED=95=9C=20?=
 =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 BE/src/config/config/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/BE/src/config/config/index.ts b/BE/src/config/config/index.ts
index 336e483b..4aa52875 100644
--- a/BE/src/config/config/index.ts
+++ b/BE/src/config/config/index.ts
@@ -52,7 +52,7 @@ export const configServiceModuleOptions = {
     GROUP_AVATAR_URLS: Joi.string().required(),
     USER_AVATAR_URLS: Joi.string().required(),
 
-    REDIS_HOST: Joi.number().when('NODE_ENV', {
+    REDIS_HOST: Joi.string().when('NODE_ENV', {
       is: 'production',
       then: Joi.required(),
       otherwise: Joi.optional(),

From 54771f2b13285a12a174cf4531165d943f621aa6 Mon Sep 17 00:00:00 2001
From: lsh23 <shseoul14@gmail.com>
Date: Fri, 8 Dec 2023 01:26:56 +0900
Subject: [PATCH 5/5] =?UTF-8?q?[BE]=20fix:=20CI=20&=20CD=EC=97=90=20USER?=
 =?UTF-8?q?=5FAVATAR=5FURLS=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/be.cd.yml | 1 +
 .github/workflows/be.ci.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.github/workflows/be.cd.yml b/.github/workflows/be.cd.yml
index c858641f..0d4159df 100644
--- a/.github/workflows/be.cd.yml
+++ b/.github/workflows/be.cd.yml
@@ -66,6 +66,7 @@ jobs:
           FILESTORE_IMAGE_PREFIX: ${{ secrets.FILESTORE_IMAGE_PREFIX }}
           FILESTORE_THUMBNAIL_PREFIX: ${{ secrets.FILESTORE_THUMBNAIL_PREFIX }}
           GROUP_AVATAR_URLS: ${{ secrets.GROUP_AVATAR_URLS }}
+          USER_AVATAR_URLS: ${{ secrets.USER_AVATAR_URLS }}
           EOF
 
       - name: Install dependencies
diff --git a/.github/workflows/be.ci.yml b/.github/workflows/be.ci.yml
index b8909499..229eec03 100644
--- a/.github/workflows/be.ci.yml
+++ b/.github/workflows/be.ci.yml
@@ -70,6 +70,7 @@ jobs:
           FILESTORE_IMAGE_PREFIX: ${{ secrets.FILESTORE_IMAGE_PREFIX }}
           FILESTORE_THUMBNAIL_PREFIX: ${{ secrets.FILESTORE_THUMBNAIL_PREFIX }}
           GROUP_AVATAR_URLS: ${{ secrets.GROUP_AVATAR_URLS }}
+          USER_AVATAR_URLS: ${{ secrets.USER_AVATAR_URLS }}
           EOF
       - name: Install dependencies
         run: npm ci