Skip to content

Commit

Permalink
Merge pull request #116 from UMC-TripPiece/feature/99
Browse files Browse the repository at this point in the history
[feature] 이미지 및 동영상 확장자 예외 처리/여행 상태 스케줄러 구현/코드 리팩토링
  • Loading branch information
StoneCAU authored Jan 16, 2025
2 parents f34b6e1 + 13057cc commit 3ecac7a
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ public enum ErrorStatus implements BaseErrorCode {
PICTURE_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "PICTURE400", "이미지의 확장자가 잘못되었습니다."),
VIDEO_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "VIDEO400", "동영상의 확장자가 잘못되었습니다."),
PAYLOAD_TOO_LARGE(HttpStatus.PAYLOAD_TOO_LARGE, "UPLOAD413", "파일 크기가 허용 범위를 초과했습니다."),
NO_FILE_EXTENTION(HttpStatus.BAD_REQUEST, "UPLOAD400", "파일의 이름에 확장자가 존재하지 않습니다."),

// 이모지 오류
EMOJI_NUMBER_ERROR(HttpStatus.BAD_REQUEST, "EMOJI400", "이모지의 갯수는 4개이여야 합니다."),
EMOJI_INPUT_ERROR(HttpStatus.BAD_REQUEST, "EMOJI401", "이모지가 아닌 입력이 있습니다."),
EMOJI_INPUT_ERROR(HttpStatus.BAD_REQUEST, "EMOJI400", "이모지가 아닌 입력이 있습니다."),

// 글자 수 오류
TEXT_LENGTH_30_ERROR(HttpStatus.BAD_REQUEST, "TEXT400", "글자 수가 30자를 초과하였습니다."),
TEXT_LENGTH_100_ERROR(HttpStatus.BAD_REQUEST, "TEXT401", "글자 수가 100자를 초과하였습니다."),
TEXT_LENGTH_100_ERROR(HttpStatus.BAD_REQUEST, "TEXT400", "글자 수가 100자를 초과하였습니다."),
TEXT_LENGTH_150_ERROR(HttpStatus.BAD_REQUEST, "TEXT400", "글자 수가 150자를 초과하였습니다."),

// 사용자 관련 오류
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "USER401", "비밀번호가 일치하지 않습니다."),
Expand All @@ -46,8 +48,12 @@ public enum ErrorStatus implements BaseErrorCode {

INVALID_PROFILE_IMAGE(HttpStatus.BAD_REQUEST, "PROFILE400", "잘못된 프로필 이미지 형식입니다."),
PROFILE_UPDATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "PROFILE500", "프로필 업데이트에 실패했습니다."),


// 토큰 관련 오류
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "TOKEN401", "유효하지 않은 토큰입니다."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "TOKEN401", "만료된 토큰입니다."),
UNSUPPORTED_TOKEN(HttpStatus.BAD_REQUEST, "TOKEN402", "지원하지 않는 형식의 토큰입니다."),
NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND, "TOKEN404", "토큰의 클레임이 비어있습니다."),

GENERAL_BAD_REQUEST(HttpStatus.BAD_REQUEST, "SOCIAL400", "일반 회원가입에 대한 요청입니다."),
PLATFORM_BAD_REQUEST(HttpStatus.BAD_REQUEST, "SOCIAL400", "유효하지 않은 소셜 플랫폼입니다. (KAKAO 또는 APPLE만 허용)"),
Expand All @@ -63,6 +69,7 @@ public enum ErrorStatus implements BaseErrorCode {
// 여행 조각 관련 오류
NOT_FOUND_TRIPPIECE(HttpStatus.NOT_FOUND, "TRIPPIECE404", "여행 조각이 존재하지 않습니다"),
INVALID_TRIPPIECE_SORT_OPTION(HttpStatus.BAD_REQUEST, "TRIPPIECE401", "유효하지 않은 정렬 조건입니다."),
INVALID_TRIPPIECE_CATEGORY(HttpStatus.BAD_REQUEST, "TRIPPIECE400", "올바르지 않은 카테고리입니다."),

// 여행기 관련 오류
INVALID_TRAVEL_PARARM(HttpStatus.BAD_REQUEST, "TRAVEL400", "유효하지 않은 파라미터입니다."),
Expand All @@ -78,6 +85,7 @@ public enum ErrorStatus implements BaseErrorCode {
// 지도 관련 오류
INVALID_CITY_COUNTRY_RELATION(HttpStatus.BAD_REQUEST, "MAP400", "유효하지 않은 도시와 국가 관계입니다.");


private final HttpStatus httpStatus;
private final String code;
private final String message;
Expand Down
33 changes: 30 additions & 3 deletions src/main/java/umc/TripPiece/aws/s3/AmazonS3Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import umc.TripPiece.apiPayload.code.status.ErrorStatus;
import umc.TripPiece.apiPayload.exception.handler.BadRequestHandler;
import umc.TripPiece.config.AmazonConfig;
import umc.TripPiece.domain.Uuid;

import java.io.IOException;
import java.util.*;
import umc.TripPiece.domain.enums.Category;

@Slf4j
@Component
Expand All @@ -24,7 +27,7 @@ public class AmazonS3Manager{
private final AmazonConfig amazonConfig;

// 여러장의 파일 저장
public List<String> saveFiles(List<String> keyNames, List<MultipartFile> files) {
public List<String> saveFiles(List<String> keyNames, List<MultipartFile> files, Category category) {
int fileNum = files.size();
List urls = new ArrayList();

Expand All @@ -36,6 +39,8 @@ public List<String> saveFiles(List<String> keyNames, List<MultipartFile> files)
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());

validateImageFileExtention(Objects.requireNonNull(file.getOriginalFilename()), category);

try {
amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata));
} catch (IOException e){
Expand All @@ -49,11 +54,13 @@ public List<String> saveFiles(List<String> keyNames, List<MultipartFile> files)
}

// 단일 파일 저장
public String uploadFile(String keyName, MultipartFile file){
public String uploadFile(String keyName, MultipartFile file, Category category){
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());

validateImageFileExtention(Objects.requireNonNull(file.getOriginalFilename()), category);

try {
amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata));
}catch (IOException e){
Expand All @@ -73,7 +80,6 @@ public void deleteFile(String keyName) {
}
}


public String generateTripPieceKeyName(Uuid uuid) {
return amazonConfig.getTripPiecePath() + '/' + uuid.getUuid();
}
Expand All @@ -88,5 +94,26 @@ public List<String> generateTripPieceKeyNames(List<Uuid> uuids) {
return keyNames;
}

private void validateImageFileExtention(String filename, Category category) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1) {
throw new BadRequestHandler(ErrorStatus.NO_FILE_EXTENTION);
}

String extention = filename.substring(lastDotIndex + 1).toLowerCase();
List<String> pictureExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");
List<String> videoExtentionList = Arrays.asList("mp4", "mov", "wmv", "avi");

if (category.equals(Category.PICTURE) || category.equals(Category.SELFIE)) {
if (!pictureExtentionList.contains(extention)) {
throw new BadRequestHandler(ErrorStatus.PICTURE_EXTENSION_ERROR);
}
}

if (category.equals(Category.VIDEO) || category.equals(Category.WHERE)) {
if (!videoExtentionList.contains(extention)) {
throw new BadRequestHandler(ErrorStatus.VIDEO_EXTENSION_ERROR);
}
}
}
}
21 changes: 19 additions & 2 deletions src/main/java/umc/TripPiece/domain/jwt/TokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package umc.TripPiece.domain.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;
import umc.TripPiece.apiPayload.code.status.ErrorStatus;
import umc.TripPiece.apiPayload.exception.GeneralException;

@Component
@Slf4j
public class TokenProvider {

private final JWTProperties jwtProperties;
Expand All @@ -33,8 +40,18 @@ public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
throw new GeneralException(ErrorStatus.INVALID_TOKEN); // 유효하지 않은 토큰 에러 반환
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
throw new GeneralException(ErrorStatus.TOKEN_EXPIRED); // 만료된 토큰 에러 반환
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
throw new GeneralException(ErrorStatus.UNSUPPORTED_TOKEN); // 지원하지 않는 형식 토큰 에러 반환
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
throw new GeneralException(ErrorStatus.NOT_FOUND_TOKEN); // 토큰의 클레임이 비어 있는 경우 에러 반환
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package umc.TripPiece.domain.scheduler;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import umc.TripPiece.repository.TravelRepository;

@Component
@RequiredArgsConstructor
public class TravelStatusScheduler {
private final TravelRepository travelRepository;

@Scheduled(cron = "0 0 0 * * ?") // 매일 자정 여행기의 기간을 검사하여 상태 변경
public void updateExpiredStatuses() {
travelRepository.updateCompletedStatuses();
}
}
11 changes: 10 additions & 1 deletion src/main/java/umc/TripPiece/repository/TravelRepository.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package umc.TripPiece.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import umc.TripPiece.domain.Travel;
import umc.TripPiece.domain.enums.TravelStatus;

Expand All @@ -10,10 +14,15 @@
public interface TravelRepository extends JpaRepository<Travel, Long> {
List<Travel> findByCityId(Long cityId);
List<Travel> findByCity_CountryId(Long countryId);
Travel findByStatusAndUserId(TravelStatus travelStatus, Long userId);
Optional<Travel> findByStatusAndUserId(TravelStatus travelStatus, Long userId);

List<Travel> findByUserId(Long userId);

List<Travel> findByCityIdInAndTravelOpenTrueOrderByCreatedAtDesc(List<Long> cityIds);
List<Travel> findByCityIdInAndTravelOpenTrueOrderByCreatedAtAsc(List<Long> cityIds);

@Modifying
@Transactional
@Query("UPDATE Travel t SET t.status = 'COMPLETED' WHERE t.endDate < CURRENT_DATE AND t.status != 'COMPLETED'")
void updateCompletedStatuses();
}
26 changes: 22 additions & 4 deletions src/main/java/umc/TripPiece/security/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import umc.TripPiece.apiPayload.code.status.ErrorStatus;
import umc.TripPiece.apiPayload.exception.GeneralException;

@Component
@RequiredArgsConstructor
Expand All @@ -18,11 +20,27 @@ public class JwtExceptionFilter extends OncePerRequestFilter {
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(request, response);
} catch (RuntimeException e) {
System.out.println("-------------------------------jwtExfilter");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: " + e.getMessage());
} catch (GeneralException e) {
String message = e.getMessage();

if (ErrorStatus.INVALID_TOKEN.getMessage().equals(message)) {
setResponse(response, ErrorStatus.INVALID_TOKEN);
}
else if (ErrorStatus.TOKEN_EXPIRED.getMessage().equals(message)) {
setResponse(response, ErrorStatus.TOKEN_EXPIRED);
}
else if (ErrorStatus.UNSUPPORTED_TOKEN.getMessage().equals(message)) {
setResponse(response, ErrorStatus.UNSUPPORTED_TOKEN);
}
else if (ErrorStatus.NOT_FOUND_TOKEN.getMessage().equals(message)) {
setResponse(response, ErrorStatus.NOT_FOUND_TOKEN);
}
}
}

private void setResponse(HttpServletResponse response, ErrorStatus errorStatus) throws RuntimeException, IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(errorStatus.getHttpStatus().value());
response.getWriter().print(errorStatus.getMessage());
}
}
Loading

0 comments on commit 3ecac7a

Please sign in to comment.