Skip to content

Commit

Permalink
Merge pull request #36 from SQUAD-D/feature#18/image-update
Browse files Browse the repository at this point in the history
test
  • Loading branch information
songhaechan authored Jul 6, 2024
2 parents 73cd095 + 5e7c136 commit 123ff8e
Show file tree
Hide file tree
Showing 20 changed files with 442 additions and 168 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# run: docker push haechansomg/simple-board2:was02

- name: Deploy to server
uses: appleboy/[email protected].0
uses: appleboy/[email protected].3
id: deploy
env:
COMPOSE: "/home/ubuntu/compose/docker-compose.yml"
Expand All @@ -85,5 +85,4 @@ jobs:
script: |
sudo docker-compose -f $COMPOSE down --rmi all
sudo docker pull haechansomg/simple-board:was01
sudo docker-compose -f $COMPOSE up -d
sudo docker image prune -f
sudo docker-compose -f $COMPOSE up -d
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-quartz:2.7.5'
implementation 'org.apache.commons:commons-collections4:4.0'
implementation group: 'com.github.javafaker', name: 'javafaker', version: '1.0.2'
implementation 'org.springframework.boot:spring-boot-starter-mail'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2")
}
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ services:
# - 9121:9121
# environment:
# REDIS_ADDR: "redis:6379"
links:
- redis
# links:
# - redis
# - prometheus
1 change: 0 additions & 1 deletion src/main/java/squad/board/BoardApplication.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package squad.board;

import org.quartz.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/squad/board/apicontroller/BoardApiController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package squad.board.apicontroller;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import squad.board.aop.BoardWriterAuth;
Expand All @@ -22,10 +24,19 @@ public class BoardApiController {
@PostMapping(value = "/boards")
public CommonIdResponse saveBoard(
@SessionAttribute Long memberId,
@Valid @RequestBody CreateBoardRequest createBoard) {
@Valid @RequestBody CreateBoardRequest createBoard) throws JsonProcessingException {
return boardService.createBoard(memberId, createBoard);
}

// @PostMapping(value = "/boards", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE,
// MediaType.APPLICATION_JSON_VALUE})
// public CommonIdResponse saveBoard(
// @SessionAttribute Long memberId,
// @RequestPart CreateBoardRequest createBoard,
// @RequestPart("images") MultipartFile[] images) {
// return boardService.createBoard(memberId, createBoard, images);
// }

// 이미지 S3 전송
@PostMapping(value = "/boards/img")
public ImageInfoResponse saveImg(
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/squad/board/config/AsyncThreadPoolConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package squad.board.config;

import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {

@Bean
public Executor asyncThreadTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(1);
threadPoolTaskExecutor.setMaxPoolSize(1);
return threadPoolTaskExecutor;
}
}
15 changes: 15 additions & 0 deletions src/main/java/squad/board/config/MailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package squad.board.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

@Configuration
public class MailConfig {

@Bean
public JavaMailSender javaMailSender() {
return new JavaMailSenderImpl();
}
}
19 changes: 11 additions & 8 deletions src/main/java/squad/board/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisConfiguration);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
}

@Bean
public RedisConnectionFactory redisConnectionFactoryToken() {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration();
Expand All @@ -36,14 +47,6 @@ public RedisConnectionFactory redisConnectionFactoryToken() {
return new LettuceConnectionFactory(redisConfiguration);
}

@Bean(name = "redisTemplate")
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}

@Bean(name = "redisTemplateToken")
public RedisTemplate<?, ?> redisTemplateToken() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/squad/board/config/TaskSchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package squad.board.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
public class TaskSchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
}

@Bean
public Executor taskScheduler() {
return Executors.newScheduledThreadPool(2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package squad.board.exception;

public class ObjectMapperException extends IllegalStateException {
}
69 changes: 38 additions & 31 deletions src/main/java/squad/board/service/BoardService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package squad.board.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
Expand All @@ -21,42 +22,39 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class BoardService {

private static final long MAX_IMAGE_SIZE = 5000000L;
private static final int MAX_IMAGE_NAME_SIZE = 100;
private static final String IMAGE_EXTENSION_EXTRACT_REGEX = "(.png|.jpg|.jpeg)$";
private static final String TEMP_FOLDER_NAME = "tmp";
private final BoardMapper boardMapper;
private final CommentMapper commentMapper;
private final ImageMapper imageMapper;
private final S3Service s3Service;
private static final long MAX_IMAGE_SIZE = 5000000L;
private static final int MAX_IMAGE_NAME_SIZE = 100;
private static final String TEMP_FOLDER_NAME = "tmp";
private static final String ORIGINAL_FOLDER_NAME = "original";
private static final String IMAGE_EXTENSION_EXTRACT_REGEX = "(.png|.jpg|.jpeg)$";

public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) {
private final S3MessageQueue messageQueue;
private final S3DeadLetterQueue deadLetterQueue;

public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) throws JsonProcessingException {
// 게시글 저장
Board board = createBoard.toEntity(memberId);
boardMapper.save(board);
// 이미지 정보 저장
if (createBoard.isImageExist()) {
saveImageInfo(board.getBoardId(), createBoard.getImageInfo());
imageMapper.save(createBoard.getImageInfo(), board.getBoardId());
deadLetterQueue.pushAll(createBoard.getImageInfo().stream()
.map(ImageInfoRequest::getImageUUID)
.toList());
}
return new CommonIdResponse(board.getBoardId());
}

private void saveImageInfo(Long boardId, List<ImageInfoRequest> imageInfoRequests) {
imageMapper.save(imageInfoRequests, boardId);
for (ImageInfoRequest request : imageInfoRequests) {
// tmp 폴더의 이미지를 original 폴더로 이동
s3Service.moveImageToOriginal(request.getImageUUID(), TEMP_FOLDER_NAME, ORIGINAL_FOLDER_NAME);
}
}

public ImageInfoResponse saveImageToS3(MultipartFile image) {
imageValidation(image);
String uuid = s3Service.saveFile(image, TEMP_FOLDER_NAME);
Expand All @@ -66,7 +64,8 @@ public ImageInfoResponse saveImageToS3(MultipartFile image) {

private void imageValidation(MultipartFile image) {
String imageName = image.getOriginalFilename();
if (!imageName.substring(imageName.lastIndexOf(".")).matches(IMAGE_EXTENSION_EXTRACT_REGEX)) {
if (!imageName.substring(imageName.lastIndexOf("."))
.matches(IMAGE_EXTENSION_EXTRACT_REGEX)) {
throw new ImageException(ImageStatus.INVALID_IMAGE_EXTENSION);
}
// 이미지 파일명 길이 제한
Expand All @@ -81,20 +80,28 @@ private void imageValidation(MultipartFile image) {
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> findBoards(Long size, Long requestPage, Long memberId) {
if (memberId == null) {
public ContentListResponse<BoardResponse> findBoards(Long size, Long requestPage,
Long memberId) {
if (memberId==null) {
throw new BoardException(BoardStatus.INVALID_MEMBER_ID);
}
Long offset = calcOffset(requestPage, size);
Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(memberId), size);
return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, memberId), boardPaging);
Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(memberId),
size);
return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, memberId),
boardPaging);
}

private Long calcOffset(Long page, Long size) {
return (page - 1) * size;
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> findBoards(Long size, Long requestPage) {
Long offset = calcOffset(requestPage, size);
Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(null), size);
return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, null), boardPaging);
return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, null),
boardPaging);
}

@Transactional(readOnly = true)
Expand All @@ -118,7 +125,7 @@ public CommonIdResponse updateBoard(Long boardId, BoardUpdateRequest updateBoard
List<ImageInfoRequest> imageInfoRequests = updateBoard.getImageInfoList();
// DB에 이미지 정보 저장
if (CollectionUtils.isNotEmpty(imageInfoRequests)) {
saveImageInfo(boardId, imageInfoRequests);
imageMapper.save(imageInfoRequests, boardId);
}
// 기존 이미지 정보
List<String> savedImageUuid = imageMapper.findImageUuid(boardId);
Expand All @@ -139,14 +146,14 @@ private void deleteImageInfo(List<String> deleteRequestImageUuid) {
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> searchBoard(String keyWord, Long size, Long requestPage, String searchType) {
public ContentListResponse<BoardResponse> searchBoard(String keyWord, Long size,
Long requestPage, String searchType) {
Long offset = calcOffset(requestPage, size);
Pagination boardPaging = new Pagination(requestPage, boardMapper.countByKeyWord(keyWord, searchType), size);
List<BoardResponse> byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset, searchType);
return new ContentListResponse<>(boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging);
}

private Long calcOffset(Long page, Long size) {
return (page - 1) * size;
Pagination boardPaging = new Pagination(requestPage,
boardMapper.countByKeyWord(keyWord, searchType), size);
List<BoardResponse> byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset,
searchType);
return new ContentListResponse<>(
boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging);
}
}
46 changes: 46 additions & 0 deletions src/main/java/squad/board/service/S3Consumer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package squad.board.service;

import com.amazonaws.AmazonServiceException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@RequiredArgsConstructor
@Slf4j
public class S3Consumer {

private final S3MessageQueue messageQueue;
private final S3DeadLetterQueue deadLetterQueue;
private final S3Service s3Service;
private final S3MailSender s3MailSender;
private final String MAIL_CONTENT = "The dead letter queue has exceeded its maximum allowable size.";

// @Scheduled(fixedDelay = 1000)
// public void normalConsume() {
// while (!messageQueue.isEmpty()) {
// String uuid = messageQueue.pop();
// try {
// s3Service.moveImageToOriginal(uuid);
// } catch (AmazonServiceException e) {
// deadLetterQueue.push(uuid);
// }
// }
// }

@Scheduled(fixedDelay = 2000)
public void deadLetterConsume() {
if (deadLetterQueue.isRateLimit()) {
s3MailSender.send(MAIL_CONTENT);
}
List<String> uuids = new ArrayList<>();
while (!deadLetterQueue.isEmpty()) {
uuids.add(deadLetterQueue.pop());
}
messageQueue.pushAll(uuids);
}
}
34 changes: 34 additions & 0 deletions src/main/java/squad/board/service/S3DeadLetterQueue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package squad.board.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class S3DeadLetterQueue {

private static final String MESSAGE_QUEUE = "s3_dead_letter";
private static final int EMPTY = 0;
private static final int LIMIT = 3;
private final RedisTemplate<String, Object> redisTemplate;

public void pushAll(List<String> imageUUID) {
for (String uuid : imageUUID)
redisTemplate.opsForList().leftPushAll(MESSAGE_QUEUE, uuid);
}

public String pop() {
return (String) redisTemplate.opsForList().rightPop(MESSAGE_QUEUE);
}

public boolean isRateLimit() {
return redisTemplate.opsForList().size(MESSAGE_QUEUE) > LIMIT;
}

public boolean isEmpty() {
return redisTemplate.opsForList().size(MESSAGE_QUEUE)==EMPTY;
}
}
Loading

0 comments on commit 123ff8e

Please sign in to comment.