Skip to content

Commit

Permalink
Merge pull request #186 from ddackkeun/develop
Browse files Browse the repository at this point in the history
리뷰, 이미지 업로드 코드 리팩터링 및 docker-compose 추가
  • Loading branch information
ddackkeun authored Jul 19, 2024
2 parents 7281089 + acdec17 commit 00ab1e7
Show file tree
Hide file tree
Showing 56 changed files with 1,774 additions and 1,485 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ out/
.vscode/

###
application-base-addi.yml

/src/main/resources/application-base-addi.yml
/docker-compose-prod.yml
/photosmap
/home
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ ARG JAR_FILE=build/libs/four_cut_photos_map-0.0.1-SNAPSHOT.jar
#ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar","/app.jar"]
ARG PROFILES
ENV SPRING_PROFILES_ACTIVE=${PROFILES}
ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "/app.jar"]
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'

developmentOnly 'org.springframework.boot:spring-boot-devtools'

//db
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
implementation "com.h2database:h2"
implementation 'mysql:mysql-connector-java'
// implementation "com.h2database:h2"
implementation 'com.mysql:mysql-connector-j:8.0.33'

// lombok
compileOnly 'org.projectlombok:lombok'
Expand Down Expand Up @@ -77,6 +77,7 @@ dependencies {

// aws 파일 업로드
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.241'

// 이미지 리사이징 을 위한 라이브러리
implementation group: 'org.imgscalr', name: 'imgscalr-lib', version: '4.2'
Expand Down
59 changes: 59 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
version: '3.8'

networks:
photosmap-network:
driver: bridge

services:
db:
image: mysql:8.0.33
container_name: db
restart: unless-stopped
ports:
- "3306:3306"
networks:
- photosmap-network
volumes:
- ./photosmap/volume/db/etc/mysql/conf.d:/etc/mysql/conf.d
- ./photosmap/volume/db/var/lib/mysql:/var/lib/mysql
- ./photosmap/volume/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
- TZ=Asia/Seoul
- MYSQL_DATABASE=photos_map
- MYSQL_ROOT_PASSWORD=root1234
- MYSQL_USER=user1
- MYSQL_PASSWORD=1234

app:
build:
context: .
dockerfile: Dockerfile
args:
PROFILES: dev
image: photosmap:latest
container_name: photosmap
restart: always
ports:
- '8080:8080'
networks:
- photosmap-network
volumes:
- ./photosmap/volume/app:/app
depends_on:
- db
- redis

redis:
image: redis:alpine
container_name: redis
restart: always
ports:
- '6379:6379'
networks:
- photosmap-network
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./photosmap/volume/redis/data:/data
- ./photosmap/volume/redis/usr/local/etc/redis/redis.conf:/usr/local/etc/redis/redis.conf
- ./photosmap/volume/redis/etc/redis/users.acl:/etc/redis/users.acl

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
@RequiredArgsConstructor
public class AwsS3Service {

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

private final AmazonS3 amazonS3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
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 awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

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

import com.idea5.four_cut_photos_map.domain.file.dto.response.UploadImageResp;
import com.idea5.four_cut_photos_map.domain.file.dto.response.ImageUploadResponse;
import com.idea5.four_cut_photos_map.domain.file.dto.response.ImageUploadResultResponse;
import com.idea5.four_cut_photos_map.domain.file.service.S3Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -22,15 +23,16 @@ public class FileUploadController {

// 단일 이미지 업로드
@PostMapping("/image")
public ResponseEntity<UploadImageResp> uploadImage(@RequestParam String category, @RequestParam MultipartFile file) {
UploadImageResp uploadImageResp = s3Service.uploadImage(category, file);
return ResponseEntity.ok(uploadImageResp);
public ResponseEntity<ImageUploadResponse> uploadImage(@RequestParam String category, @RequestParam MultipartFile file) {
ImageUploadResponse imageUploadResponse = s3Service.uploadImage(category, file);
return ResponseEntity.ok(imageUploadResponse);
}

// 다중 이미지 업로드
@PostMapping("/images")
public ResponseEntity<List<UploadImageResp>> uploadImages(@RequestParam String category, @RequestParam List<MultipartFile> files) {
List<UploadImageResp> uploadImageResps = s3Service.uploadImages(category, files);
return ResponseEntity.ok(uploadImageResps);
public ResponseEntity<ImageUploadResultResponse> uploadImages(@RequestParam String category, @RequestParam List<MultipartFile> files) {
log.info("files: {}, size of file list: {}", files, files.size());
ImageUploadResultResponse response = s3Service.uploadImages(category, files);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.net.URL;

/**
* 이미지 업로드 응답
*/
@Getter
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UploadImageResp {
public class ImageUploadResponse {
private String fileName;
private String url;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.idea5.four_cut_photos_map.domain.file.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

/**
* 이미지 업로드 결과 응답 객체
*/
@Getter
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class ImageUploadResultResponse {
private List<ImageUploadResponse> successfulUploads;
private List<ImageUploadResponse> failedUploads;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.idea5.four_cut_photos_map.domain.file.dto.response.UploadImageResp;
import com.idea5.four_cut_photos_map.domain.file.dto.response.ImageUploadResponse;
import com.idea5.four_cut_photos_map.domain.file.dto.response.ImageUploadResultResponse;
import com.idea5.four_cut_photos_map.global.error.ErrorCode;
import com.idea5.four_cut_photos_map.global.error.exception.BusinessException;
import com.idea5.four_cut_photos_map.global.util.Util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
Expand All @@ -17,63 +17,87 @@
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {
private final AmazonS3Client amazonS3Client;
private static final String IMAGE_CONTENT_TYPE_PREFIX = "image";

@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3Client amazonS3Client;
private final String bucketName;
private final String cloudFrontDomain;

@Value("${cloud.aws.cloudFront.domainName}")
private String cloudFront;
public S3Service(AmazonS3Client amazonS3Client,
@Value("${cloud.aws.s3.bucket-name}") String bucketName,
@Value("${cloud.aws.cloudFront.domain}") String cloudFrontDomain) {
this.amazonS3Client = amazonS3Client;
this.bucketName = bucketName;
this.cloudFrontDomain = cloudFrontDomain;
}

// 단일 이미지 파일 업로드
public UploadImageResp uploadImage(String category, MultipartFile file) {
// 1. 이미지 파일이 아닌 경우 예외처리
public ImageUploadResponse uploadImage(String category, MultipartFile file) {
// 이미지 파일이 아닌 경우 예외발생
validImageFile(file);

// 2. 객체 키 생성(키 이름 중복 방지)
String key = Util.generateS3ObjectKey(category, file.getOriginalFilename());
log.info("key = " + key);
log.info("key : {}" , key);

// 3. 파일 업로드
String imageUrl = putS3(key, file);
return new UploadImageResp(imageUrl);
return new ImageUploadResponse(file.getOriginalFilename(), imageUrl);
}

// 다중 이미지 파일 업로드
public List<UploadImageResp> uploadImages(String category, List<MultipartFile> files) {
List<UploadImageResp> images = new ArrayList<>();
for(MultipartFile file : files) {
images.add(uploadImage(category, file));
public ImageUploadResultResponse uploadImages(String category, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
throw new BusinessException(ErrorCode.NO_FILES_PROVIDED);
}

List<ImageUploadResponse> successfulUploads = new ArrayList<>();
List<ImageUploadResponse> failedUploads = new ArrayList<>();

// 업로드 실패 시 개별 처리 방식 정책 진행
for (MultipartFile file : files) {
try {
successfulUploads.add(uploadImage(category, file));
} catch (BusinessException e) {
failedUploads.add(new ImageUploadResponse(file.getOriginalFilename(), null));
}
}
return images;

return new ImageUploadResultResponse(successfulUploads, failedUploads);
}

// 이미지 파일인지 검사
public void validImageFile(MultipartFile file) {
if(file.getContentType().startsWith("image") == false) {
if(file.getContentType() == null || !file.getContentType().startsWith(IMAGE_CONTENT_TYPE_PREFIX)) {
log.warn("file is not an image file.");
throw new BusinessException(ErrorCode.NOT_IMAGE_FILE);
}
}

// S3 객체 생성
public String putS3(String key, MultipartFile file) {
// metadata 생성
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
// 1. s3 파일 업로드

// s3버킷 파일 업로드
try {
amazonS3Client.putObject(bucket, key, file.getInputStream(), metadata);
amazonS3Client.putObject(bucketName, key, file.getInputStream(), metadata);
} catch (IOException e) {
throw new RuntimeException("파일 업로드에 실패했습니다.");
log.error("Failed to upload image to S3", e);
throw new BusinessException(ErrorCode.IMAGE_UPLOAD_FAILED);
}
// 2. 업로드한 이미지 URL 리턴

// 업로드한 이미지 URL 리턴(cloudFront 캐시된 이미지 URL)
return getImageUrl(key);
}

// 이미지 URL 조회
private String getImageUrl(String key) {
// cloudFront 도메인 URL 로 리턴
return "https://" + cloudFront + "/" + key;
return cloudFrontDomain + "/" + key;
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.idea5.four_cut_photos_map.domain.review.dto.entity;
package com.idea5.four_cut_photos_map.domain.member.dto.response;


import com.fasterxml.jackson.databind.PropertyNamingStrategies;
Expand All @@ -12,7 +12,7 @@
@NoArgsConstructor
@ToString
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class MemberResp {
public class MemberResponse {
private Long id; // 회원 번호
private String nickname; // 닉네임
private String mainMemberTitle; // 회원 대표 칭호
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.idea5.four_cut_photos_map.domain.member.mapper;

import com.idea5.four_cut_photos_map.domain.member.dto.response.MemberResponse;
import com.idea5.four_cut_photos_map.domain.member.entity.Member;
import com.idea5.four_cut_photos_map.domain.memberTitle.entity.MemberTitle;
import org.springframework.stereotype.Component;

@Component
public class MemberMapper {
public MemberResponse toResponse(Member member) {
return MemberResponse.builder()
.id(member.getId())
.nickname(member.getNickname())
.build();
}

public MemberResponse toResponse(Member member, String mainMemberTitleName) {
return MemberResponse.builder()
.id(member.getId())
.nickname(member.getNickname())
.mainMemberTitle(mainMemberTitleName)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import com.idea5.four_cut_photos_map.domain.member.repository.MemberRepository;
import com.idea5.four_cut_photos_map.domain.memberTitle.entity.MemberTitleLog;
import com.idea5.four_cut_photos_map.domain.memberTitle.service.MemberTitleService;
import com.idea5.four_cut_photos_map.domain.review.service.ReviewWriteService;
import com.idea5.four_cut_photos_map.domain.review.service.RequestReviewServiceImpl;
import com.idea5.four_cut_photos_map.global.common.RedisDao;
import com.idea5.four_cut_photos_map.global.error.ErrorCode;
import com.idea5.four_cut_photos_map.global.error.exception.BusinessException;
Expand All @@ -38,7 +38,7 @@ public class MemberService {
private final MemberTitleService memberTitleService;
private final FavoriteService favoriteService;
private final JwtService jwtService;
private final ReviewWriteService reviewWriteService;
private final RequestReviewServiceImpl requestReviewServiceImpl;

// 서비스 로그인
@Transactional
Expand Down Expand Up @@ -132,7 +132,7 @@ public MemberWithdrawlResp deleteMember(Long id) {
// 2. Member 삭제하기 전 Member 를 참조하고 있는 엔티티(MemberTitleLog, Favorite, Review) 먼저 삭제하기
memberTitleService.deleteByMemberId(id);
favoriteService.deleteByMemberId(id);
reviewWriteService.deleteByWriterId(id);
requestReviewServiceImpl.deleteByWriterId(id);
// 3. DB 에서 회원 삭제
memberRepository.deleteById(id);
return new MemberWithdrawlResp(id);
Expand Down
Loading

0 comments on commit 00ab1e7

Please sign in to comment.