diff --git a/api-gateway/src/main/resources/application-prod.yaml b/api-gateway/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..fe046db5 --- /dev/null +++ b/api-gateway/src/main/resources/application-prod.yaml @@ -0,0 +1,3 @@ +management: + tracing: + enabled: true diff --git a/api-gateway/src/main/resources/application.yaml b/api-gateway/src/main/resources/application.yaml index e3ee983d..3eb0c25b 100644 --- a/api-gateway/src/main/resources/application.yaml +++ b/api-gateway/src/main/resources/application.yaml @@ -39,6 +39,13 @@ spring: filters: - JWTAuthenticationFilter - RewritePath=/user/(?/?.*), /$\{segment} + - id: photo-service-add-photo + uri: http://photo-service + predicates: + - Path=/photo/v1/photos + - Method=POST + filters: + - RewritePath=/photo/(?/?.*), /$\{segment} - id: photo-service uri: http://photo-service predicates: @@ -64,6 +71,7 @@ management: tracing: sampling: probability: 1.0 + enabled: false zipkin: tracing: endpoint: http://zipkin/api/v2/spans diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java index fd49fbe0..1be85907 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java @@ -6,6 +6,7 @@ import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import org.springframework.web.util.DefaultUriBuilderFactory; @EnableWebFlux @Configuration @@ -17,7 +18,10 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { @Bean("externalWebClient") public WebClient externalServiceWebClient() { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); return WebClient.builder() + .uriBuilderFactory(factory) .codecs(clientCodecConfigurer -> { clientCodecConfigurer .defaultCodecs() diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index 0f846e1d..a4f0efe2 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -33,12 +33,6 @@ public Mono createPhoto( String memberId, PhotoCreateRequest request ){ - - // HACK : 프엔 응급 요청으로 더미 응답 추가했습니다. - 추후 삭제 예정 - if (request.qrUrl().equals("https://mafoo.kr/")) { - return Mono.just(new PhotoResponse("id", "https://kr.object.ncloudstorage.com/mafoo//24c576bc-60b0-4e43-af9a-0c3311b97f35", LIFE_FOUR_CUTS, null)); - } - return photoService .createNewPhoto(request.qrUrl(), memberId) .map(PhotoResponse::fromEntity); diff --git a/photo-service/src/main/java/kr/mafoo/photo/domain/BrandType.java b/photo-service/src/main/java/kr/mafoo/photo/domain/BrandType.java index 141e04e1..d3f2e73a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/domain/BrandType.java +++ b/photo-service/src/main/java/kr/mafoo/photo/domain/BrandType.java @@ -7,6 +7,12 @@ public enum BrandType { PHOTOISM(Pattern.compile("https://qr\\.seobuk\\.kr/.*")), HARU_FILM(Pattern.compile("http://haru\\d+\\.mx\\d+\\.co\\.kr/.*")), DONT_LOOK_UP(Pattern.compile("https://x\\.dontlxxkup\\.kr/.*")), + MY_FOUR_CUT(Pattern.compile("https://firebasestorage\\.googleapis\\.com:443/v0/b/my4ccu\\.appspot\\.com/.*")), + PHOTOGRAY(Pattern.compile("https://pgshort\\.aprd\\.io/.*")), + MONOMANSION(Pattern.compile("https://monomansion\\.net/.*")), + PHOTO_SIGNATURE(Pattern.compile("http://photoqr3\\.kr/.*")), + PICDOT(Pattern.compile("https://picdot\\.kr/.*")), + MAFOO(Pattern.compile("https://mafoo\\.kr/.*")) ; private final Pattern urlPattern; diff --git a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java index 35db5ef9..6785e82b 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java +++ b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java @@ -62,6 +62,15 @@ public String getId() { return photoId; } + public Boolean hasOwnerMemberId() { + return this.getOwnerMemberId() != null; + } + + public PhotoEntity updateOwnerMemberId(String ownerMemberId) { + this.ownerMemberId = ownerMemberId; + return this; + } + public PhotoEntity updateAlbumId(String albumId) { this.albumId = albumId; return this; diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java index a681ab1e..87fb4cc0 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java @@ -7,6 +7,7 @@ import kr.mafoo.photo.util.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,6 +16,7 @@ public class AlbumService { private final AlbumRepository albumRepository; + @Transactional public Mono createNewAlbum(String ownerMemberId, String albumName, AlbumType albumType) { AlbumEntity albumEntity = AlbumEntity.newAlbum(IdGenerator.generate(), albumName, albumType, ownerMemberId); return albumRepository.save(albumEntity); @@ -38,6 +40,7 @@ public Mono findByAlbumId(String albumId, String requestMemberId) { }); } + @Transactional public Mono deleteAlbumById(String albumId, String requestMemberId) { return albumRepository .findById(albumId) @@ -52,6 +55,7 @@ public Mono deleteAlbumById(String albumId, String requestMemberId) { }); } + @Transactional public Mono updateAlbumName(String albumId, String albumName, String requestMemberId) { return albumRepository .findById(albumId) @@ -66,6 +70,7 @@ public Mono updateAlbumName(String albumId, String albumName, Strin }); } + @Transactional public Mono updateAlbumType(String albumId, AlbumType albumType, String requestMemberId) { return albumRepository .findById(albumId) @@ -80,6 +85,7 @@ public Mono updateAlbumType(String albumId, AlbumType albumType, St }); } + @Transactional public Mono increaseAlbumPhotoCount(String albumId, String requestMemberId) { return albumRepository .findById(albumId) @@ -94,6 +100,7 @@ public Mono increaseAlbumPhotoCount(String albumId, String requestMemberId }); } + @Transactional public Mono decreaseAlbumPhotoCount(String albumId, String requestMemberId) { if (albumId == null) { diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 5478921e..bc33cf6c 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -7,10 +7,13 @@ import kr.mafoo.photo.repository.PhotoRepository; import kr.mafoo.photo.util.IdGenerator; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +@Slf4j @RequiredArgsConstructor @Service public class PhotoService { @@ -21,6 +24,7 @@ public class PhotoService { private final QrService qrService; private final ObjectStorageService objectStorageService; + @Transactional public Mono createNewPhoto(String qrUrl, String requestMemberId) { return qrService .getFileFromQrUrl(qrUrl) @@ -37,7 +41,7 @@ public Flux findAllByAlbumId(String albumId, String requestMemberId .findById(albumId) .switchIfEmpty(Mono.error(new AlbumNotFoundException())) .flatMapMany(albumEntity -> { - if(!albumEntity.getOwnerMemberId().equals(requestMemberId)) { + if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { @@ -46,26 +50,34 @@ public Flux findAllByAlbumId(String albumId, String requestMemberId }); } + @Transactional public Mono deletePhotoById(String photoId, String requestMemberId) { return photoRepository .findById(photoId) .switchIfEmpty(Mono.error(new PhotoNotFoundException())) .flatMap(photoEntity -> { - if(!photoEntity.getOwnerMemberId().equals(requestMemberId)) { + if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { // 내 사진이 아니면 그냥 없는 사진 처리 return Mono.error(new PhotoNotFoundException()); } else { - return photoRepository.deleteById(photoId); + return photoRepository.deleteById(photoId) + .then(albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), requestMemberId)); } }); } + @Transactional public Mono updatePhotoAlbumId(String photoId, String albumId, String requestMemberId) { return photoRepository .findById(photoId) .switchIfEmpty(Mono.error(new PhotoNotFoundException())) .flatMap(photoEntity -> { - if(!photoEntity.getOwnerMemberId().equals(requestMemberId)) { + + if (!photoEntity.hasOwnerMemberId()) { + photoRepository.save(photoEntity.updateOwnerMemberId(requestMemberId)); + } + + if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { // 내 사진이 아니면 그냥 없는 사진 처리 return Mono.error(new PhotoNotFoundException()); } else { @@ -73,7 +85,7 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri .findById(albumId) .switchIfEmpty(Mono.error(new AlbumNotFoundException())) .flatMap(albumEntity -> { - if(!albumEntity.getOwnerMemberId().equals(requestMemberId)) { + if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/QrService.java b/photo-service/src/main/java/kr/mafoo/photo/service/QrService.java index 5140f51b..129a3cb8 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/QrService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/QrService.java @@ -2,19 +2,12 @@ import kr.mafoo.photo.domain.BrandType; import kr.mafoo.photo.exception.PhotoBrandNotExistsException; -import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; -import kr.mafoo.photo.exception.RedirectUriNotFoundException; import kr.mafoo.photo.service.dto.FileDto; +import kr.mafoo.photo.service.vendors.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; - -import java.net.URI; -import java.util.LinkedHashMap; -import java.util.Map; import java.util.Optional; @Slf4j @@ -22,130 +15,39 @@ @Service public class QrService { - private final WebClient externalWebClient; + private final LifeFourCutsQrVendor lifeFourCutsQrVendor; + private final PhotoismQrVendor photoismQrVendor; + private final DontLookUpQrVendor dontLookUpQrVendor; + private final HaruFilmQrVendor haruFilmQrVendor; + private final MyFourCutQrVendor myFourCutQrVendor; + private final PhotoGrayQrVendor photoGrayQrVendor; + private final MonoMansionQrVendor monoMansionQrVendor; + private final PhotoSignatureQrVendor photoSignatureQrVendor; + private final PicDotQrVendor picDotQrVendor; + private final MafooQrVendor mafooQrVendor; + public Mono getFileFromQrUrl(String qrUrl) { BrandType brandType = Optional.ofNullable(BrandType.matchBrandType(qrUrl)) .orElseThrow(PhotoBrandNotExistsException::new); - return switch (brandType) { - case LIFE_FOUR_CUTS -> createFileDto(brandType, getLifeFourCutsFiles(qrUrl)); - case PHOTOISM -> createFileDto(brandType, getPhotoismFiles(qrUrl)); - case HARU_FILM -> createFileDto(brandType, getHaruFilmFiles(qrUrl)); - case DONT_LOOK_UP -> createFileDto(brandType, getDontLookUpFiles(qrUrl)); + QrVendor qrVendor = switch (brandType) { + case LIFE_FOUR_CUTS -> lifeFourCutsQrVendor; + case PHOTOISM -> photoismQrVendor; + case HARU_FILM -> haruFilmQrVendor; + case DONT_LOOK_UP -> dontLookUpQrVendor; + case MY_FOUR_CUT -> myFourCutQrVendor; + case PHOTOGRAY -> photoGrayQrVendor; + case MONOMANSION -> monoMansionQrVendor; + case PHOTO_SIGNATURE -> photoSignatureQrVendor; + case PICDOT -> picDotQrVendor; + case MAFOO -> mafooQrVendor; }; + + return createFileDto(brandType, qrVendor.extractImageFromQrUrl(qrUrl)); } private Mono createFileDto(BrandType brandType, Mono fileMono) { return fileMono.map(file -> new FileDto(brandType, file)); } - - private Mono getLifeFourCutsFiles(String qrUrl) { - - return getRedirectUri(qrUrl) - .flatMap(redirectUri -> { - String imageUrl = extractValueFromUrl(redirectUri, "path=")[1].replace("index.html", "image.jpg"); - - // TODO : 추후 비디오 URL 추가 예정 - // String videoUrl = redirectUri.toString().replace("index.html", "video.mp4"); - - return getFileAsByte(imageUrl); - }) - .onErrorMap(e -> new PhotoQrUrlExpiredException()); - } - - private Mono getPhotoismFiles(String qrUrl) { - return getRedirectUri(qrUrl) - .flatMap(redirectUri -> { - String uid = extractValueFromUrl(redirectUri, "u=")[1]; - - return externalWebClient - .post() - .uri("https://cmsapi.seobuk.kr/v1/etc/seq/resource") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(Map.of("uid", uid)) - .retrieve() - .bodyToMono(LinkedHashMap.class) - .flatMap(responseBody -> { - LinkedHashMap content = (LinkedHashMap) responseBody.get("content"); - LinkedHashMap fileInfo = (LinkedHashMap) content.get("fileInfo"); - LinkedHashMap picFile = (LinkedHashMap) fileInfo.get("picFile"); - String imageUrl = (String) picFile.get("path"); - - return getFileAsByte(imageUrl); - }); - }) - .onErrorMap(e -> { - e.printStackTrace(); - return new PhotoQrUrlExpiredException(); - }); - } - - private Mono getHaruFilmFiles(String qrUrl) { - String[] urlValueList = extractValueFromUrl(qrUrl, "/@"); - String albumCode = urlValueList[1]; - - String baseUrl = urlValueList[0] + "/base_api?command=albumdn&albumCode="; - String imageUrl = baseUrl + albumCode + "&type=photo&file_name=output.jpg&max=10&limit=+24 hours"; - - // TODO : 추후 비디오 URL 추가 예정 - // String videoUrl = baseUrl + albumCode + "&type=video&max=10&limit=+24 hours"; - - return getFileAsByte(imageUrl) - .onErrorMap(e -> new PhotoQrUrlExpiredException()); - } - - private Mono getDontLookUpFiles(String qrUrl) { - String imageName = extractValueFromUrl(qrUrl, ".kr/image/")[1]; - - String baseUrl = "https://x.dontlxxkup.kr/uploads/"; - String imageUrl = baseUrl + imageName; - - // TODO : 추후 비디오 URL 추가 예정 - // String videoName = imageName.replace("image", "video").replace(".jpg", ".mp4"); - // String videoUrl = baseUrl + videoName; - - return getRedirectUri(qrUrl) - .flatMap(redirectUri -> { - if (redirectUri.endsWith("/delete")) { - return Mono.error(new PhotoQrUrlExpiredException()); - } else { - return getFileAsByte(imageUrl); - } - }) - .onErrorResume( - RedirectUriNotFoundException.class, e -> getFileAsByte(imageUrl) - ); - } - - private Mono getRedirectUri(String url) { - return externalWebClient - .get() - .uri(url) - .retrieve() - .toBodilessEntity() - .flatMap(response -> { - URI redirectUri = response.getHeaders().getLocation(); - if (redirectUri == null) { - return Mono.error(new RedirectUriNotFoundException()); - } else { - return Mono.just(redirectUri.toString()); - } - }); - } - - private String[] extractValueFromUrl(String url, String delimiter) { - return url.split(delimiter); - } - - private Mono getFileAsByte(String url) { - return externalWebClient - .get() - .uri(url) - .accept(MediaType.APPLICATION_OCTET_STREAM) - .retrieve() - .bodyToMono(byte[].class); - } - - } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/DontLookUpQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/DontLookUpQrVendor.java new file mode 100644 index 00000000..db3a31f9 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/DontLookUpQrVendor.java @@ -0,0 +1,39 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.exception.RedirectUriNotFoundException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class DontLookUpQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + String imageName = qrUrl.split(".kr/image/")[1]; + + String baseUrl = "https://x.dontlxxkup.kr/uploads/"; + String imageUrl = baseUrl + imageName; + + // TODO : 추후 비디오 URL 추가 예정 + // String videoName = imageName.replace("image", "video").replace(".jpg", ".mp4"); + // String videoUrl = baseUrl + videoName; + + return WebClientUtil.getRedirectUri(webClient, qrUrl) + .flatMap(redirectUri -> { + if (redirectUri.endsWith("/delete")) { + return Mono.error(new PhotoQrUrlExpiredException()); + } else { + return WebClientUtil.getBlob(webClient, imageUrl); + } + }) + .onErrorResume( + RedirectUriNotFoundException.class, e -> WebClientUtil.getBlob(webClient, imageUrl) + ); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/HaruFilmQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/HaruFilmQrVendor.java new file mode 100644 index 00000000..242f5245 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/HaruFilmQrVendor.java @@ -0,0 +1,29 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class HaruFilmQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + String[] urlValueList = qrUrl.split("/@"); + String albumCode = urlValueList[1]; + + String baseUrl = urlValueList[0] + "/base_api?command=albumdn&albumCode="; + String imageUrl = baseUrl + albumCode + "&type=photo&file_name=output.jpg&max=10&limit=+24 hours"; + + // TODO : 추후 비디오 URL 추가 예정 + // String videoUrl = baseUrl + albumCode + "&type=video&max=10&limit=+24 hours"; + + return WebClientUtil.getBlob(webClient, imageUrl) + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/LifeFourCutsQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/LifeFourCutsQrVendor.java new file mode 100644 index 00000000..c5c52a84 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/LifeFourCutsQrVendor.java @@ -0,0 +1,28 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class LifeFourCutsQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + return WebClientUtil.getRedirectUri(webClient, qrUrl) + .flatMap(redirectUri -> { + String imageUrl = redirectUri.split("path=")[1].replace("index.html", "image.jpg"); + + // TODO : 추후 비디오 URL 추가 예정 + // String videoUrl = redirectUri.toString().replace("index.html", "video.mp4"); + + return WebClientUtil.getBlob(webClient, imageUrl); + }) + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MafooQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MafooQrVendor.java new file mode 100644 index 00000000..10bb1f06 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MafooQrVendor.java @@ -0,0 +1,23 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class MafooQrVendor implements QrVendor { + private final WebClient webClient; + private final static String sampleImageUrl + = "https://i.ibb.co/VY7s8m1/c8dbdc0d-65d6-490b-ac68-c37d99d494bf.jpg"; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + return WebClientUtil + .getBlobByAnyMediaType(webClient, sampleImageUrl) //just image url + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MonoMansionQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MonoMansionQrVendor.java new file mode 100644 index 00000000..2e28f7fe --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MonoMansionQrVendor.java @@ -0,0 +1,32 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class MonoMansionQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + String qrCode = extractQrCodeFromUrl(qrUrl); + String imageUrl = String.format("https://monomansion.net/api/download.php?qrcode=%s&type=P", qrCode); + return WebClientUtil + .getBlob(webClient, imageUrl) + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } + + private String extractQrCodeFromUrl(String url) { + return UriComponentsBuilder + .fromUriString(url) + .build() + .getQueryParams() + .getFirst("qrcode"); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MyFourCutQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MyFourCutQrVendor.java new file mode 100644 index 00000000..7b6a6519 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/MyFourCutQrVendor.java @@ -0,0 +1,21 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class MyFourCutQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + return WebClientUtil + .getBlob(webClient, qrUrl) //just image url + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoGrayQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoGrayQrVendor.java new file mode 100644 index 00000000..af87e6b1 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoGrayQrVendor.java @@ -0,0 +1,50 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.apache.http.client.utils.URLEncodedUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@RequiredArgsConstructor +@Component +public class PhotoGrayQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + return WebClientUtil // https://pgshort.aprd.io/{qr} + .getRedirectUri(webClient, qrUrl) //https://photogray-download.aprd.io?id={base64} + .flatMap((currentUrl) -> { + String encodedStr = extractIdFromUrl(currentUrl); + String decodedStr = new String(Base64.getDecoder().decode(encodedStr)); + String sessionId = extractSessionIdFromQueryString(decodedStr); + String imageUrl = String.format("https://pg-qr-resource.aprd.io/%s/image.jpg", sessionId); + return WebClientUtil.getBlob(webClient, imageUrl); + }) //https://pg-qr-resource.aprd.io/{sessionId}/image.jpg + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } + + private String extractIdFromUrl(String url) { + return UriComponentsBuilder + .fromUriString(url) + .build() + .getQueryParams() + .getFirst("id"); + } + + private String extractSessionIdFromQueryString(String queryString) { + return URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8) + .stream() + .filter(e -> e.getName().equals("sessionId")) + .findFirst() + .orElseThrow() + .getValue(); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoSignatureQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoSignatureQrVendor.java new file mode 100644 index 00000000..4abe690a --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoSignatureQrVendor.java @@ -0,0 +1,22 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class PhotoSignatureQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + String imageUrl = qrUrl.replace("index.html", "a.jpg"); + return WebClientUtil + .getBlobByAnyMediaType(webClient, imageUrl) + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoismQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoismQrVendor.java new file mode 100644 index 00000000..c909aff8 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PhotoismQrVendor.java @@ -0,0 +1,46 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class PhotoismQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + return WebClientUtil.getRedirectUri(webClient, qrUrl) + .flatMap(redirectUri -> { + String uid = redirectUri.split("u=")[1]; + + return webClient + .post() + .uri("https://cmsapi.seobuk.kr/v1/etc/seq/resource") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("uid", uid)) + .retrieve() + .bodyToMono(LinkedHashMap.class) + .flatMap(responseBody -> { + LinkedHashMap content = (LinkedHashMap) responseBody.get("content"); + LinkedHashMap fileInfo = (LinkedHashMap) content.get("fileInfo"); + LinkedHashMap picFile = (LinkedHashMap) fileInfo.get("picFile"); + String imageUrl = (String) picFile.get("path"); + + return WebClientUtil.getBlob(webClient, imageUrl); + }); + }) + .onErrorMap(e -> { + e.printStackTrace(); + return new PhotoQrUrlExpiredException(); + }); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PicDotQrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PicDotQrVendor.java new file mode 100644 index 00000000..e04b03b7 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/PicDotQrVendor.java @@ -0,0 +1,32 @@ +package kr.mafoo.photo.service.vendors; + +import kr.mafoo.photo.exception.PhotoQrUrlExpiredException; +import kr.mafoo.photo.util.WebClientUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class PicDotQrVendor implements QrVendor { + private final WebClient webClient; + + @Override + public Mono extractImageFromQrUrl(String qrUrl) { + String qrCode = extractQrCodeFromUrl(qrUrl); + String imageUrl = String.format("https://picdot.kr/api/download.php?qrcode=%s&type=P", qrCode); + return WebClientUtil + .getBlob(webClient, imageUrl) + .onErrorMap(e -> new PhotoQrUrlExpiredException()); + } + + private String extractQrCodeFromUrl(String url) { + return UriComponentsBuilder + .fromUriString(url) + .build() + .getQueryParams() + .getFirst("qrcode"); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/vendors/QrVendor.java b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/QrVendor.java new file mode 100644 index 00000000..451bc14f --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/vendors/QrVendor.java @@ -0,0 +1,7 @@ +package kr.mafoo.photo.service.vendors; + +import reactor.core.publisher.Mono; + +public interface QrVendor { + Mono extractImageFromQrUrl(String qrUrl); +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/util/WebClientUtil.java b/photo-service/src/main/java/kr/mafoo/photo/util/WebClientUtil.java new file mode 100644 index 00000000..b4687655 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/util/WebClientUtil.java @@ -0,0 +1,43 @@ +package kr.mafoo.photo.util; + +import kr.mafoo.photo.exception.RedirectUriNotFoundException; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.net.URI; + +public class WebClientUtil { + public static Mono getRedirectUri(WebClient client, String url) { + return client + .get() + .uri(url) + .retrieve() + .toBodilessEntity() + .flatMap(response -> { + URI redirectUri = response.getHeaders().getLocation(); + if (redirectUri == null) { + return Mono.error(new RedirectUriNotFoundException()); + } else { + return Mono.just(redirectUri.toString()); + } + }); + } + + public static Mono getBlob(WebClient client, String url) { + return client + .get() + .uri(url) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .retrieve() + .bodyToMono(byte[].class); + } + + public static Mono getBlobByAnyMediaType(WebClient client, String url) { + return client + .get() + .uri(url) + .retrieve() + .bodyToMono(byte[].class); + } +} diff --git a/photo-service/src/main/resources/application-prod.yaml b/photo-service/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..fe046db5 --- /dev/null +++ b/photo-service/src/main/resources/application-prod.yaml @@ -0,0 +1,3 @@ +management: + tracing: + enabled: true diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index dc14e9cb..6845a49b 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -37,6 +37,7 @@ management: tracing: sampling: probability: 1.0 + enabled: false zipkin: tracing: endpoint: http://zipkin/api/v2/spans diff --git a/photo-service/src/main/resources/db/migration/V5__changeOwnerMemberIdAsNullableInPhotoTable.sql b/photo-service/src/main/resources/db/migration/V5__changeOwnerMemberIdAsNullableInPhotoTable.sql new file mode 100644 index 00000000..d9699208 --- /dev/null +++ b/photo-service/src/main/resources/db/migration/V5__changeOwnerMemberIdAsNullableInPhotoTable.sql @@ -0,0 +1,2 @@ +alter table photo + modify owner_member_id char(26) null comment '사진소유자아이디'; \ No newline at end of file diff --git a/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java b/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java index 2fe2fd61..e097d8c3 100644 --- a/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java +++ b/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java @@ -9,7 +9,9 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Tag(name = "인증(로그인) 관련 API", description = "토큰 발행, 로그인 등 API") @@ -19,13 +21,15 @@ public interface AuthApi { @Operation(summary = "카카오 로그인", description = "카카오 인가 코드로 로그인(토큰 발행)합니다.") @PostMapping("/login/kakao") Mono loginWithKakao( - @RequestBody KakaoLoginRequest request + @RequestBody KakaoLoginRequest request, + ServerWebExchange exchange ); @Operation(summary = "애플 로그인" , description = "애플 인가 코드로 로그인(토큰 발행)합니다.") @PostMapping("/login/apple") Mono loginWithApple( - @RequestBody AppleLoginRequest request + @RequestBody AppleLoginRequest request, + ServerWebExchange exchange ); @Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 기존 토큰을 갱신합니다.") diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index b274df31..57eb2beb 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -5,9 +5,11 @@ import kr.mafoo.user.controller.dto.request.KakaoLoginRequest; import kr.mafoo.user.controller.dto.request.TokenRefreshRequest; import kr.mafoo.user.controller.dto.response.LoginResponse; +import kr.mafoo.user.domain.AuthToken; import kr.mafoo.user.service.AuthService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @RequiredArgsConstructor @@ -16,23 +18,34 @@ public class AuthController implements AuthApi { private final AuthService authService; @Override - public Mono loginWithKakao(KakaoLoginRequest request) { + public Mono loginWithKakao(KakaoLoginRequest request, ServerWebExchange exchange) { + String userAgent = getUserAgent(exchange); return authService - .loginWithKakao(request.code()) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .loginWithKakao(request.accessToken(), userAgent) + .map(this::toLoginResponse); } @Override - public Mono loginWithApple(AppleLoginRequest request) { + public Mono loginWithApple(AppleLoginRequest request, ServerWebExchange exchange) { + String userAgent = getUserAgent(exchange); return authService - .loginWithApple(request.identityToken()) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .loginWithApple(request.identityToken(), userAgent) + .map(this::toLoginResponse); } @Override public Mono loginWithRefreshToken(TokenRefreshRequest request) { return authService .loginWithRefreshToken(request.refreshToken()) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .map(this::toLoginResponse); } + + private String getUserAgent(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().getFirst("User-Agent"); + } + + private LoginResponse toLoginResponse(AuthToken authToken) { + return new LoginResponse(authToken.accessToken(), authToken.refreshToken()); + } + } diff --git a/user-service/src/main/java/kr/mafoo/user/controller/dto/request/KakaoLoginRequest.java b/user-service/src/main/java/kr/mafoo/user/controller/dto/request/KakaoLoginRequest.java index e03d20be..bbed68df 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/dto/request/KakaoLoginRequest.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/dto/request/KakaoLoginRequest.java @@ -4,7 +4,7 @@ @Schema(description = "카카오 로그인 요청") public record KakaoLoginRequest( - @Schema(description = "인가 코드", example = "test") - String code + @Schema(description = "카카오 엑세스 토큰", example = "test") + String accessToken ) { } diff --git a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java index 8f7c11ee..fc1ee2bb 100644 --- a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java +++ b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java @@ -3,7 +3,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Persistable; @@ -24,7 +23,6 @@ public class MemberEntity implements Persistable { @Column("name") private String name; - @CreatedDate @Column("created_at") private LocalDateTime createdAt; @@ -54,6 +52,7 @@ public static MemberEntity newMember(String id, String name, String profileImage member.id = id; member.name = name; member.profileImageUrl = profileImageUrl; + member.createdAt = LocalDateTime.now(); member.isNew = true; return member; } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 4ace3f6c..15a23748 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -20,10 +20,10 @@ import kr.mafoo.user.util.NicknameGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; -import java.security.Key; import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Base64; @@ -40,29 +40,32 @@ public class AuthService { private final AppleOAuthProperties appleOAuthProperties; private final ObjectMapper objectMapper; - - public Mono loginWithKakao(String code) { - return getKakaoTokenWithCode(code) - .flatMap(this::getUserInfoWithKakaoToken) + @Transactional + public Mono loginWithKakao(String kakaoAccessToken, String userAgent) { + return getUserInfoWithKakaoToken(kakaoAccessToken) .flatMap(kakaoLoginInfo -> getOrCreateMember( IdentityProvider.KAKAO, kakaoLoginInfo.id(), kakaoLoginInfo.nickname(), - kakaoLoginInfo.profileImageUrl() + kakaoLoginInfo.profileImageUrl(), + userAgent )); } - public Mono loginWithApple(String identityToken) { + @Transactional + public Mono loginWithApple(String identityToken, String userAgent) { return getApplePublicKeys() .flatMap(keyObj -> getUserInfoWithAppleAccessToken(keyObj.keys(), identityToken)) .flatMap(appleLoginInfo -> getOrCreateMember( IdentityProvider.APPLE, appleLoginInfo.id(), NicknameGenerator.generate(), - null + null, + userAgent )); } + @Transactional public Mono loginWithRefreshToken(String refreshToken){ return Mono .fromCallable(() -> jwtTokenService.extractUserIdFromRefreshToken(refreshToken)) @@ -73,10 +76,10 @@ public Mono loginWithRefreshToken(String refreshToken){ }); } - private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl) { + private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) { return socialMemberRepository .findByIdentityProviderAndId(provider, id) - .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl)) + .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl, userAgent)) .map(socialMember -> { String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId()); @@ -84,9 +87,9 @@ private Mono getOrCreateMember(IdentityProvider provider, String id, }); } - private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl) { + private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) { return memberService - .createNewMember(username, profileImageUrl) + .createNewMember(username, profileImageUrl, userAgent) .flatMap(newMember -> socialMemberRepository.save( SocialMemberEntity.newSocialMember(provider, id, newMember.getId()) )); diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 9db3bd7e..63af4cb7 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -5,13 +5,17 @@ import kr.mafoo.user.repository.MemberRepository; import kr.mafoo.user.util.IdGenerator; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Mono; +@Slf4j @RequiredArgsConstructor @Service public class MemberService { private final MemberRepository memberRepository; + private final SlackService slackService; public Mono quitMemberByMemberId(String memberId) { return memberRepository.deleteMemberById(memberId); @@ -23,8 +27,20 @@ public Mono getMemberByMemberId(String memberId) { .switchIfEmpty(Mono.error(new MemberNotFoundException())); } - public Mono createNewMember(String username, String profileImageUrl) { + @Transactional + public Mono createNewMember(String username, String profileImageUrl, String userAgent) { MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl); - return memberRepository.save(memberEntity); + + return memberRepository.save(memberEntity) + .flatMap(savedMember -> + slackService.sendNewMemberNotification( + memberEntity.getId(), + memberEntity.getName(), + memberEntity.getProfileImageUrl(), + memberEntity.getCreatedAt().toString(), + userAgent + ) + .then(Mono.just(savedMember)) + ); } } diff --git a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java new file mode 100644 index 00000000..81f10d4b --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java @@ -0,0 +1,117 @@ +package kr.mafoo.user.service; + +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.model.block.Blocks; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.MarkdownTextObject; +import com.slack.api.model.block.composition.TextObject; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.slack.api.model.block.Blocks.*; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; +import static com.slack.api.model.block.composition.BlockCompositions.plainText; + +@Service +@RequiredArgsConstructor +public class SlackService { + + @Value(value = "${slack.webhook.channel.error}") + private String errorChannel; + + @Value(value = "${slack.webhook.channel.member}") + private String memberChannel; + + private final MethodsClient methodsClient; + + public void sendErrorNotification(Throwable throwable, String method, String uri, String statusCode, long executionTime, String userAgent) { + try { + List textObjects = new ArrayList<>(); + + textObjects.add(markdownText(">*예상하지 못한 에러가 발생했습니다!*\n")); + textObjects.add(markdownText("\n")); + + textObjects.add(markdownText("*메소드:* \n`" + method + "`\n")); + textObjects.add(markdownText("*URI:* \n`" + uri + "`\n")); + textObjects.add(markdownText("*상태코드:* \n`" + statusCode + "`\n")); + textObjects.add(markdownText("*메세지:* \n`" + throwable.getMessage() + "`\n")); + textObjects.add(markdownText("*소요시간:* \n`" + executionTime + " ms`\n")); + textObjects.add(markdownText("*사용자:* \n`" + userAgent + "`\n")); + + ChatPostMessageRequest request = ChatPostMessageRequest + .builder() + .channel(errorChannel) + .blocks( + asBlocks( + divider(), + section( + section -> section.fields(textObjects) + ) + )) + .build(); + + methodsClient.chatPostMessage(request); + } catch (SlackApiException | IOException e) { + throw new RuntimeException("Can't send Slack Message.", e); + } + } + + public Mono sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, String userAgent) { + return Mono.fromCallable(() -> { + List layoutBlocks = new ArrayList<>(); + + layoutBlocks.add( + Blocks.header( + headerBlockBuilder -> + headerBlockBuilder.text(plainText("🎉 신규 사용자 가입")))); + layoutBlocks.add(divider()); + + MarkdownTextObject userIdMarkdown = + MarkdownTextObject.builder().text("`사용자 ID`\n" + memberId).build(); + + MarkdownTextObject userNameMarkdown = + MarkdownTextObject.builder().text("`사용자 닉네임`\n" + memberName).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userIdMarkdown, userNameMarkdown)))); + + MarkdownTextObject userProfileImageMarkdown = + MarkdownTextObject.builder().text("`프로필 이미지`\n" + memberProfileImageUrl).build(); + + MarkdownTextObject userCreatedAtMarkdown = + MarkdownTextObject.builder().text("`가입 일자`\n" + memberCreatedAt).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)))); + + MarkdownTextObject userUserAgentMarkdown = + MarkdownTextObject.builder().text("`가입 환경`\n" + userAgent).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userUserAgentMarkdown)))); + + ChatPostMessageRequest chatPostMessageRequest = + ChatPostMessageRequest + .builder() + .text("신규 사용자 가입 알림") + .channel(memberChannel) + .blocks(layoutBlocks) + .build(); + + return methodsClient.chatPostMessage(chatPostMessageRequest); + }) + .then(); + } + +} diff --git a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java deleted file mode 100644 index b0aafcca..00000000 --- a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java +++ /dev/null @@ -1,65 +0,0 @@ -package kr.mafoo.user.slack; - -import com.slack.api.Slack; -import com.slack.api.methods.MethodsClient; -import com.slack.api.methods.SlackApiException; -import com.slack.api.methods.request.chat.ChatPostMessageRequest; -import com.slack.api.model.block.composition.TextObject; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static com.slack.api.model.block.Blocks.*; -import static com.slack.api.model.block.composition.BlockCompositions.markdownText; - -@Service -@RequiredArgsConstructor -public class SlackNotificationService { - - @Value(value = "${slack.webhook.token}") - private String token; - - @Value(value = "${slack.webhook.channel.error}") - private String errorChannel; - - @Value(value = "${slack.webhook.channel.member}") - private String memberChannel; - - public void sendErrorNotification(Throwable throwable, String method, String uri, String statusCode, long executionTime, String userAgent) { - try { - List textObjects = new ArrayList<>(); - - textObjects.add(markdownText(">*예상하지 못한 에러가 발생했습니다!*\n")); - textObjects.add(markdownText("\n")); - - textObjects.add(markdownText("*메소드:* \n`" + method + "`\n")); - textObjects.add(markdownText("*URI:* \n`" + uri + "`\n")); - textObjects.add(markdownText("*상태코드:* \n`" + statusCode + "`\n")); - textObjects.add(markdownText("*메세지:* \n`" + throwable.getMessage() + "`\n")); - textObjects.add(markdownText("*소요시간:* \n`" + executionTime + " ms`\n")); - textObjects.add(markdownText("*사용자:* \n`" + userAgent + "`\n")); - - MethodsClient methods = Slack.getInstance().methods(token); - ChatPostMessageRequest request = ChatPostMessageRequest - .builder() - .channel(errorChannel) - .blocks( - asBlocks( - divider(), - section( - section -> section.fields(textObjects) - ) - )) - .build(); - - methods.chatPostMessage(request); - } catch (SlackApiException | IOException e) { - throw new RuntimeException("Can't send Slack Message.", e); - } - } - -} diff --git a/user-service/src/main/resources/application-prod.yaml b/user-service/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..fe046db5 --- /dev/null +++ b/user-service/src/main/resources/application-prod.yaml @@ -0,0 +1,3 @@ +management: + tracing: + enabled: true diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index d6128548..bfa2a3c1 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -39,6 +39,7 @@ management: tracing: sampling: probability: 1.0 + enabled: false zipkin: tracing: endpoint: http://zipkin/api/v2/spans @@ -48,3 +49,4 @@ slack: token: ${SLACK_TOKEN} channel: error: ${SLACK_ERROR_CHANNEL} + member: ${SLACK_MEMBER_CHANNEL} \ No newline at end of file