Skip to content

Commit

Permalink
Merge pull request #224 from boostcampwm2023/develop
Browse files Browse the repository at this point in the history
v0.1 배포
  • Loading branch information
lsh23 authored Nov 23, 2023
2 parents b0e68cb + bdde9cf commit be1567a
Show file tree
Hide file tree
Showing 136 changed files with 4,939 additions and 530 deletions.
53 changes: 31 additions & 22 deletions .github/workflows/be.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,50 @@ name: Motimate BE CI

on:
push:
paths: 'BE/**'
paths:
- 'BE/**'
- '.github/**'
branches: [ "develop", "main" ]
pull_request:
paths: 'BE/**'
paths:
- 'BE/**'
- '.github/**'
branches: [ "develop", "main" ]

jobs:
build:
runs-on: ubuntu-latest
env:
DB: "mysql"
DB_HOST: "127.0.0.1"
DB_PORT: "13306"
DB_USERNAME: "root"
DB_PASSWORD: "1234"
DB_DATABASE: "motimate_test"
DB_ENTITIES: "src/**/*.entity{.ts,.js}"
DB_LOGGING: false
SWAGGER_TITLE: "Motimate"
SWAGGER_DESCRIPTION: "The Motimate API Documents"
SWAGGER_VERSION: "0.1.0"
SWAGGER_TAG: "motimate"
APPLE_PUBLIC_KEY_URL: "https://appleid.apple.com/auth/keys"
JWT_SECRET: "!@sehyeong!@"
JWT_VALIDITY: 3600000
REFRESH_JWT_SECRET: "!@sehyeongrefresh!@"
REFRESH_JWT_VALIDITY: 604800000

DB: ${{ secrets.DB }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_PORT: ${{ secrets.DB_PORT }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_DATABASE: ${{ secrets.DB_DATABASE }}
DB_ENTITIES: ${{ secrets.DB_ENTITIES }}
DB_LOGGING: ${{ secrets.DB_LOGGING }}
SWAGGER_TITLE: ${{ secrets.SWAGGER_TITLE }}
SWAGGER_DESCRIPTION: ${{ secrets.SWAGGER_DESCRIPTION }}
SWAGGER_VERSION: ${{ secrets.SWAGGER_VERSION }}
SWAGGER_TAG: ${{ secrets.SWAGGER_TAG }}
APPLE_PUBLIC_KEY_URL: ${{ secrets.APPLE_PUBLIC_KEY_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
JWT_VALIDITY: ${{ secrets.JWT_VALIDITY }}
REFRESH_JWT_SECRET: ${{ secrets.REFRESH_JWT_SECRET }}
REFRESH_JWT_VALIDITY: ${{ secrets.REFRESH_JWT_VALIDITY }}
LOCAL_BASEPATH: ${{ secrets.LOCAL_BASEPATH }}
NCP_ENDPOINT: ${{ secrets.BCRYPT_SALT }}
NCP_REGION: ${{ secrets.NCP_REGION }}
NCP_ACCESS_KEY_ID: ${{ secrets.NCP_ACCESS_KEY_ID }}
NCP_SECRET_ACCESS_KEY: ${{ secrets.NCP_SECRET_ACCESS_KEY }}
NCP_BUCKET_NAME: ${{ secrets.NCP_BUCKET_NAME }}

services:
mysql:
image: mysql:8.0.34
env:
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: "motimate_test"
MYSQL_ROOT_PASSWORD: ${{ env.DB_PASSWORD }}
MYSQL_DATABASE: ${{ env.DB_DATABASE }}
ports:
- 13306:3306

Expand Down
5 changes: 4 additions & 1 deletion BE/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
</head>
<body>
<p>모티 앱 v0.0 <a href="itms-services://?action=download-manifest&amp;url=https://kr.object.ncloudstorage.com/motimate-deploy/manifest.plist">Download</a></p>
<p>Last updated: 2023-11-16 23:18</p>

<p>모티 앱 v0.1 <a href="itms-services://?action=download-manifest&amp;url=https://kr.object.ncloudstorage.com/motimate-deploy/manifest-0.1.plist">Download</a></p>
<p>Last updated: 2023-11-23 20:02</p>

</body>
</html>
1 change: 1 addition & 0 deletions BE/scripts/down-sampling/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
virtualenv
36 changes: 36 additions & 0 deletions BE/scripts/down-sampling/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os

import boto3
from PIL import Image

service_name: str = 's3'
endpoint_url: str = 'https://kr.object.ncloudstorage.com'


def downsample_image(input_path: str, output_path: str, downsample_size: tuple[int, int] = (500, 500)):
if not os.path.exists(os.path.dirname(output_path)):
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with Image.open(input_path) as img:
downsampled_img: Image.Image = img.resize(downsample_size, Image.Resampling.LANCZOS)
downsampled_img.save(output_path)


def main(args):
bucket_name: str = args['container_name']
object_name: str = args['object_name']

s3: boto3 = boto3.client(
service_name,
endpoint_url=endpoint_url,
aws_access_key_id=args['access_key'],
aws_secret_access_key=args['scret_key'],
)

object_path: str = f"./{object_name}"
thumbnail_path: str = f"./thumbnail/{object_name}"

s3.download_file(bucket_name, object_name, object_path)
downsample_image(object_path, thumbnail_path)
s3.upload_file(thumbnail_path, bucket_name, thumbnail_path)

return args
8 changes: 8 additions & 0 deletions BE/scripts/down-sampling/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
boto3==1.29.5
botocore==1.32.5
jmespath==1.0.1
Pillow==10.1.0
python-dateutil==2.8.2
s3transfer==0.7.0
six==1.16.0
urllib3==2.0.7
12 changes: 12 additions & 0 deletions BE/src/achievement/achievement.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AchievementController } from './controller/achievement.controller';
import { AchievementService } from './application/achievement.service';
import { CustomTypeOrmModule } from '../config/typeorm/custom-typeorm.module';
import { AchievementRepository } from './entities/achievement.repository';

@Module({
imports: [CustomTypeOrmModule.forCustomRepository([AchievementRepository])],
controllers: [AchievementController],
providers: [AchievementService],
})
export class AchievementModule {}
151 changes: 151 additions & 0 deletions BE/src/achievement/application/achievement.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AchievementService } from './achievement.service';
import { CustomTypeOrmModule } from '../../config/typeorm/custom-typeorm.module';
import { AchievementRepository } from '../entities/achievement.repository';
import { ConfigModule } from '@nestjs/config';
import { configServiceModuleOptions } from '../../config/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmModuleOptions } from '../../config/typeorm';
import { UsersTestModule } from '../../../test/user/users-test.module';
import { UsersFixture } from '../../../test/user/users-fixture';
import { CategoryFixture } from '../../../test/category/category-fixture';
import { AchievementFixture } from '../../../test/achievement/achievement-fixture';
import { CategoryTestModule } from '../../../test/category/category-test.module';
import { AchievementTestModule } from '../../../test/achievement/achievement-test.module';
import { PaginateAchievementRequest } from '../dto/paginate-achievement-request';
import { NoSuchAchievementException } from '../exception/no-such-achievement.exception';

describe('AchievementService Test', () => {
let achievementService: AchievementService;
let usersFixture: UsersFixture;
let categoryFixture: CategoryFixture;
let achievementFixture: AchievementFixture;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot(configServiceModuleOptions),
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
CustomTypeOrmModule.forCustomRepository([AchievementRepository]),
UsersTestModule,
CategoryTestModule,
AchievementTestModule,
],
providers: [AchievementService],
}).compile();

achievementService = module.get<AchievementService>(AchievementService);
usersFixture = module.get<UsersFixture>(UsersFixture);
categoryFixture = module.get<CategoryFixture>(CategoryFixture);
achievementFixture = module.get<AchievementFixture>(AchievementFixture);
});

test('개인 달성 기록 리스트에 대한 페이지네이션 조회를 할 수 있다.', async () => {
// given
const user = await usersFixture.getUser('ABC');
const category = await categoryFixture.getCategory(user, 'ABC');
const achievements = [];
for (let i = 0; i < 10; i++) {
achievements.push(
await achievementFixture.getAchievement(user, category),
);
}

// when
const firstRequest = new PaginateAchievementRequest(category.id, 4);
const firstResponse = await achievementService.getAchievements(
user.id,
firstRequest,
);

const nextRequest = new PaginateAchievementRequest(
category.id,
4,
firstResponse.next.whereIdLessThan,
);
const nextResponse = await achievementService.getAchievements(
user.id,
nextRequest,
);

const lastRequest = new PaginateAchievementRequest(
category.id,
4,
nextResponse.next.whereIdLessThan,
);
const lastResponse = await achievementService.getAchievements(
user.id,
lastRequest,
);

expect(firstResponse.count).toEqual(4);
expect(firstResponse.data.length).toEqual(4);
expect(firstResponse.next.whereIdLessThan).toEqual(7);

expect(nextResponse.count).toEqual(4);
expect(nextResponse.data.length).toEqual(4);
expect(nextResponse.next.whereIdLessThan).toEqual(3);

expect(lastResponse.count).toEqual(2);
expect(lastResponse.data.length).toEqual(2);
expect(lastResponse.next).toEqual(null);
});

test('달성 기록 상세정보를 조회를 할 수 있다.', async () => {
// given
const user = await usersFixture.getUser('ABC');
const category = await categoryFixture.getCategory(user, 'ABC');
const achievements = [];
for (let i = 0; i < 10; i++) {
achievements.push(
await achievementFixture.getAchievement(user, category),
);
}
// when
const detail = await achievementService.getAchievementDetail(
user.id,
achievements[7].id,
);

expect(detail.id).toBeDefined();
expect(detail.title).toBeDefined();
expect(detail.content).toBeDefined();
expect(detail.imageUrl).toBeDefined();
expect(detail.category.id).toEqual(category.id);
expect(detail.category.name).toEqual(category.name);
expect(detail.category.achieveCount).toEqual(8);
});

test('자신이 소유하지 않은 달성 기록 정보를 조회하면 NoSuchAchievementException을 던진다.', async () => {
// given
const user = await usersFixture.getUser('ABC');
const category = await categoryFixture.getCategory(user, 'ABC');
const achievements = [];
for (let i = 0; i < 10; i++) {
achievements.push(
await achievementFixture.getAchievement(user, category),
);
}
// when
// then
await expect(
achievementService.getAchievementDetail(user.id + 1, achievements[7].id),
).rejects.toThrow(NoSuchAchievementException);
});

test('유효하지 않은 달성 기록 id를 통해 조회하면 NoSuchAchievementException를 던진다.', async () => {
// given
const user = await usersFixture.getUser('ABC');
const category = await categoryFixture.getCategory(user, 'ABC');
const achievements = [];
for (let i = 0; i < 10; i++) {
achievements.push(
await achievementFixture.getAchievement(user, category),
);
}
// when
// then
await expect(
achievementService.getAchievementDetail(user.id, achievements[9].id + 1),
).rejects.toThrow(NoSuchAchievementException);
});
});
39 changes: 39 additions & 0 deletions BE/src/achievement/application/achievement.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { AchievementRepository } from '../entities/achievement.repository';
import { AchievementResponse } from '../dto/achievement-response';
import { PaginateAchievementRequest } from '../dto/paginate-achievement-request';
import { PaginateAchievementResponse } from '../dto/paginate-achievement-response';
import { AchievementDetailResponse } from '../dto/achievement-detail-response';
import { Transactional } from '../../config/transaction-manager';
import { NoSuchAchievementException } from '../exception/no-such-achievement.exception';

@Injectable()
export class AchievementService {
constructor(private readonly achievementRepository: AchievementRepository) {}
async getAchievements(
userId: number,
paginateAchievementRequest: PaginateAchievementRequest,
) {
const achievements = await this.achievementRepository.findAll(
userId,
paginateAchievementRequest,
);
return new PaginateAchievementResponse(
paginateAchievementRequest,
achievements.map((achievement) => AchievementResponse.from(achievement)),
);
}

@Transactional({ readonly: true })
async getAchievementDetail(userId: number, achievementId: number) {
const achievement: AchievementDetailResponse =
await this.achievementRepository.findAchievementDetail(
userId,
achievementId,
);
if (!achievement) {
throw new NoSuchAchievementException();
}
return achievement;
}
}
Loading

0 comments on commit be1567a

Please sign in to comment.