Skip to content

Commit

Permalink
feat: branch 링크 생성 API 구현 (#246)
Browse files Browse the repository at this point in the history
* rename: DTO를 infra 모듈에서 api 모듈로 이동

* rename: 파일 관련 API DTO 및 디렉토리 변경

* feat: branch 링크 생성 API 구현

* test: branch 링크 생성 API 테스트 코드 작성

* refactor: 유튜브 링크 유효성 검사 로직 리팩토링

* test: branch API 호출 실패 케이스에 대한 테스트 코드 추가
  • Loading branch information
leeeeeyeon authored Jul 23, 2024
1 parent 0ae4e77 commit 2ac9c30
Show file tree
Hide file tree
Showing 21 changed files with 340 additions and 41 deletions.
24 changes: 22 additions & 2 deletions packy-api/src/main/java/com/dilly/admin/api/AdminController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.dilly.admin.api;

import com.dilly.admin.application.AdminService;
import com.dilly.admin.dto.request.BranchRequest;
import com.dilly.admin.dto.response.BoxImgResponse;
import com.dilly.admin.dto.response.ImgResponse;
import com.dilly.admin.dto.response.MusicResponse;
import com.dilly.admin.dto.response.SettingResponse;
import com.dilly.admin.dto.response.UrlResponse;
import com.dilly.admin.dto.response.YoutubeUrlValidationResponse;
import com.dilly.application.BranchService;
import com.dilly.application.YoutubeService;
import com.dilly.dto.response.YoutubeUrlValidationResponse;
import com.dilly.exception.ErrorCode;
import com.dilly.gift.dto.response.EnvelopeListResponse;
import com.dilly.global.response.DataResponseDto;
Expand All @@ -22,6 +25,8 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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.RestController;
Expand All @@ -34,6 +39,7 @@ public class AdminController {

private final AdminService adminService;
private final YoutubeService youtubeService;
private final BranchService branchService;

@Operation(summary = "서버 상태 체크")
@GetMapping("/health")
Expand Down Expand Up @@ -91,7 +97,10 @@ public DataResponseDto<YoutubeUrlValidationResponse> validateYoutubeUrl(
@Schema(description = "유튜브 링크", type = "string")
@RequestParam(value = "url")
String url) {
return DataResponseDto.from(youtubeService.validateYoutubeUrl(url));
YoutubeUrlValidationResponse validationResponse = YoutubeUrlValidationResponse.from(
youtubeService.validateYoutubeUrl(url));

return DataResponseDto.from(validationResponse);
}

@Operation(summary = "설정 링크 조회", description = """
Expand All @@ -105,4 +114,15 @@ public DataResponseDto<YoutubeUrlValidationResponse> validateYoutubeUrl(
public DataResponseDto<List<SettingResponse>> getSettingUrls() {
return DataResponseDto.from(adminService.getSettingUrls());
}

@Operation(summary = "branch 링크 생성")
@PostMapping("/branch")
public DataResponseDto<UrlResponse> createBranchUrl(
@RequestBody BranchRequest branchRequest
) {
Long boxId = branchRequest.boxId();
UrlResponse urlResponse = UrlResponse.from(branchService.createBranchUrl(boxId));

return DataResponseDto.from(urlResponse);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.dilly.file;
package com.dilly.admin.api;

import com.dilly.admin.dto.response.UrlResponse;
import com.dilly.application.FileService;
import com.dilly.dto.request.FileRequest;
import com.dilly.exception.ErrorCode;
import com.dilly.global.response.DataResponseDto;
import com.dilly.global.swagger.ApiErrorCodeExample;
Expand All @@ -28,12 +28,14 @@ public class FileController {
)
@GetMapping("/presigned-url/{fileName}")
@ApiErrorCodeExample(ErrorCode.FILE_SERVER_ERROR)
public DataResponseDto<FileRequest> getPresignedUrl(
public DataResponseDto<UrlResponse> getPresignedUrl(
@PathVariable(name = "fileName") @Schema(description = "확장자명을 포함해주세요")
String fileName) {
String activeProfile = System.getProperty("spring.profiles.active");

return DataResponseDto.from(
UrlResponse urlResponse = UrlResponse.from(
fileService.getPresignedUrl("images/" + activeProfile, fileName));

return DataResponseDto.from(urlResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dilly.admin.dto.request;

import lombok.Builder;

@Builder
public record BranchRequest(
Long boxId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dilly.admin.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record UrlResponse(
@Schema(example = "www.example.com")
String url
) {

public static UrlResponse from(String url) {
return UrlResponse.builder()
.url(url)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dilly.dto.response;
package com.dilly.admin.dto.response;

import lombok.Builder;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/api/v1/auth/sign-up",
"/api/v1/auth/sign-in/**",
"/api/v1/auth/reissue",
"/api/v1/giftboxes/web/**"
"/api/v1/giftboxes/web/**",
"/api/v1/admin/branch"
).permitAll()
.anyRequest().authenticated()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import static org.hamcrest.Matchers.hasSize;
import static org.mockito.BDDMockito.given;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.dilly.admin.dto.request.BranchRequest;
import com.dilly.admin.dto.response.BoxImgResponse;
import com.dilly.admin.dto.response.ImgResponse;
import com.dilly.admin.dto.response.MusicResponse;
import com.dilly.admin.dto.response.SettingResponse;
import com.dilly.dto.response.YoutubeUrlValidationResponse;
import com.dilly.admin.dto.response.YoutubeUrlValidationResponse;
import com.dilly.gift.dto.response.EnvelopeListResponse;
import com.dilly.gift.dto.response.EnvelopePaperResponse;
import com.dilly.global.ControllerTestSupport;
Expand Down Expand Up @@ -189,7 +192,7 @@ void validateYoutubeUrl() throws Exception {
YoutubeUrlValidationResponse validationResponse = YoutubeUrlValidationResponse.builder()
.status(true).build();

given(youtubeService.validateYoutubeUrl(url)).willReturn(validationResponse);
given(youtubeService.validateYoutubeUrl(url)).willReturn(validationResponse.status());

// when // then
mockMvc.perform(
Expand Down Expand Up @@ -223,4 +226,26 @@ void getSettingUrls() throws Exception {
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data", hasSize(3)));
}

@DisplayName("선물박스 아이디를 받아 브랜치 URL을 반환한다.")
@Test
@WithCustomMockUser
void createBranchUrl() throws Exception {
// given
BranchRequest branchRequest = BranchRequest.builder().boxId(1L).build();
String expectedUrl = "www.test.com";

given(branchService.createBranchUrl(branchRequest.boxId())).willReturn(expectedUrl);

// when // then
mockMvc.perform(
post(baseUrl + "/admin/branch")
.with(csrf())
.contentType("application/json")
.content(objectMapper.writeValueAsString(branchRequest))
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.url").value(expectedUrl));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dilly.admin.api.AdminController;
import com.dilly.admin.application.AdminService;
import com.dilly.application.BranchService;
import com.dilly.application.YoutubeService;
import com.dilly.gift.api.GiftBoxController;
import com.dilly.gift.api.GiftController;
Expand Down Expand Up @@ -51,6 +52,9 @@ public abstract class ControllerTestSupport {
@MockBean
protected YoutubeService youtubeService;

@MockBean
protected BranchService branchService;

@MockBean
protected GiftService giftService;

Expand Down
5 changes: 5 additions & 0 deletions packy-api/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@ cloud:
youtube:
api:
key: test

branch:
api:
key: test
url: test
3 changes: 3 additions & 0 deletions packy-common/src/main/java/com/dilly/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public enum ErrorCode {
FILE_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 서버 연동에 오류가 발생했습니다."),
FILE_DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제에 실패했습니다."),

// Branch
BRANCH_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Branch 서버 연동에 오류가 발생했습니다."),

// Authorization
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."),
MALFORMED_JWT(HttpStatus.UNAUTHORIZED, "올바르지 않은 형식의 JWT 토큰입니다."),
Expand Down
3 changes: 3 additions & 0 deletions packy-infra/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {

// webflux (for WebClient)
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// test
testImplementation 'com.squareup.okhttp3:mockwebserver'
}

test {
Expand Down
61 changes: 61 additions & 0 deletions packy-infra/src/main/java/com/dilly/application/BranchService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.dilly.application;

import com.dilly.exception.ErrorCode;
import com.dilly.exception.internalserver.InternalServerException;
import com.dilly.model.BranchUrl;
import java.util.HashMap;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;

@Service
@RequiredArgsConstructor
@AllArgsConstructor
@Slf4j
public class BranchService {

@Value("${branch.api.key}")
private String branchApiKey;

@Value("${branch.api.url}")
private String branchApiUrl;

public String createBranchUrl(Long boxId) {
WebClient webClient = WebClient.builder()
.baseUrl(branchApiUrl)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();

Map<String, Object> bodyData = new HashMap<>();

bodyData.put("branch_key", branchApiKey);

Map<String, String> data = new HashMap<>();
data.put("boxId", boxId.toString());
bodyData.put("data", data);

try {
BranchUrl branchUrl = webClient.post()
.bodyValue(bodyData)
.retrieve()
.bodyToMono(BranchUrl.class)
.block();

if (branchUrl == null) {
throw new InternalServerException(ErrorCode.BRANCH_SERVER_ERROR);
}

return branchUrl.url();
} catch (WebClientException e) {
throw new InternalServerException(ErrorCode.BRANCH_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;
import com.dilly.dto.request.FileRequest;
import com.dilly.exception.ErrorCode;
import com.dilly.exception.internalserver.InternalServerException;
import java.net.URL;
Expand All @@ -29,17 +28,15 @@ public class FileService {

private final AmazonS3 amazonS3;

public FileRequest getPresignedUrl(String prefix, String fileName) {
public String getPresignedUrl(String prefix, String fileName) {
if (!prefix.isEmpty()) {
fileName = createPath(prefix, fileName);
}

GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePresignedUrlRequest(bucket, fileName);
URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);

return FileRequest.builder()
.url(url.toString())
.build();
return url.toString();
}

public void deleteFile(String imgUrl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.dilly.application;

import com.dilly.dto.response.YoutubeUrlValidationResponse;
import com.dilly.exception.ErrorCode;
import com.dilly.exception.internalserver.InternalServerException;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
Expand All @@ -27,30 +26,32 @@ public class YoutubeService {
private String apiKey;
private final JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

public YoutubeUrlValidationResponse validateYoutubeUrl(String url) {
String videoId = extractVideoId(url);
public Boolean validateYoutubeUrl(String url) {
boolean isYoutubeUrlValid = true;

// videoId 추출 불가능
String videoId = extractVideoId(url);
if (videoId == null) {
return YoutubeUrlValidationResponse.from(false);
isYoutubeUrlValid = false;
}

// video 정보 접근 불가능
Optional<Video> video = getVideoInfo(videoId);
if (video.isEmpty()) {
return YoutubeUrlValidationResponse.from(false);
isYoutubeUrlValid = false;
}

// 임베딩 불가능
if (Boolean.FALSE.equals(video.get().getStatus().getEmbeddable())) {
return YoutubeUrlValidationResponse.from(false);
isYoutubeUrlValid = false;
}

// 공개되지 않은 영상
if (video.get().getStatus().getPrivacyStatus().equals("private")) {
return YoutubeUrlValidationResponse.from(false);
isYoutubeUrlValid = false;
}

return YoutubeUrlValidationResponse.builder().status(true).build();
return isYoutubeUrlValid;
}

private String extractVideoId(String url) {
Expand Down
17 changes: 0 additions & 17 deletions packy-infra/src/main/java/com/dilly/dto/request/FileRequest.java

This file was deleted.

Loading

0 comments on commit 2ac9c30

Please sign in to comment.