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

[#19] 사용자 profile 이미지 업로드 #30

Open
wants to merge 2 commits into
base: feature/15-email-verify-code-access-token-guard
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ lerna-debug.log*
*.env*
# mysql
mysql-data

# uploads
uploads/
Comment on lines +44 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EOL

36 changes: 33 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"class-validator": "^0.14.1",
"cross-env": "^7.0.3",
"express": "^4.21.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.11.4",
"nodemailer": "^6.9.16",
"reflect-metadata": "^0.2.2",
Expand All @@ -43,6 +44,7 @@
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.9.0",
"@types/nodemailer": "^6.4.17",
"@types/supertest": "^6.0.2",
Expand Down
89 changes: 89 additions & 0 deletions src/controllers/members.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
NotFoundException,
Param,
ParseIntPipe,
Patch,
UploadedFile,
UseInterceptors,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import * as bcrypt from "bcrypt";

import { CurrentMemberDecorator } from "@APP/common/decorators/current-member.decorator";
import { UpdateMemberDto } from "@APP/dtos/update-member.dto";
import { MembersService } from "@APP/services/members.service";

@Controller("members")
export class MembersController {
constructor(private readonly membersService: MembersService) {}

@Get(":memberId")
async getMember(@Param("memberId", new ParseIntPipe()) memberId: number) {
return await this.membersService.findById(memberId);
}

@Delete(":memberId")
async deleteMember(
@CurrentMemberDecorator("id") currentMemberId: number,
@Param("memberId", new ParseIntPipe()) memberId: number,
) {
const member = await this.membersService.findById(memberId);

if (!member) {
throw new NotFoundException("존재하지 않는 회원입니다.");
}

if (currentMemberId !== memberId) {
throw new ForbiddenException("권한이 없습니다.");
}

await this.membersService.deleteById(memberId);
}

@Patch(":memberId")
async patchMember(
@CurrentMemberDecorator("id") currentMemberId: number,
@Param("memberId", new ParseIntPipe()) memberId: number,
@Body() dto: UpdateMemberDto,
) {
const member = await this.membersService.findById(memberId);

if (!member) {
throw new NotFoundException("존재하지 않는 회원입니다.");
}

if (currentMemberId !== memberId) {
throw new ForbiddenException("권한이 없습니다.");
}

if (dto.password) {
const hashedPassword = await bcrypt.hash(dto.password, 10);
dto.password = hashedPassword;
}

await this.membersService.updateById(memberId, dto);

return await this.membersService.findById(memberId);
}

@Patch(":memberId/profile-image")
@UseInterceptors(FileInterceptor("image"))
async patchProfileImage(
@CurrentMemberDecorator("id") currentMemberId: number,
@UploadedFile() file: Express.Multer.File,
) {
await this.membersService.updateProfileImage(
currentMemberId,
file.filename,
);

return {
filename: file.filename,
};
}
}
7 changes: 7 additions & 0 deletions src/dtos/update-member.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { OmitType, PartialType } from "@nestjs/swagger";

import { RegisterMemberDto } from "./register-member.dto";

export class UpdateMemberDto extends PartialType(
OmitType(RegisterMemberDto, ["email"]),
) {}
7 changes: 6 additions & 1 deletion src/entities/member.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
import { Exclude } from "class-transformer";
import { IsEmail, IsNotEmpty, IsString, Length } from "class-validator";
import {
Column,
CreateDateColumn,
Expand All @@ -24,6 +25,10 @@ export class MemberEntity {

@IsNotEmpty()
@IsString()
@Exclude({
toPlainOnly: true,
})
@Length(4, 20)
@Column({ type: "varchar", length: 60, nullable: false })
password!: string;

Expand Down
2 changes: 1 addition & 1 deletion src/modules/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import { MembersModule } from "./members.module";
imports: [JwtModule.register({}), MembersModule],
controllers: [AuthController],
providers: [AuthService, MailsService],
exports: [],
exports: [AuthService],
})
export class AuthModule {}
39 changes: 36 additions & 3 deletions src/modules/members.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import { Module } from "@nestjs/common";
import { BadRequestException, Module } from "@nestjs/common";
import { MulterModule } from "@nestjs/platform-express";
import { TypeOrmModule } from "@nestjs/typeorm";
import multer from "multer";
import { extname } from "path";

import { MembersController } from "@APP/controllers/members.controller";
import { MemberEntity } from "@APP/entities/member.entity";
import { MembersRepository } from "@APP/repositories/members.repository";
import { MembersService } from "@APP/services/members.service";

@Module({
imports: [TypeOrmModule.forFeature([MemberEntity])],
controllers: [],
imports: [
TypeOrmModule.forFeature([MemberEntity]),
MulterModule.register({
limits: {
fileSize: 10000000,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정확한 단위를 알기 쉽게 표현하는 게 어떨까요?

},
fileFilter: (_req, file, cb) => {
const ext = extname(file.originalname);
if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") {
return cb(
new BadRequestException(
"jpg/jpeg/png 파일만 업로드 가능합니다",
),
false,
);
}

return cb(null, true);
},

storage: multer.diskStorage({
destination: function (_req, _file, cb) {
cb(null, "uploads/");
},
filename: function (_req, file, cb) {
cb(null, `${Date.now()}-${file.originalname}`);
},
}),
}),
],
controllers: [MembersController],
providers: [MembersService, MembersRepository],
exports: [MembersService],
})
Expand Down
31 changes: 31 additions & 0 deletions src/services/members.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BadRequestException, Injectable } from "@nestjs/common";

import { RegisterMemberDto } from "@APP/dtos/register-member.dto";
import { UpdateMemberDto } from "@APP/dtos/update-member.dto";
import { VerifyEmailDto } from "@APP/dtos/verify-email.dto";
import { MembersRepository } from "@APP/repositories/members.repository";

Expand All @@ -16,6 +17,29 @@ export class MembersService {
});
}

async findById(memberId: number) {
return this.membersRepository.findOne({
where: {
id: memberId,
},
});
}

async updateById(memberId: number, dto: UpdateMemberDto) {
return this.membersRepository.update(
{
id: memberId,
},
dto,
);
}

async deleteById(memberId: number) {
return this.membersRepository.delete({
id: memberId,
});
}

async verifyEmail(dto: VerifyEmailDto) {
const verifyCodeExists = await this.membersRepository.exists({
where: {
Expand Down Expand Up @@ -77,4 +101,11 @@ export class MembersService {
},
);
}

async updateProfileImage(memberId: number, filename: string) {
return await this.membersRepository.update(
{ id: memberId },
{ profileImage: filename },
);
}
}