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

[Feature] 이동봉사자, 이동봉사 중개 자체 회원가입 API 구현 #23

Merged
merged 7 commits into from
Oct 29, 2023
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ dependencies {
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

tasks.named('test') {
Expand Down
69 changes: 69 additions & 0 deletions src/main/java/com/pawwithu/connectdog/common/s3/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.pawwithu.connectdog.common.s3;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

import static com.pawwithu.connectdog.error.ErrorCode.INVALID_FILE_UPLOAD;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {

private final AmazonS3Client amazonS3Client;

@Value("${cloud.aws.s3.bucket.name}")
private String bucketName;

@Value("${cloud.aws.s3.bucket.url}")
private String defaultUrl;

public String uploadFile(MultipartFile multipartFile, String imageType) {
if (multipartFile == null || multipartFile.isEmpty()) return null;

String savedFileName = getSavedFileName(multipartFile, imageType);
ObjectMetadata metadata = new ObjectMetadata();

try (InputStream inputStream = multipartFile.getInputStream()) {
amazonS3Client.putObject(bucketName, savedFileName, inputStream, metadata);
} catch (IOException e) {
log.error("Failed to upload image", e);
throw new BadRequestException(INVALID_FILE_UPLOAD);
}
return getResourceUrl(savedFileName);
}

public void deleteFile(String fileUrl) {
String fileName = getFileNameFromResourceUrl(fileUrl);
amazonS3Client.deleteObject(new DeleteObjectRequest(bucketName, fileName));
}

// imageType: volunteer -> "volunteer"
private String getSavedFileName(MultipartFile multipartFile, String imageType) {
return String.format("%s/%s-%s",
imageType, getRandomUUID(), multipartFile.getOriginalFilename());
}

private String getRandomUUID() {
return UUID.randomUUID().toString().replace("-", "");
}

private String getResourceUrl(String savedFileName) {
return amazonS3Client.getResourceUrl(bucketName, savedFileName);
}

private String getFileNameFromResourceUrl(String fileUrl) {
return fileUrl.replace(defaultUrl + "/", "");
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/pawwithu/connectdog/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pawwithu.connectdog.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pawwithu.connectdog.domain.auth.controller;

import com.pawwithu.connectdog.domain.auth.dto.request.EmailRequest;
import com.pawwithu.connectdog.domain.auth.dto.request.IntermediarySignUpRequest;
import com.pawwithu.connectdog.domain.auth.dto.request.VolunteerSignUpRequest;
import com.pawwithu.connectdog.domain.auth.dto.response.EmailResponse;
import com.pawwithu.connectdog.domain.auth.service.AuthService;
Expand All @@ -13,10 +14,13 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "Sign-Up", description = "Sign-Up API")
@RestController
Expand All @@ -26,15 +30,31 @@ public class SignUpController {
private final AuthService authService;
private final EmailService emailService;

@Operation(summary = "자체 회원가입", description = "이메일을 사용해 회원가입을 합니다.",
responses = {@ApiResponse(responseCode = "204", description = "자체 회원가입 성공")
@Operation(summary = "이동봉사자 자체 회원가입", description = "이동봉사자 자체 회원가입을 합니다.",
responses = {@ApiResponse(responseCode = "204", description = "이동봉사자 자체 회원가입 성공")
, @ApiResponse(responseCode = "400"
, description = "1. 이미 존재하는 이메일입니다. \t\n 2. 이미 존재하는 사용자 닉네임입니다."
, description = "V1, 이메일 형식에 맞지 않습니다. \t\n V1, 이메일은 필수 입력 값입니다. \t\n V1, 영문+숫자 10자 이상 또는 영문+숫자+특수기호 8자 이상을 입력해 주세요. \t\n " +
"V1, 닉네임은 한글, 숫자만 사용가능합니다. \t\n V1, 닉네임은 필수 입력 값입니다. \t\n V1, 2~10자의 닉네임이어야 합니다 \t\n A1, 이미 존재하는 이메일입니다. \t\n A2, 이미 존재하는 사용자 닉네임입니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("volunteers/sign-up")
public ResponseEntity<Void> signUp(@RequestBody VolunteerSignUpRequest volunteerSignUpRequest) {
authService.signUp(volunteerSignUpRequest);
@PostMapping("/volunteers/sign-up")
public ResponseEntity<Void> volunteerSignUp(@RequestBody @Valid VolunteerSignUpRequest request) {
authService.volunteerSignUp(request);
return ResponseEntity.noContent().build();
}

@Operation(summary = "이동봉사 중개 자체 회원가입", description = "이동봉사 중개 자체 회원가입을 합니다.",
responses = {@ApiResponse(responseCode = "204", description = "이동봉사 중개 자체 회원가입 성공")
, @ApiResponse(responseCode = "400"
, description = "V1, 이메일 형식에 맞지 않습니다. \t\n V1, 이메일은 필수 입력 값입니다. \t\n " +
"V1, 영문+숫자 10자 이상 또는 영문+숫자+특수기호 8자 이상을 입력해 주세요. \t\n V1, url 형식을 입력해 주세요. \t\n " +
"A1, 이미 존재하는 이메일입니다. \t\n F1, 파일이 존재하지 않습니다. \t\n F2, 파일 업로드에 실패했습니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping(value = "/intermediaries/sign-up", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<Void> intermediarySignUp(@RequestPart @Valid IntermediarySignUpRequest request,
@RequestPart(name = "file", required = false) MultipartFile file) {
authService.intermediarySignUp(request, file);
return ResponseEntity.noContent().build();
}

Expand All @@ -45,8 +65,8 @@ public ResponseEntity<Void> signUp(@RequestBody VolunteerSignUpRequest volunteer
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping(value = {"/volunteers/sign-up/email", "/intermediaries/sign-up/email"})
public ResponseEntity<EmailResponse> mailConfirm(@RequestBody @Valid EmailRequest emailRequest){
EmailResponse emailResponse = emailService.sendEmail(emailRequest);
public ResponseEntity<EmailResponse> mailConfirm(@RequestBody @Valid EmailRequest request){
EmailResponse emailResponse = emailService.sendEmail(request);
return ResponseEntity.ok(emailResponse);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.pawwithu.connectdog.domain.auth.dto.request;

import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.entity.IntermediaryRole;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record IntermediarySignUpRequest(@Email(message="이메일 형식에 맞지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.") String email,
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{10,}$|^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=?]).{8,}$",
message = "영문+숫자 10자 이상 또는 영문+숫자+특수기호 8자 이상을 입력해 주세요.") String password,
String name,
@Pattern(regexp = "^(http|https)://[a-zA-Z0-9-.]+\\.[a-zA-Z]{2,}(/\\S*)?$",
message = "url 형식을 입력해 주세요.") String url,
Boolean isOptionAgr) {

public static Intermediary toEntity(IntermediarySignUpRequest request, String authImage) {
return Intermediary.builder()
.email(request.email)
.password(request.password)
.name(request.name)
.url(request.url)
.authImage(authImage)
.isOptionAgr(request.isOptionAgr)
.role(IntermediaryRole.INTERMEDIARY)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import com.pawwithu.connectdog.domain.volunteer.entity.VolunteerRole;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record VolunteerSignUpRequest(String email, String password, String nickname, Boolean isOptionAgr) {
public record VolunteerSignUpRequest(@Email(message="이메일 형식에 맞지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.") String email,
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{10,}$|^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=?]).{8,}$",
message = "영문+숫자 10자 이상 또는 영문+숫자+특수기호 8자 이상을 입력해 주세요.") String password,
@Pattern(regexp = "^[가-힣0-9]*$", message = "닉네임은 한글, 숫자만 사용가능합니다.")
@NotBlank(message = "닉네임은 필수 입력 값입니다.")
@Size(min=2, max=10, message = "2~10자의 닉네임이어야 합니다.") String nickname, Boolean isOptionAgr) {

public Volunteer toEntity() {
return Volunteer.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.pawwithu.connectdog.domain.auth.service;

import com.pawwithu.connectdog.common.s3.FileService;
import com.pawwithu.connectdog.domain.auth.dto.request.IntermediarySignUpRequest;
import com.pawwithu.connectdog.domain.auth.dto.request.VolunteerSignUpRequest;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import com.pawwithu.connectdog.domain.volunteer.repository.VolunteerRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
Expand All @@ -9,9 +13,9 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import static com.pawwithu.connectdog.error.ErrorCode.ALREADY_EXIST_EMAIL;
import static com.pawwithu.connectdog.error.ErrorCode.ALREADY_EXIST_NICKNAME;
import static com.pawwithu.connectdog.error.ErrorCode.*;

@Slf4j
@Service
Expand All @@ -20,20 +24,34 @@
public class AuthService {

private final VolunteerRepository volunteerRepository;
private final IntermediaryRepository intermediaryRepository;
private final PasswordEncoder passwordEncoder;
private final FileService fileService;

public void signUp(VolunteerSignUpRequest volunteerSignUpRequest) {
public void volunteerSignUp(VolunteerSignUpRequest request) {

if (volunteerRepository.existsByEmail(volunteerSignUpRequest.email())) {
if (volunteerRepository.existsByEmail(request.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
if (volunteerRepository.existsByNickname(volunteerSignUpRequest.nickname())) {
if (volunteerRepository.existsByNickname(request.nickname())) {
throw new BadRequestException(ALREADY_EXIST_NICKNAME);
}

Volunteer volunteer = volunteerSignUpRequest.toEntity();
Volunteer volunteer = request.toEntity();
volunteer.passwordEncode(passwordEncoder);
volunteerRepository.save(volunteer);
}

public void intermediarySignUp(IntermediarySignUpRequest request, MultipartFile file) {
if (intermediaryRepository.existsByEmail(request.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
String authImage = fileService.uploadFile(file, "intermediary");
if (authImage == null) {
throw new BadRequestException(FILE_NOT_FOUND);
}
Intermediary intermediary = IntermediarySignUpRequest.toEntity(request, authImage);
intermediary.passwordEncode(passwordEncoder);
intermediaryRepository.save(intermediary);
}
Copy link
Member

Choose a reason for hiding this comment

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

request, file 한 번에 받는 형식 잘 구현한 것 같습니다 👍

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,17 @@ private MimeMessage createEmailForm(String email) throws MessagingException, Uns
/**
* 메일 전송
*/
public EmailResponse sendEmail(EmailRequest emailRequest) throws BadRequestException {
public EmailResponse sendEmail(EmailRequest request) throws BadRequestException {
// 이메일 중복 검사
if (volunteerRepository.existsByEmail(emailRequest.email())) {
if (volunteerRepository.existsByEmail(request.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
if (intermediaryRepository.existsByEmail(emailRequest.email())) {
if (intermediaryRepository.existsByEmail(request.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
try{
// 메일전송에 필요한 정보 설정
MimeMessage emailForm = createEmailForm(emailRequest.email());
MimeMessage emailForm = createEmailForm(request.email());
Copy link
Member

Choose a reason for hiding this comment

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

보니까 request dto 파라미터명을 request로 전부 수정했네요.
장단점이 있지만 저는 이렇게 쓰는 거 좋아합니다 ㅎ.ㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

테스트 코드를 작성하다가 들었던 생각인데! dto에 넣을 request 이름을 개별 메소드마다 다르게 지정할 때 변경해야 하는 번거로움이 있더라고요! request 앞에는 사용할 Dto record의 이름이 들어가니까 괜찮다고 판단해서 바꿨습니다!

emailSender.send(emailForm);
return new EmailResponse(authNum);
}catch (UnsupportedEncodingException | MessagingException e){
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/pawwithu/connectdog/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ public enum ErrorCode {

TOKEN_NOT_EXIST("T1", "토큰이 존재하지 않습니다."),
TOKEN_IS_EXPIRED("T2", "만료된 토큰입니다."),
INVALID_TOKEN("T3", "잘못된 토큰입니다.");
INVALID_TOKEN("T3", "잘못된 토큰입니다."),

FILE_NOT_FOUND("F1", "파일이 존재하지 않습니다."),
INVALID_FILE_UPLOAD("F2", "파일 업로드에 실패했습니다.");


private final String code;
Expand Down
Loading