Skip to content

Commit

Permalink
Merge pull request #75 from YAPP-Github/dev
Browse files Browse the repository at this point in the history
[PROD 배포] v.
  • Loading branch information
gmkim20713 authored Nov 7, 2024
2 parents 373edbc + 2bba7a5 commit 389e9cb
Show file tree
Hide file tree
Showing 35 changed files with 1,243 additions and 249 deletions.
3 changes: 2 additions & 1 deletion photo-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation("io.opentelemetry:opentelemetry-exporter-zipkin:1.40.0")
implementation("io.micrometer:micrometer-registry-prometheus:1.13.2")
implementation("com.slack.api:slack-api-client:1.40.3")
implementation("net.bramp.ffmpeg:ffmpeg:0.8.0")
}

tasks.withType<Test> {
Expand All @@ -52,7 +53,7 @@ jib {
val imageTag: String? = System.getenv("IMAGE_TAG")
val serverPort: String = System.getenv("SERVER_PORT") ?: "8080"
from {
image = "amazoncorretto:17-alpine3.17-jdk"
image = "spinachpasta/photo-service-base:latest"
}
to {
image = imageName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.mafoo.photo.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.mafoo.photo.annotation.RequestMemberId;
import kr.mafoo.photo.controller.dto.request.ObjectStoragePreSignedUrlRequest;
import kr.mafoo.photo.controller.dto.response.PreSignedUrlResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@Validated
@Tag(name = "Object Storage 관련 API", description = "Object Storage 관련 API")
@RequestMapping("/v1/object-storage")
public interface ObjectStorageApi {

@Operation(summary = "Pre-signed Url 요청", description = "Pre-signed Url 목록을 발급합니다.")
@PostMapping
Mono<PreSignedUrlResponse> createPreSignedUrls(
@RequestMemberId
String memberId,

@RequestBody
ObjectStoragePreSignedUrlRequest request
);
}
41 changes: 32 additions & 9 deletions photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import jakarta.validation.Valid;
import kr.mafoo.photo.annotation.RequestMemberId;
import kr.mafoo.photo.annotation.ULID;
import kr.mafoo.photo.controller.dto.request.PhotoCreateRequest;
import kr.mafoo.photo.controller.dto.request.PhotoBulkUpdateAlbumIdRequest;
import kr.mafoo.photo.controller.dto.request.PhotoUpdateAlbumIdRequest;
import kr.mafoo.photo.controller.dto.request.PhotoUpdateDisplayIndexRequest;
import kr.mafoo.photo.controller.dto.request.*;
import kr.mafoo.photo.controller.dto.response.PhotoResponse;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
Expand All @@ -31,18 +28,44 @@ Flux<PhotoResponse> getPhotos(
@ULID
@Parameter(description = "앨범 ID", example = "test_album_id")
@RequestParam
String albumId
String albumId,

@Parameter(description = "정렬 종류", example = "ASC | DESC")
@RequestParam(required = false)
String sort
);

@Operation(summary = "(수정 이전) QR 사진 업로드", description = "QR을 사용해 사진을 업로드합니다.")
@PostMapping(value = "")
Mono<PhotoResponse> uploadQrPhotoOriginal(
@RequestMemberId
String memberId,

@Valid
@RequestBody
PhotoQrUploadRequest request
);

@Operation(summary = "QR 사진 업로드", description = "QR을 사용해 사진을 업로드합니다.")
@PostMapping(value = "/qr")
Mono<PhotoResponse> uploadQrPhoto(
@RequestMemberId
String memberId,

@Valid
@RequestBody
PhotoQrUploadRequest request
);

@Operation(summary = "사진 생성", description = "사진을 생성합니다.")
@PostMapping
Mono<PhotoResponse> createPhoto(
@Operation(summary = "파일(url) 사진 n건 업로드", description = "파일(url)을 사용해 사진을 업로드합니다.")
@PostMapping(value = "/file-urls")
Flux<PhotoResponse> uploadFileUrlPhoto(
@RequestMemberId
String memberId,

@Valid
@RequestBody
PhotoCreateRequest request
PhotoFileUrlUploadRequest request
);

@Operation(summary = "사진 파일로 업로드", description = "사진을 직접 업로드합니다.")
Expand Down
37 changes: 37 additions & 0 deletions photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kr.mafoo.photo.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import kr.mafoo.photo.annotation.RequestMemberId;
import kr.mafoo.photo.controller.dto.request.RecapCreateRequest;
import kr.mafoo.photo.controller.dto.response.RecapResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@Validated
@Tag(name = "리캡 관련 API", description = "리캡 생성 API")
@RequestMapping("/v1/recaps")
public interface RecapApi {
@Operation(summary = "리캡 생성", description = "앨범의 리캡을 생성합니다.")
@PostMapping
Mono<RecapResponse> createRecap(
@RequestMemberId
String memberId,

@Valid
@RequestBody
RecapCreateRequest request,

@Parameter(description = "정렬 종류", example = "ASC | DESC")
@RequestParam(required = false)
String sort,

// Authorization Header를 받아올 목적
ServerHttpRequest serverHttpRequest
);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.mafoo.photo.config;

import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Slf4j
@Configuration
public class FFmpegConfig {

@Value("${ffmpeg.path}")
private String ffmpegPath;

@Bean
public FFmpegExecutor ffMpegExecutor() throws IOException {
FFmpeg ffmpeg = new FFmpeg(ffmpegPath);
return new FFmpegExecutor(ffmpeg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//package kr.mafoo.photo.config;
//
//import org.springframework.core.io.buffer.DataBuffer;
//import org.springframework.core.io.buffer.DataBufferUtils;
//import org.springframework.http.server.reactive.ServerHttpRequest;
//import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
//import org.springframework.stereotype.Component;
//import org.springframework.web.server.ServerWebExchange;
//import org.springframework.web.server.ServerWebExchangeDecorator;
//import org.springframework.web.server.WebFilter;
//import org.springframework.web.server.WebFilterChain;
//import reactor.core.publisher.Flux;
//import reactor.core.publisher.Mono;
//
//@Component
//public class RequestBodyCachingFilter implements WebFilter {
//
// // TODO: 추후 정리 필요
//
// @Override
// public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// return DataBufferUtils.join(exchange.getRequest().getBody())
// .flatMap(dataBuffer -> {
// byte[] bytes = new byte[dataBuffer.readableByteCount()];
// dataBuffer.read(bytes);
// DataBufferUtils.release(dataBuffer);
//
// ServerHttpRequestDecorator decoratedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
// @Override
// public Flux<DataBuffer> getBody() {
// return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes));
// }
// };
//
// ServerWebExchangeDecorator decoratedExchange = new ServerWebExchangeDecorator(exchange) {
// @Override
// public ServerHttpRequest getRequest() {
// return decoratedRequest;
// }
// };
//
// return chain.filter(decoratedExchange);
// });
// }
//}
//
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@
import kr.mafoo.photo.controller.dto.response.ErrorResponse;
import kr.mafoo.photo.exception.DomainException;
import kr.mafoo.photo.exception.ErrorCode;
import kr.mafoo.photo.exception.PhotoBrandNotExistsException;
import kr.mafoo.photo.service.SlackService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@RestControllerAdvice
@RequiredArgsConstructor
public class WebExceptionHandler {

private final Logger logger = LoggerFactory.getLogger(WebExceptionHandler.class);
private final SlackService slackService;

@ExceptionHandler(DomainException.class)
public ResponseEntity<ErrorResponse> handleDomainException(DomainException exception) {
return ResponseEntity
Expand Down Expand Up @@ -42,4 +61,99 @@ public ResponseEntity<ErrorResponse> validException(Exception ex) {
.badRequest()
.body(response);
}

@ExceptionHandler(PhotoBrandNotExistsException.class)
public Mono<ResponseEntity<ErrorResponse>> handlePhotoBrandNotExistsException(ServerWebExchange exchange, PhotoBrandNotExistsException exception) {
String method = extractMethod(exchange);
String userAgent = extractUserAgent(exchange);
String fullPath = extractURI(exchange);
String originIp = extractOriginIp(exchange);

return extractRequestBody(exchange).flatMap(requestBody -> {
logException(method, fullPath, originIp, userAgent, exception);

return slackService.sendQrRelatedErrorNotification(
method, fullPath, requestBody, originIp, userAgent, exception.getMessage()
).then(Mono.just(
ResponseEntity
.badRequest()
.body(ErrorResponse.fromErrorCode(exception.getErrorCode()))
));
});
}

@ExceptionHandler(ResponseStatusException.class)
public Mono<ResponseEntity<String>> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException exception) {
return handleExceptionInternal(exchange, exception, (HttpStatus) exception.getStatusCode());
}

@ExceptionHandler(Exception.class)
public Mono<ResponseEntity<String>> handleGenericException(ServerWebExchange exchange, Exception exception) {
return handleExceptionInternal(exchange, exception, HttpStatus.INTERNAL_SERVER_ERROR);
}

private Mono<ResponseEntity<String>> handleExceptionInternal(ServerWebExchange exchange, Exception exception, HttpStatus status) {
String method = extractMethod(exchange);
String userAgent = extractUserAgent(exchange);
String fullPath = extractURI(exchange);
String originIp = extractOriginIp(exchange);

return extractRequestBody(exchange).flatMap(requestBody -> {

logException(method, fullPath, originIp, userAgent, exception);

if (status == HttpStatus.INTERNAL_SERVER_ERROR) {
return slackService.sendErrorNotification(
method, fullPath, requestBody, originIp, userAgent, exception.getMessage()
).then(Mono.just(new ResponseEntity<>("Internal Server Error", status)));
}

return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status));
});
}

private String extractMethod(ServerWebExchange exchange) {
return exchange.getRequest().getMethod().toString();
}

private String extractUserAgent(ServerWebExchange exchange) {
return exchange.getRequest().getHeaders().getFirst("User-Agent");
}

private String extractURI(ServerWebExchange exchange) {
var request = exchange.getRequest();

String scheme = request.getURI().getScheme();
String host = request.getURI().getHost();
int port = request.getURI().getPort();
String fullPath = request.getURI().getRawPath();
String query = request.getURI().getQuery();

String baseUrl = (port != -1) ? host + ":" + port : host;
String uriWithHost = scheme + "://" + baseUrl + fullPath;

return (query != null && !query.isEmpty()) ? uriWithHost + "?" + query : uriWithHost;
}

private String extractOriginIp(ServerWebExchange exchange) {
String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
return Optional.ofNullable(proxyIp)
.orElseGet(() -> remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "UNKNOWN SOURCE");
}

private Mono<String> extractRequestBody(ServerWebExchange exchange) {
return exchange.getRequest().getBody().map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);

return new String(bytes, StandardCharsets.UTF_8);
}).reduce(new StringBuilder(), StringBuilder::append)
.map(StringBuilder::toString);
}

private void logException(String method, String fullPath, String originIp, String userAgent, Exception exception) {
logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, exception.getMessage(), userAgent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.mafoo.photo.controller;

import kr.mafoo.photo.api.ObjectStorageApi;
import kr.mafoo.photo.controller.dto.request.*;
import kr.mafoo.photo.controller.dto.response.PreSignedUrlResponse;
import kr.mafoo.photo.service.ObjectStorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RequiredArgsConstructor
@RestController
public class ObjectStorageController implements ObjectStorageApi {

private final ObjectStorageService objectStorageService;

@Override
public Mono<PreSignedUrlResponse> createPreSignedUrls(
String memberId,
ObjectStoragePreSignedUrlRequest request
) {
return objectStorageService
.createPreSignedUrls(request.fileNames(), memberId)
.map(PreSignedUrlResponse::fromStringArray);
}
}
Loading

0 comments on commit 389e9cb

Please sign in to comment.