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

[FEAT] csv 파일 업로드 및 url 반환 로직 구현 #325

Merged
merged 5 commits into from
Aug 24, 2024
Merged
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 main/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ dependencies {

// AWS SDK for S3
implementation "software.amazon.awssdk:s3:2.27.0"

// csv 관련
implementation 'com.opencsv:opencsv:5.5.2'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum ErrorStatus {
* 500 SERVER_ERROR
*/
NOTIFICATION_SERVER_ERROR("알림 서버에 에러가 발생했습니다."),
CSV_ERROR("csv 처리 과정에 에러가 발생했습니다."),
S3_STORAGE_ERROR("s3 스토리지에 에러가 발생했습니다."),
INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ List<Apply> findAllByUserIdAndStatus(@Param("userId") Integer userId,
@Query("select a from Apply a join fetch a.meeting m join fetch m.user u where a.userId = :userId")
List<Apply> findAllByUserId(@Param("userId") Integer userId);

@Query("select a "
+ "from Apply a "
+ "join fetch a.user u "
+ "where a.meetingId = :meetingId "
+ "and a.status in :statuses order by :order")
List<Apply> findAllByMeetingIdWithUser(@Param("meetingId") Integer meetingId, @Param("statuses") List<EnApplyStatus> statuses, @Param("order") String order);

List<Apply> findAllByMeetingIdAndStatus(Integer meetingId, EnApplyStatus statusValue);

List<Apply> findAllByMeetingId(Integer meetingId);
Expand All @@ -34,8 +41,6 @@ List<Apply> findAllByUserIdAndStatus(@Param("userId") Integer userId,
@Query("delete from Apply a where a.meeting.id = :meetingId and a.userId = :userId")
void deleteByMeetingIdAndUserId(@Param("meetingId") Integer meetingId, @Param("userId") Integer userId);

Optional<Apply> findById(Integer applyId);

default Apply findByIdOrThrow(Integer applyId) {
return findById(applyId)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_APPLY.getErrorCode()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ public final class AwsProperties {
private final String algorithm;
private final String contentType;
private final String requestType;
private final String objectUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.sopt.makers.crew.main.common.exception.ErrorStatus.*;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
Expand All @@ -28,11 +29,15 @@
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.utils.BinaryUtils;

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {
private static final String MEETING_PATH = "meeting";
private static final String S3_SERVCIE = "s3";
Expand All @@ -42,6 +47,29 @@ public class S3Service {

private final AwsProperties awsProperties;

public String uploadCSVFile(String fileName) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String curDate = LocalDateTime.now().format(formatter);

String objectKey = MEETING_PATH + "/" + curDate + "/" + fileName;

File file = new File(fileName);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(awsProperties.getS3BucketName())
.key(objectKey)
.build();

s3Client.putObject(putObjectRequest, RequestBody.fromFile(file));
log.info("File uploaded successfully to S3: {}", fileName);

return awsProperties.getObjectUrl() + objectKey;
} catch (Exception e) {
throw new ServerException(S3_STORAGE_ERROR.getErrorCode());
}

}

public PreSignedUrlResponseDto generatePreSignedUrl(String contentType) {
SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
dateTimeFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "모임")
public interface MeetingV2Api {
Expand Down Expand Up @@ -125,8 +125,8 @@ ResponseEntity<TempResponseDto<MeetingV2GetAllMeetingDto>> getMeetingsTemp(@Mode
@Operation(summary = "모임 지원자 목록 csv 파일 다운로드", description = "모임 지원자 목록 csv 파일 다운로드")
ResponseEntity<AppliesCsvFileUrlResponseDto> getAppliesCsvFileUrl(
@PathVariable Integer meetingId,
@PathParam("status") Integer status,
@PathParam("type") Integer type,
@PathParam("order") String order,
@RequestParam(name = "status") List<Integer> status,
@RequestParam(name = "type") List<Integer> type,
@RequestParam(name = "order", required = false, defaultValue = "desc") String order,
Principal principal);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

Expand Down Expand Up @@ -162,7 +163,7 @@ public ResponseEntity<Void> updateApplyStatus(
@Override
@GetMapping("/presigned-url")
public ResponseEntity<PreSignedUrlResponseDto> createPreSignedUrl(
@PathParam("contentType") String contentType, Principal principal) {
@RequestParam("contentType") String contentType, Principal principal) {
PreSignedUrlResponseDto responseDto = s3Service.generatePreSignedUrl(contentType);

return ResponseEntity.ok(responseDto);
Expand All @@ -172,12 +173,14 @@ public ResponseEntity<PreSignedUrlResponseDto> createPreSignedUrl(
@GetMapping("/{meetingId}/list/csv")
public ResponseEntity<AppliesCsvFileUrlResponseDto> getAppliesCsvFileUrl(
@PathVariable Integer meetingId,
@PathParam("status") Integer status,
@PathParam("type") Integer type,
@PathParam("order") String order,
@RequestParam(name = "status") List<Integer> status,
@RequestParam(name = "type") List<Integer> type,
@RequestParam(name = "order", required = false, defaultValue = "desc") String order,
Principal principal) {

Integer userId = UserUtil.getUserId(principal);
AppliesCsvFileUrlResponseDto responseDto = meetingV2Service.getAppliesCsvFileUrl(meetingId, status, order, userId);

return null;
return ResponseEntity.ok(responseDto);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package org.sopt.makers.crew.main.meeting.v2.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
@Schema(name = "AppliesCsvFileUrlResponseDto", description = "csv url Dto")
public class AppliesCsvFileUrlResponseDto {

@NotNull
@Schema(description = "csv 파일 url", example = "[url] 형식")
private final String url;

public static AppliesCsvFileUrlResponseDto of(String url){
return new AppliesCsvFileUrlResponseDto(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,54 @@

import com.fasterxml.jackson.annotation.JsonProperty;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
@Schema(name = "PreSignedUrlFieldResponseDto", description = "presigned 필드 Dto")
public class PreSignedUrlFieldResponseDto {

@JsonProperty("Content-Type")
@NotNull
@Schema(description = "contentType", example = "image/jpeg")
private final String contentType;

@JsonProperty("key")
@NotNull
@Schema(description = "key", example = "key 값")
private final String key;

@JsonProperty("bucket")
@NotNull
@Schema(description = "bucket", example = "bucket 값")
private final String bucket;

@JsonProperty("X-Amz-Algorithm")
@NotNull
@Schema(description = "algorithm", example = "algorithm 값")
private final String algorithm;

@JsonProperty("X-Amz-Credential")
@NotNull
@Schema(description = "credential", example = "credential 값")
private final String credential;

@JsonProperty("X-Amz-Date")
@NotNull
@Schema(description = "X-Amz-Date", example = "X-Amz-Date 값")
private final String date;

@JsonProperty("Policy")
@NotNull
@Schema(description = "policy", example = "policy 값")
private final String policy;

@JsonProperty("X-Amz-Signature")
@NotNull
@Schema(description = "X-Amz-Signature", example = "X-Amz-Signature 값")
private final String signature;

public static PreSignedUrlFieldResponseDto of(String contentType, String key, String bucket, String algorithm,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package org.sopt.makers.crew.main.meeting.v2.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
@Schema(name = "PreSignedUrlResponseDto", description = "presigned url Dto")
public class PreSignedUrlResponseDto {

@NotNull
@Schema(description = "presignedUrl", example = "[url] 형식")
private final String url;

@NotNull
@Schema(description = "field", example = "")
private final PreSignedUrlFieldResponseDto fields;

public static PreSignedUrlResponseDto of(String url, PreSignedUrlFieldResponseDto fields){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.sopt.makers.crew.main.meeting.v2.dto.request.ApplyV2UpdateStatusBodyDto;
import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto;
import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.AppliesCsvFileUrlResponseDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto;
Expand Down Expand Up @@ -37,4 +38,6 @@ MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryComm
void updateMeeting(Integer meetingId, MeetingV2CreateMeetingBodyDto requestBody, Integer userId);

void updateApplyStatus(Integer meetingId, ApplyV2UpdateStatusBodyDto requestBody, Integer userId);

AppliesCsvFileUrlResponseDto getAppliesCsvFileUrl(Integer meetingId, List<Integer> status, String order, Integer userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@
import static org.sopt.makers.crew.main.common.constant.CrewConst.ACTIVE_GENERATION;
import static org.sopt.makers.crew.main.common.exception.ErrorStatus.*;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.RequiredArgsConstructor;

import org.sopt.makers.crew.main.common.dto.MeetingResponseDto;
import org.sopt.makers.crew.main.common.exception.BadRequestException;
import org.sopt.makers.crew.main.common.exception.ServerException;
import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto;
import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto;
import org.sopt.makers.crew.main.common.util.Time;
Expand All @@ -37,6 +44,7 @@
import org.sopt.makers.crew.main.entity.user.UserRepository;
import org.sopt.makers.crew.main.entity.user.enums.UserPart;
import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO;
import org.sopt.makers.crew.main.external.s3.service.S3Service;
import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper;
import org.sopt.makers.crew.main.meeting.v2.dto.MeetingMapper;
import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto;
Expand All @@ -45,6 +53,7 @@
import org.sopt.makers.crew.main.meeting.v2.dto.request.ApplyV2UpdateStatusBodyDto;
import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto;
import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.AppliesCsvFileUrlResponseDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto;
import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto;
Expand All @@ -59,6 +68,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.opencsv.CSVWriter;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -73,6 +84,8 @@ public class MeetingV2ServiceImpl implements MeetingV2Service {
private final CommentRepository commentRepository;
private final LikeRepository likeRepository;

private final S3Service s3Service;

private final MeetingMapper meetingMapper;
private final ApplyMapper applyMapper;

Expand Down Expand Up @@ -196,7 +209,6 @@ public void applyMeetingCancel(Integer meetingId, Integer userId) {
}

@Override
@Transactional(readOnly = true)
public MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryCommand,
Integer meetingId,
Integer userId) {
Expand Down Expand Up @@ -294,6 +306,59 @@ public void updateApplyStatus(Integer meetingId, ApplyV2UpdateStatusBodyDto requ

}

@Override
public AppliesCsvFileUrlResponseDto getAppliesCsvFileUrl(Integer meetingId, List<Integer> status, String order, Integer userId) {
Meeting meeting = meetingRepository.findByIdOrThrow(meetingId);
meeting.validateMeetingCreator(userId);

List<EnApplyStatus> statuses = status.stream().map(EnApplyStatus::ofValue).toList();
List<Apply> applies = applyRepository.findAllByMeetingIdWithUser(meetingId, statuses, order);

String csvFilePath = createCsvFile(applies);
String csvFileUrl = s3Service.uploadCSVFile(csvFilePath);
deleteCsvFile(csvFilePath);

return AppliesCsvFileUrlResponseDto.of(csvFileUrl);
}

private void deleteCsvFile(String filePath) {
try {
Files.deleteIfExists(Paths.get(filePath));
} catch (IOException e) {
throw new ServerException(CSV_ERROR.getErrorCode());
}
}

private String createCsvFile(List<Apply> applies) {
String filePath = UUID.randomUUID() + ".csv";

try (CSVWriter writer = new CSVWriter(new FileWriter(filePath))) {
// CSV 파일의 헤더 정의
String[] header = {"이름", "최근 활동 파트", "최근 활동 기수", "전화번호", "신청 날짜 및 시간", "신청 내용"};
writer.writeNext(header);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (Apply apply : applies) {
User user = apply.getUser();
UserActivityVO activity = user.getRecentActivityVO();

String[] data = {
user.getName(),
activity.getPart(),
String.valueOf(activity.getGeneration()),
String.format("\"%s\"", user.getPhone()),
apply.getAppliedDate().format(formatter),
apply.getContent()
};
writer.writeNext(data);
}
} catch (Exception e) {
throw new ServerException(CSV_ERROR.getErrorCode());
}

return filePath;
}

private Boolean checkActivityStatus(Meeting meeting) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime mStartDate = meeting.getMStartDate();
Expand Down
Loading
Loading