Skip to content

Commit

Permalink
Merge pull request #77 from HongDam-org/feat/profile-image
Browse files Browse the repository at this point in the history
[FEAT] profile image 업로드 api
  • Loading branch information
ohksj77 authored Dec 12, 2023
2 parents bf85917 + 37be8ac commit 905856c
Show file tree
Hide file tree
Showing 19 changed files with 263 additions and 26 deletions.
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ out/
.vscode/

/src/main/generated/

/src/main/resources/*.json
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'org.springframework.cloud:spring-cloud-gcp-starter:1.2.5.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-gcp-storage:1.2.5.RELEASE'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
Expand Down
9 changes: 9 additions & 0 deletions backend/src/docs/image.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4

== Image
=== 프로필 이미지 업로드
operation::post upload image[snippets='http-request,http-response']
1 change: 1 addition & 0 deletions backend/src/docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ include::place.adoc[]
include::member.adoc[]
include::friend.adoc[]
include::group.adoc[]
include::image.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
KakaoProperties.class,
TmapProperties.class,
RabbitMQProperties.class,
RedisProperties.class
RedisProperties.class,
StorageProperties.class
})
public class PropertiesConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.twtw.backend.config.storage;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import com.twtw.backend.global.properties.StorageProperties;

import lombok.RequiredArgsConstructor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;

@Configuration
@RequiredArgsConstructor
public class StorageConfig {

private final StorageProperties storageProperties;

@Bean
public Storage storage() throws IOException {
final GoogleCredentials googleCredentials =
GoogleCredentials.fromStream(
new ClassPathResource(storageProperties.getCredentialsFile())
.getInputStream());

return StorageOptions.newBuilder().setCredentials(googleCredentials).build().getService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.twtw.backend.domain.friend.exception.InvalidFriendMemberException;
import com.twtw.backend.global.advice.ErrorResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -13,7 +12,6 @@ public class FriendControllerAdvice {

@ExceptionHandler(InvalidFriendMemberException.class)
public ResponseEntity<ErrorResponse> invalidFriendMember(final InvalidFriendMemberException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.twtw.backend.domain.group.exception.IllegalGroupMemberException;
import com.twtw.backend.global.advice.ErrorResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -13,7 +12,6 @@ public class GroupControllerAdvice {

@ExceptionHandler(IllegalGroupMemberException.class)
public ResponseEntity<ErrorResponse> invalidFriendMember(final IllegalGroupMemberException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.twtw.backend.domain.image.controller;

import com.twtw.backend.domain.image.dto.ImageResponse;
import com.twtw.backend.domain.image.service.ImageService;

import lombok.RequiredArgsConstructor;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
@RequiredArgsConstructor
@RequestMapping("images")
public class ImageController {

private final ImageService imageService;

@PostMapping
public ResponseEntity<ImageResponse> uploadImage(final MultipartFile image) throws IOException {
return ResponseEntity.ok(imageService.uploadImage(image));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.twtw.backend.domain.image.controller.advice;

import com.twtw.backend.domain.image.exception.InvalidFileTypeException;
import com.twtw.backend.global.advice.ErrorResponse;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

@RestControllerAdvice
public class ImageControllerAdvice {

@ExceptionHandler(InvalidFileTypeException.class)
public ResponseEntity<ErrorResponse> handleInvalidFileType(final InvalidFileTypeException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(IOException.class)
public ResponseEntity<ErrorResponse> io(final IOException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.twtw.backend.domain.image.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ImageResponse {
private String imageUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.twtw.backend.domain.image.exception;

public class InvalidFileTypeException extends IllegalArgumentException {

private static final String MESSAGE = "파일 형식이 잘못되었습니다.";

public InvalidFileTypeException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.twtw.backend.domain.image.service;

import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.twtw.backend.domain.image.dto.ImageResponse;
import com.twtw.backend.domain.image.exception.InvalidFileTypeException;
import com.twtw.backend.domain.member.entity.Member;
import com.twtw.backend.domain.member.service.AuthService;
import com.twtw.backend.global.properties.StorageProperties;

import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

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

@Service
@RequiredArgsConstructor
public class ImageService {

private static final String IMAGE_FILE_TYPE = "image";
private final AuthService authService;
private final Storage storage;
private final StorageProperties storageProperties;

public ImageResponse uploadImage(final MultipartFile image) throws IOException {
final Member member = authService.getMemberByJwt();
final String contentType = image.getContentType();
validate(contentType);

final String uuid = UUID.randomUUID().toString();
upload(image, contentType, uuid);

final String imagePath = getImageUrl(uuid);
member.updateProfileImage(imagePath);

return new ImageResponse(imagePath);
}

private String getImageUrl(final String uuid) {
return storageProperties.getStorageUrl() + uuid;
}

private void upload(final MultipartFile image, final String contentType, final String uuid)
throws IOException {
storage.create(
BlobInfo.newBuilder(storageProperties.getBucket(), uuid)
.setContentType(contentType)
.build(),
image.getInputStream());
}

private void validate(final String contentType) {
if (contentType == null || !contentType.startsWith(IMAGE_FILE_TYPE)) {
throw new InvalidFileTypeException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.twtw.backend.domain.member.exception.RefreshTokenValidationException;
import com.twtw.backend.global.advice.ErrorResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -16,20 +15,17 @@ public class AuthControllerAdvice {
@ExceptionHandler(RefreshTokenInfoMismatchException.class)
public ResponseEntity<ErrorResponse> refreshTokenInfoMismatch(
final RefreshTokenInfoMismatchException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(RefreshTokenValidationException.class)
public ResponseEntity<ErrorResponse> refreshTokenValidation(
final RefreshTokenValidationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(NicknameExistsException.class)
public ResponseEntity<ErrorResponse> nicknameExists(final NicknameExistsException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ public void addGroupMember(final GroupMember groupMember) {
public boolean hasNoGroupMember() {
return this.groupMembers.isEmpty();
}

public void updateProfileImage(final String profileImage) {
this.profileImage = profileImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.twtw.backend.domain.plan.exception.PlanMakerNotExistsException;
import com.twtw.backend.global.advice.ErrorResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -15,14 +14,12 @@ public class PlanControllerAdvice {
@ExceptionHandler(InvalidPlanMemberException.class)
public ResponseEntity<ErrorResponse> refreshTokenInfoMismatch(
final InvalidPlanMemberException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(PlanMakerNotExistsException.class)
public ResponseEntity<ErrorResponse> refreshTokenInfoMismatch(
final PlanMakerNotExistsException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.twtw.backend.global.exception.EntityNotFoundException;
import com.twtw.backend.global.exception.WebClientResponseException;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -14,19 +13,16 @@ public class GlobalErrorAdvice {

@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<ErrorResponse> webClientResponse(final WebClientResponseException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> entityNotFound(final EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(AuthorityException.class)
public ResponseEntity<ErrorResponse> authority(final AuthorityException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.twtw.backend.global.properties;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "spring.cloud.gcp.storage")
public class StorageProperties {
private final String bucket;
private final String credentialsFile;
private final String storageUrl;
}
Loading

0 comments on commit 905856c

Please sign in to comment.