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] 기록하기(Album) API 구현 #135

Merged
merged 11 commits into from
Mar 1, 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public class SecurityConfig {
// "/log-out",
"/test", "/profile", "/health", "/actuator/health",
"/alarm/qna", "/alarm/drink",
"/demo/**"
"/demo/**",
"/album/image"
};

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package sopt.org.umbba.api.controller.album;

import static sopt.org.umbba.api.config.jwt.JwtProvider.*;
import static sopt.org.umbba.common.exception.SuccessType.*;
import static sopt.org.umbba.external.s3.S3BucketPrefix.*;

import java.security.Principal;
import java.util.List;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import sopt.org.umbba.api.controller.album.dto.request.AlbumImgUrlRequestDto;
import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto;
import sopt.org.umbba.api.controller.album.dto.response.AlbumResponseDto;
import sopt.org.umbba.api.service.album.AlbumService;
import sopt.org.umbba.common.exception.dto.ApiResponse;
import sopt.org.umbba.external.s3.PreSignedUrlDto;
import sopt.org.umbba.external.s3.S3BucketPrefix;
import sopt.org.umbba.external.s3.S3Service;

@RestController
@RequestMapping("/album")
@RequiredArgsConstructor
public class AlbumController {

private final AlbumService albumService;
private final S3Service s3Service;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse createAlbum(@Valid @RequestBody final CreateAlbumRequestDto request, final Principal principal, HttpServletResponse response) {
String imgUrl = s3Service.getS3ImgUrl(ALBUM_PREFIX.getValue(), request.getImgFileName());
Long albumId = albumService.createAlbum(request, imgUrl, getUserFromPrincial(principal));
response.setHeader("Location", "/album/" + albumId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q) 혹시 header를 지정함으로써 해서 얻는 효과가 무엇일까요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP 통신 규약에 따르면 201 Created로 응답하는 경우에 생성된 리소스의 값을 응답에 포함시키는 게 원칙이라고 하더라구요! 그래서 응답 헤더(Location)에 생성된 리소스의 식별자를 요청 url에 붙여서 반환하도록 HttpServleResponse를 인자로 받아 넣어 주었습니다!!
이 부분은 다른 API에도 추후 적용해보면 좋을 것 같네요 :)

*HTTP 201 Created에 관한 규약 아래 내용 참고해 주세요!!
https://developer.mozilla.org/ko/docs/Web/HTTP/Status/201

return ApiResponse.success(CREATE_ALBUM_SUCCESS);
}

// PreSigned Url 이용 (클라이언트에서 해당 URL로 업로드)
@PatchMapping("/image")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 PATCH인것 확인후에 명세서에 반영해뒀습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 감사합니다 !!

@ResponseStatus(HttpStatus.OK)
public ApiResponse<PreSignedUrlDto> getImgPreSignedUrl(@RequestBody final AlbumImgUrlRequestDto request) {
return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.of(request.getImgPrefix())));
}

@DeleteMapping("/{albumId}")
@ResponseStatus(HttpStatus.OK)
public ApiResponse deleteAlbum(@PathVariable final Long albumId, final Principal principal) {
String imgUrl = albumService.deleteAlbum(albumId, getUserFromPrincial(principal));
s3Service.deleteS3Image(imgUrl);
return ApiResponse.success(DELETE_ALBUM_SUCCESS);
}

@GetMapping
@ResponseStatus(HttpStatus.OK)
public ApiResponse<List<AlbumResponseDto>> getAlbumList(final Principal principal) {
return ApiResponse.success(GET_ALBUM_LIST_SUCCESS, albumService.getAlbumList(getUserFromPrincial(principal)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package sopt.org.umbba.api.controller.album.dto.request;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AlbumImgUrlRequestDto {

private String imgPrefix;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sopt.org.umbba.api.controller.album.dto.request;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class CreateAlbumRequestDto {

@NotBlank(message = "제목은 필수 입력 값입니다.")
@Size(max = 15)
private String title;

@NotBlank(message = "소개글은 필수 입력 값입니다.")
@Size(max = 32)
private String content;

@NotBlank(message = "이미지 파일명은 필수 입력 값입니다.")
private String imgFileName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package sopt.org.umbba.api.controller.album.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.Builder;
import lombok.Getter;
import sopt.org.umbba.domain.domain.album.Album;

@Getter
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AlbumResponseDto {

private Long albumId;
private String title;
private String content;
private String writer;
private String imgUrl;

public static AlbumResponseDto of(Album album) {
return AlbumResponseDto.builder()
.albumId(album.getId())
.title(album.getTitle())
.content(album.getContent())
.writer(album.getWriter())
.imgUrl(album.getImgUrl())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package sopt.org.umbba.api.service.album;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto;
import sopt.org.umbba.api.controller.album.dto.response.AlbumResponseDto;
import sopt.org.umbba.common.exception.ErrorType;
import sopt.org.umbba.common.exception.model.CustomException;
import sopt.org.umbba.domain.domain.album.Album;
import sopt.org.umbba.domain.domain.album.repository.AlbumRepository;
import sopt.org.umbba.domain.domain.parentchild.Parentchild;
import sopt.org.umbba.domain.domain.user.User;
import sopt.org.umbba.domain.domain.user.repository.UserRepository;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AlbumService {

private final AlbumRepository albumRepository;
private final UserRepository userRepository;

@Transactional
public Long createAlbum(final CreateAlbumRequestDto request, final String imgUrl, final Long userId) {

User user = getUserById(userId);
Parentchild parentchild = getParentchildByUser(user);

Album album = Album.builder()
.title(request.getTitle())
.content(request.getContent())
.imgUrl(imgUrl)
.writer(user.getUsername())
.parentchild(parentchild)
.build();
albumRepository.save(album);
album.setParentchild(parentchild);
parentchild.addAlbum(album);

return album.getId();
}

@Transactional
public String deleteAlbum(final Long albumId, final Long userId) {

User user = getUserById(userId);
Parentchild parentchild = getParentchildByUser(user);
Album album = getAlbumById(albumId);

album.deleteParentchild();
parentchild.deleteAlbum(album);
albumRepository.delete(album);

return album.getImgUrl();
}

public List<AlbumResponseDto> getAlbumList(final Long userId) {
User user = getUserById(userId);
Parentchild parentchild = getParentchildByUser(user);
List<Album> albumList = albumRepository.findAllByParentchildOrderByCreatedAtDesc(
parentchild);

return albumList.stream()
.map(AlbumResponseDto::of)
.collect(Collectors.toList());
}

private User getUserById(Long userId) { // TODO userId -> Parentchild 한번에 가져오기
return userRepository.findById(userId).orElseThrow(
() -> new CustomException(ErrorType.INVALID_USER)
);
}

private Album getAlbumById(Long albumId) {
return albumRepository.findById(albumId).orElseThrow(
() -> new CustomException(ErrorType.NOT_FOUND_ALBUM)
);
}

private Parentchild getParentchildByUser(User user) {
Parentchild parentchild = user.getParentChild();
if (parentchild == null) {
throw new CustomException(ErrorType.USER_HAVE_NO_PARENTCHILD);
}

return parentchild;
}
}

This file was deleted.

1 change: 1 addition & 0 deletions umbba-api/src/main/resources/application-dev1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cloud:
static: ${CLOUD_REGION_DEV}
s3:
bucket: ${BUCKET_NAME_DEV}
bucketImg: ${IMG_BUCKET_DEV}
stack:
auto: false
sqs:
Expand Down
1 change: 1 addition & 0 deletions umbba-api/src/main/resources/application-dev2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cloud:
static: ${CLOUD_REGION_DEV}
s3:
bucket: ${BUCKET_NAME_DEV}
bucketImg: ${IMG_BUCKET_DEV}
stack:
auto: false
sqs:
Expand Down
1 change: 1 addition & 0 deletions umbba-api/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cloud:
static: ${CLOUD_REGION_LOCAL}
s3:
bucket: ${BUCKET_NAME_LOCAL}
bucketImg: ${IMG_BUCKET_LOCAL}
stack:
auto: false
sqs:
Expand Down
1 change: 1 addition & 0 deletions umbba-api/src/main/resources/application-prod1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cloud:
static: ${CLOUD_REGION_PROD}
s3:
bucket: ${BUCKET_NAME_PROD}
bucketImg: ${IMG_BUCKET_PROD}
stack:
auto: false
sqs:
Expand Down
1 change: 1 addition & 0 deletions umbba-api/src/main/resources/application-prod2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cloud:
static: ${CLOUD_REGION_PROD}
s3:
bucket: ${BUCKET_NAME_PROD}
bucketImg: ${IMG_BUCKET_PROD}
stack:
auto: false
sqs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public enum ErrorType {
ALREADY_EXISTS_PARENT_CHILD_USER(HttpStatus.BAD_REQUEST, "이미 해당 유저의 부모자식 관계가 존재합니다."),
ALREADY_QNA_LIST_FULL(HttpStatus.BAD_REQUEST, "이미 QNA 리스트가 가득 찼습니다"),

// Album
INVALID_BUCKET_PREFIX(HttpStatus.BAD_REQUEST, "유효하지 않은 S3 버킷 디렉토리명입니다."),

/**
* 401 UNAUTHORIZED
Expand Down Expand Up @@ -61,6 +63,7 @@ public enum ErrorType {
PARENTCHILD_HAVE_NO_QNALIST(HttpStatus.NOT_FOUND, "부모자식 관계가 가지고 있는 QnA 데이터가 없습니다."),
PARENTCHILD_HAVE_NO_OPPONENT(HttpStatus.NOT_FOUND, "부모자식 관계에 1명만 참여하고 있습니다."),
NOT_FOUND_SECTION(HttpStatus.NOT_FOUND, "해당 아이디와 일치하는 섹션이 없습니다."),
NOT_FOUND_ALBUM(HttpStatus.NOT_FOUND, "존재하지 않는 앨범입니다."),

/**
* About Apple (HttpStatus 고민)
Expand All @@ -80,6 +83,9 @@ public enum ErrorType {
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 관련 에러가 발생했습니다."),
FIREBASE_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파이어베이스 서버와의 연결에 실패했습니다."),
FAIL_TO_SEND_PUSH_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "푸시 알림 메세지 전송에 실패했습니다."),
FAIL_TO_GET_IMAGE_PRE_SIGNED_URL(HttpStatus.INTERNAL_SERVER_ERROR, "PreSigned Url을 가져오는 데 실패했습니다."),
FAIL_TO_DELETE_IMAGE(HttpStatus.INTERNAL_SERVER_ERROR, "S3 버킷에서 이미지를 삭제하는 데 실패했습니다."),
S3_BUCKET_GET_IMAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 버킷에서 이미지를 불러오는 데 실패했습니다."),

// ETC
INDEX_OUT_OF_BOUNDS(HttpStatus.INTERNAL_SERVER_ERROR, "인덱스 범위를 초과했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public enum SuccessType {
TEST_SUCCESS(HttpStatus.OK, "데모데이 테스트용 API 호출에 성공했습니다."),
RESTART_QNA_SUCCESS(HttpStatus.OK, "7일 이후 문답이 정상적으로 시작되었습니다."),
GET_USER_FIRST_ENTRY_SUCCESS(HttpStatus.OK, "유저의 첫 진입여부 조회에 성공했습니다."),
GET_PRE_SIGNED_URL_SUCCESS(HttpStatus.OK, "PreSigned Url 조회에 성공했습니다."),
IMAGE_S3_DELETE_SUCCESS(HttpStatus.OK, "S3 버킷에서 이미지를 삭제하는 데 성공했습니다."),
DELETE_ALBUM_SUCCESS(HttpStatus.OK, "앨범의 기록 삭제에 성공했습니다."),
GET_ALBUM_LIST_SUCCESS(HttpStatus.OK, "앨범의 기록 목록 조회에 성공했습니다."),


/**
Expand All @@ -37,6 +41,7 @@ public enum SuccessType {
CREATE_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "온보딩 정보를 입력받아 부모자식 관계를 생성하는 데 성공했습니다."),
MATCH_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "부모자식 관계 매칭에 성공했습니다."),
ANSWER_TODAY_QUESTION_SUCCESS(HttpStatus.CREATED, "오늘의 일일문답에 답변을 완료하였습니다."),
CREATE_ALBUM_SUCCESS(HttpStatus.CREATED, "앨범의 기록 등록에 성공했습니다."),

;

Expand Down
Loading
Loading