Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: s3 image 업로드 로직 수정 #116

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ export class AuthController {
const nickname = signupRequestDto.nickname;
const avatar = signupRequestDto.avatar;

await this.authService.signup(user.id, nickname, avatar);
const preSignedUrl = await this.authService.signup(
user.id,
nickname,
avatar,
);

return { preSignedUrl: preSignedUrl };
}

// TODO: 테스트용 코드. 추후 삭제
Expand Down
30 changes: 24 additions & 6 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,40 @@ export class AuthService {

private readonly logger = new Logger(AuthService.name);

async signup(userId: number, nickname: string, avatar: string) {
async signup(userId: number, nickname: string, hasAvatar: boolean) {
const user = await this.validateUserExist(userId);

await this.validateUserAlreadySignUp(user);

await this.validateNickname(nickname);

let ret;
let updateUserDataDto;
if (hasAvatar) {
const { avatar, preSignedUrl } =
await this.usersRepository.getAvatarAndPresignedUrl(nickname);
updateUserDataDto = {
nickname: nickname,
avatar: avatar,
};
ret = preSignedUrl;
} else {
updateUserDataDto = {
nickname: nickname,
};
ret = null;
}

const updateRes = await this.usersRepository.update(userId, {
avatar: avatar,
nickname: nickname,
status: UserStatus.ONLINE,
...updateUserDataDto,
});

if (updateRes.affected !== 1) {
throw DBUpdateFailureException(`user ${userId} update failed`);
throw DBUpdateFailureException(
`유저 ${userId}의 db 업데이트가 실패했습니다`,
);
}

return ret;
}

async validateUserExist(userId: number) {
Expand Down
3 changes: 1 addition & 2 deletions src/auth/dto/signup-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { ApiProperty } from '@nestjs/swagger';
export class SignupRequestDto {
@ApiProperty({ description: '아바타' })
@IsNotEmpty()
@IsString()
avatar: string;
avatar: boolean;
@ApiProperty({ description: '닉네임' })
@IsNotEmpty()
@IsString()
Expand Down
30 changes: 15 additions & 15 deletions src/channels/dto/channel-Invitation-list-return.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { ApiProperty } from '@nestjs/swagger';
import { ChannelUserType } from 'src/common/enum';

export class ChannelInvitationListDto {
@ApiProperty({ description: '채널유저id' })
channelUserId: number;
@ApiProperty({ description: '유저id' })
userId : number;
@ApiProperty({ description: '유저닉네임' })
nickname : string;
@ApiProperty({ description: '유저아바타' })
avatar : string;
@ApiProperty({ description: '친구여부' })
isFriend : boolean;
@ApiProperty({ description: '차단여부' })
isBlocked : boolean;
@ApiProperty({ description: '채널유저타입' })
channelUserType : ChannelUserType;
}
@ApiProperty({ description: '채널유저id' })
channelUserId: number;
@ApiProperty({ description: '유저id' })
userId: number;
@ApiProperty({ description: '유저닉네임' })
nickname: string;
@ApiProperty({ description: '유저아바타' })
avatar: string | null;
@ApiProperty({ description: '친구여부' })
isFriend: boolean;
@ApiProperty({ description: '차단여부' })
isBlocked: boolean;
@ApiProperty({ description: '채널유저타입' })
channelUserType: ChannelUserType;
}
2 changes: 1 addition & 1 deletion src/channels/dto/channel-user-info-return.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type ChannelUserInfoReturnDto = {
channelUserId: number;
userId: number;
nickname: string;
avatar: string;
avatar: string | null;
isFriend: boolean;
isBlocked: boolean;
channelUserType: ChannelUserType;
Expand Down
10 changes: 5 additions & 5 deletions src/channels/dto/dmchannel-list-return.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class DmChannelListReturnDto {
channelId: number;
partnerName: string;
status: string;
avatar: string;
}
channelId: number;
partnerName: string;
status: string;
avatar: string | null;
}
7 changes: 7 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ export const CHANNEL_NAME_REGEXP = /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9]*$/;
/* If K is of a lower value, then the rating is changed by a small fraction
but if K is of a higher value, then the changes in the rating are significant.*/
export const K = 30;

export const S3_IMAGE_URL_PREFIX = 'https://d5xph0h5q8hbn.cloudfront.net';

export const S3_OBJECT_KEY_PREFIX = 'image';
export const S3_OBJECT_KEY_SUFFIX = '.jpeg';

export const S3_OBJECT_CONTENTTYPE = 'image/jpeg';
3 changes: 1 addition & 2 deletions src/friends/dto/block-user-return.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ export class BlockUserReturnDto {
@ApiProperty({ description: '닉네임' })
nickname: string;
@ApiProperty({ description: '아바타' })
@IsNotEmpty()
avatar: string;
avatar: string | null;
@ApiProperty({ description: '상태(온라인 / 오프라인 / 인게임)' })
status: string;
}
2 changes: 1 addition & 1 deletion src/friends/dto/friend-user-return.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type FriendUserReturnDto = {
id: number;
nickname: string;
avatar: string;
avatar: string | null;
status: string;
};
2 changes: 1 addition & 1 deletion src/game/dto/emit-event-match-end-param.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GameType } from '../../common/enum';
export class EmitEventMatchEndParamDto {
gameType: GameType;
rivalName: string;
rivalAvatar: string;
rivalAvatar: string | null;
rivalScore: number | null;
myScore: number | null;
isWin: boolean | null;
Expand Down
2 changes: 1 addition & 1 deletion src/game/dto/emit-event-server-game-ready-param.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class EmitEventServerGameReadyParamDto {
rivalNickname: string;
rivalAvatar: string;
rivalAvatar: string | null;
myPosition: string;
ball: {
x: number;
Expand Down
2 changes: 1 addition & 1 deletion src/user-repository/dto/my-profile-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type MyProfileResponseDto = {
id: number;
nickname: string;
avatar: string;
avatar: string | null;
statusMessage: string | null;
loseCount: number;
winCount: number;
Expand Down
2 changes: 1 addition & 1 deletion src/user-repository/dto/user-profile-return.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type UserProfileReturnDto = {
id: number;
nickname: string;
avatar: string;
avatar: string | null;
statusMessage: string | null;
loseCount: number;
winCount: number;
Expand Down
4 changes: 2 additions & 2 deletions src/user-repository/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export class User extends BaseEntity {
@Matches(NICKNAME_REGEXP)
nickname: string;

@Column({ default: null })
@Column({ default: null, type: 'varchar' })
@IsString()
avatar: string;
avatar: string | null;

@Column()
@IsNotEmpty()
Expand Down
62 changes: 61 additions & 1 deletion src/user-repository/users.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
BadRequestException,
ForbiddenException,
Inject,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Redis from 'ioredis';
import { UserStatus } from 'src/common/enum';
Expand All @@ -8,11 +12,23 @@ import { MyProfileResponseDto } from './dto/my-profile-response.dto';
import { UserProfileReturnDto } from './dto/user-profile-return.dto';
import { User } from './entities/user.entity';
import { RankUserReturnDto } from '../users/dto/rank-user-return.dto';
import s3Config from '../config/s3.config';
import { ConfigType } from '@nestjs/config';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import {
S3_OBJECT_CONTENTTYPE,
S3_OBJECT_KEY_PREFIX,
S3_OBJECT_KEY_SUFFIX,
S3_IMAGE_URL_PREFIX,
} from '../common/constants';

export class UsersRepository extends Repository<User> {
constructor(
@InjectRepository(User) private dataSource: DataSource,
@InjectRedis() private readonly redis: Redis,
@Inject(s3Config.KEY)
private readonly s3Configure: ConfigType<typeof s3Config>,
) {
super(User, dataSource.manager);
}
Expand Down Expand Up @@ -161,4 +177,48 @@ export class UsersRepository extends Repository<User> {

return { rankUsers, totalItemCount };
}

async getAvatarAndPresignedUrl(nickname: string) {
const date = this.getNowDate();
const key = `${S3_OBJECT_KEY_PREFIX}/${nickname}${date}${S3_OBJECT_KEY_SUFFIX}`;
const preSignedUrl = await this.getPresignedUrl(key);
return {
avatar: `${S3_IMAGE_URL_PREFIX}/${key}`,
preSignedUrl: preSignedUrl,
};
}

private async getPresignedUrl(key: string) {
const command = new PutObjectCommand({
Bucket: this.s3Configure.S3_BUCKET_NAME,
Key: key,
ContentType: S3_OBJECT_CONTENTTYPE,
});
return await getSignedUrl(this.s3Configure.S3Object, command, {
expiresIn: 180, //초 단위
});
}

// async deleteS3Image(userId: number) {
// const command = new DeleteObjectCommand({
// Bucket: this.s3Configure.S3_BUCKET_NAME,
// Key: `images/${userId}.jpeg`,
// });
//
// const response = await this.s3Configure.S3Object.send(command);
// // TODO: s3 이미지 삭제에 실패했을 때?
// if (response.$metadata.httpStatusCode !== 200) {
// console.error(response.$metadata.httpStatusCode);
// // throw new InternalServerErrorException();
// }
// }

private getNowDate(): string {
const date = new Date();
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 월은 0부터 시작하므로 +1을 해줌
const day = date.getDate().toString().padStart(2, '0');

return `${year}${month}${day}`;
}
}
2 changes: 1 addition & 1 deletion src/users/dto/rank-user-return.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type RankUserReturnDto = {
nickname: string;
avatar: string;
avatar: string | null;
ladderScore: number;
ranking: number;
};
2 changes: 1 addition & 1 deletion src/users/dto/user-profile-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type UserProfileResponseDto = {
id: number;
nickname: string;
avatar: string;
avatar: string | null;
statusMessage: string | null;
loseCount: number;
winCount: number;
Expand Down
10 changes: 3 additions & 7 deletions src/users/ranks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,11 @@ export class RanksService {
@InjectRedis() private readonly redis: Redis,
) {}

async findRanksWithPage(page: number): Promise<RankUserResponseDto> {
async findRanksWithPage(): Promise<RankUserResponseDto> {
//userIDRanking: [userId, userId, userId, ...]
// 시간 재기
const startTime = new Date().getTime();
const userRanking = await this.redis.zrevrange(
'rankings',
(page - 1) * 15,
page * 15 - 1,
);
const userRanking = await this.redis.zrevrange('rankings', 0, 99);
const endTime = new Date().getTime();
console.log(`redis time: ${endTime - startTime}ms`);

Expand All @@ -34,7 +30,7 @@ export class RanksService {
const rankUsers: RankUserReturnDto[] = foundUsers.map(
(user, index) => ({
...user,
ranking: index + 1 + (page - 1) * 15,
ranking: index + 1,
}),
);
const totalItemCount = userRanking.length;
Expand Down
34 changes: 9 additions & 25 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Logger,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
Expand Down Expand Up @@ -88,9 +86,9 @@ export class UsersController {
summary: '랭킹 조회',
description: '레디스로부터 pagination해 랭킹 목록을 제공합니다.',
})
async paging(@Query('page', ParseIntPipe, PositiveIntPipe) page: number) {
async paging() {
// const rankResponseDto = await this.usersService.findRanksWithPage(); // 레디스 없이 DB에서 랭킹을 조회하는 코드
let rankResponseDto = await this.ranksServices.findRanksWithPage(page); // 레디스로부터 랭킹을 조회하는 코드
let rankResponseDto = await this.ranksServices.findRanksWithPage(); // 레디스로부터 랭킹을 조회하는 코드
if (rankResponseDto.rankUsers.length === 0) {
this.logger.log('랭킹이 없습니다. 랭킹을 추가합니다.');
rankResponseDto = await this.usersService.findRanksWithPage();
Expand Down Expand Up @@ -136,28 +134,14 @@ export class UsersController {
})
async updateMyAvatar(
@GetUser() user: User,
@Body('avatar') avatar: string,
@Body('avatar') avatar: boolean,
) {
await this.usersService.updateMyAvatar(user.id, avatar);
}

@UseGuards(AuthGuard('access'))
@Get('/s3image')
async getPresignedUrl(@GetUser() user: User) {
const presignedUrl = await this.usersService.getPresignedUrl(user.id);

return presignedUrl;
}

@UseGuards(AuthGuard('access'))
@Delete('/s3image')
async deleteAndGetPresignedUrl(@GetUser() user: User) {
const userId = user.id;

await this.usersService.deleteS3Image(userId);

const presignedUrl = await this.usersService.getPresignedUrl(userId);
const preSignedUrl = await this.usersService.updateMyAvatar(
user.id,
user.nickname,
avatar,
);

return presignedUrl;
return { preSignedUrl: preSignedUrl };
}
}
Loading