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] 선물 등록 관련 기능 구현 #26

Merged
merged 10 commits into from
Jan 9, 2024
79 changes: 79 additions & 0 deletions src/main/java/org/sopt/sweet/domain/gift/controller/GiftApi.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,86 @@
package org.sopt.sweet.domain.gift.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.sopt.sweet.domain.gift.dto.request.CreateGiftRequestDto;
import org.sopt.sweet.domain.gift.dto.request.MyGiftsRequestDto;
import org.sopt.sweet.global.common.SuccessResponse;
import org.sopt.sweet.global.config.auth.UserId;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;

@Tag(name = "선물", description = "선물 관련 API")
public interface GiftApi {
@Operation(
summary = "새로운 선물 등록 API",
responses = {
@ApiResponse(
responseCode = "201",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = SuccessResponse.class)
)
)
},
security = @SecurityRequirement(name = "token")
)
ResponseEntity<SuccessResponse<?>> createNewGift(
@Parameter(
description = "authorization token에서 얻은 userId, 임의입력하면 대체됩니다.",
required = true,
example = "12345"
) @UserId Long userId,
@Valid @RequestBody CreateGiftRequestDto createGiftRequestDto
);

@Operation(
summary = "내가 등록한 선물 조회 API",
responses = {
@ApiResponse(
responseCode = "200",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = SuccessResponse.class)
)
)
},
security = @SecurityRequirement(name = "token")
)
ResponseEntity<SuccessResponse<?>> getMyGift(
@Parameter(
description = "authorization token에서 얻은 userId, 임의입력하면 대체됩니다.",
required = true,
example = "12345"
) @UserId Long userId,
@Valid @RequestBody MyGiftsRequestDto myGiftsRequestDto
);

@Operation(
summary = "내가 등록한 선물 삭제 API",
responses = {
@ApiResponse(
responseCode = "200",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = SuccessResponse.class)
)
)
},
security = @SecurityRequirement(name = "token")
)
ResponseEntity<SuccessResponse<?>> deleteMyGift(
@Parameter(
description = "authorization token에서 얻은 userId, 임의입력하면 대체됩니다.",
required = true,
example = "12345"
) @UserId Long userId,
@PathVariable Long giftId
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
package org.sopt.sweet.domain.gift.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.sopt.sweet.domain.gift.dto.request.CreateGiftRequestDto;
import org.sopt.sweet.domain.gift.dto.request.MyGiftsRequestDto;
import org.sopt.sweet.domain.gift.dto.response.MyGiftsResponseDto;
import org.sopt.sweet.domain.gift.service.GiftService;
import org.sopt.sweet.global.common.SuccessResponse;
import org.sopt.sweet.global.config.auth.UserId;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RequestMapping("/api/gift")
@RestController
public class GiftController implements GiftApi{
public class GiftController implements GiftApi {

private final GiftService giftService;

@PostMapping
public ResponseEntity<SuccessResponse<?>> createNewGift(@UserId Long userId, @RequestBody CreateGiftRequestDto createGiftRequestDto) {
giftService.createNewGift(userId, createGiftRequestDto);
return SuccessResponse.created(null);
}

@GetMapping("/my")
public ResponseEntity<SuccessResponse<?>> getMyGift(@UserId Long userId, @RequestBody MyGiftsRequestDto myGiftsRequestDto) {
final MyGiftsResponseDto myGiftsResponseDto = giftService.getMyGift(userId, myGiftsRequestDto);
return SuccessResponse.ok(myGiftsResponseDto);
}

@DeleteMapping ("/my/{giftId}")
public ResponseEntity<SuccessResponse<?>> deleteMyGift(@UserId Long userId, @PathVariable Long giftId) {
giftService.deleteMyGift(userId, giftId);
return SuccessResponse.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.sweet.domain.gift.dto.request;

public record CreateGiftRequestDto(
Long roomId,
String url,
String name,
int cost,
String imageUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.sopt.sweet.domain.gift.dto.request;

public record MyGiftsRequestDto(
Long roomId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.sopt.sweet.domain.gift.dto.response;

import lombok.Builder;

@Builder
public record MyGiftDto(
Long giftId,
String imageUrl,
String name,
int cost
) {
public static MyGiftDto of(Long giftId, String imageUrl, String name, int cost) {
return MyGiftDto.builder()
.giftId(giftId)
.imageUrl(imageUrl)
.name(name)
.cost(cost)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sopt.sweet.domain.gift.dto.response;

import java.util.List;

public record MyGiftsResponseDto(
List<MyGiftDto> myGiftDtoList
) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.sopt.sweet.domain.gift.repository;

import org.sopt.sweet.domain.gift.entity.Gift;
import org.sopt.sweet.domain.member.entity.Member;
import org.sopt.sweet.domain.room.entity.Room;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface GiftRepository extends JpaRepository<Gift, Long> {
long countByRoomAndMember(Room room, Member member);
List<Gift> findByRoomAndMember(Room room, Member member);
}
110 changes: 110 additions & 0 deletions src/main/java/org/sopt/sweet/domain/gift/service/GiftService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,119 @@
package org.sopt.sweet.domain.gift.service;

import lombok.RequiredArgsConstructor;
import org.sopt.sweet.domain.gift.dto.request.CreateGiftRequestDto;
import org.sopt.sweet.domain.gift.dto.request.MyGiftsRequestDto;
import org.sopt.sweet.domain.gift.dto.response.MyGiftDto;
import org.sopt.sweet.domain.gift.dto.response.MyGiftsResponseDto;
import org.sopt.sweet.domain.gift.entity.Gift;
import org.sopt.sweet.domain.gift.repository.GiftRepository;
import org.sopt.sweet.domain.member.entity.Member;
import org.sopt.sweet.domain.member.repository.MemberRepository;
import org.sopt.sweet.domain.room.entity.Room;
import org.sopt.sweet.domain.room.entity.RoomMember;
import org.sopt.sweet.domain.room.repository.RoomMemberRepository;
import org.sopt.sweet.domain.room.repository.RoomRepository;
import org.sopt.sweet.global.error.exception.BusinessException;
import org.sopt.sweet.global.error.exception.EntityNotFoundException;
import org.sopt.sweet.global.error.exception.ForbiddenException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static org.sopt.sweet.global.error.ErrorCode.*;

@RequiredArgsConstructor
@Service
@Transactional
public class GiftService {
private final GiftRepository giftRepository;
private final MemberRepository memberRepository;
private final RoomRepository roomRepository;
private final RoomMemberRepository roomMemberRepository;
private static final int MAX_GIFT_COUNT = 2;

public void createNewGift(Long memberId, CreateGiftRequestDto createGiftRequestDto) {
Member member = findMemberByIdOrThrow(memberId);
Room room = findRoomByIdOrThrow(createGiftRequestDto.roomId());
checkRoomMemberNotExists(room, member);
checkGiftCountNotExceeded(room, member);
Gift gift = buildGift(member, room, createGiftRequestDto);
giftRepository.save(gift);
}

@Transactional(readOnly = true)
public MyGiftsResponseDto getMyGift(Long memberId, MyGiftsRequestDto myGiftsRequestDto) {
Member member = findMemberByIdOrThrow(memberId);
Room room = findRoomByIdOrThrow(myGiftsRequestDto.roomId());
checkRoomMemberNotExists(room, member);
List<Gift> gifts = giftRepository.findByRoomAndMember(room, member);
List<MyGiftDto> myGiftsDtoList = mapGiftsToMyGiftDtoList(gifts);
return new MyGiftsResponseDto(myGiftsDtoList);
}

public void deleteMyGift(Long memberId, Long giftId){
Member member = findMemberByIdOrThrow(memberId);
Gift gift = findByIdOrThrow(giftId);
validateMemberGiftOwner(member, gift);
giftRepository.delete(gift);
}

private Gift buildGift(Member member, Room room, CreateGiftRequestDto createGiftRequestDto) {
return Gift.builder()
.url(createGiftRequestDto.url())
.name(createGiftRequestDto.name())
.cost(createGiftRequestDto.cost())
.imageUrl(createGiftRequestDto.imageUrl())
.room(room)
.member(member)
.build();
}

private List<MyGiftDto> mapGiftsToMyGiftDtoList(List<Gift> gifts) {
return gifts.stream()
.map(gift -> MyGiftDto.of(gift.getId(), gift.getImageUrl(), gift.getName(), gift.getCost()))
.collect(Collectors.toList());
}

private void checkGiftCountNotExceeded(Room room, Member member) {
long giftCount = giftRepository.countByRoomAndMember(room, member);
if (giftCount >= MAX_GIFT_COUNT) {
throw new BusinessException(MEMBER_GIFT_COUNT_EXCEEDED);
}
}

private boolean isRoomMemberExists(Room room, Member member) {
Optional<RoomMember> existingRoomMember = roomMemberRepository.findByRoomAndMember(room, member);
return existingRoomMember.isPresent();
}

private void checkRoomMemberNotExists(Room room, Member member) {
if (!isRoomMemberExists(room, member)) {
throw new ForbiddenException(MEMBER_NOT_IN_ROOM);
}
}

private void validateMemberGiftOwner(Member member, Gift gift) {
if (!gift.getMember().equals(member)) {
throw new ForbiddenException(MEMBER_NOT_GIFT_OWNER);
}
}

private Member findMemberByIdOrThrow(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new EntityNotFoundException(MEMBER_NOT_FOUND));
}

private Room findRoomByIdOrThrow(Long roomId) {
return roomRepository.findById(roomId)
.orElseThrow(() -> new EntityNotFoundException(ROOM_NOT_FOUND));
}

private Gift findByIdOrThrow(Long giftId){
return giftRepository.findById(giftId)
.orElseThrow(() -> new EntityNotFoundException(GIFT_NOT_FOUND));
}
}
4 changes: 4 additions & 0 deletions src/main/java/org/sopt/sweet/global/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum ErrorCode {
NAME_LENGTH_EXCEEDED(HttpStatus.BAD_REQUEST, "이름은 10글자를 초과할 수 없습니다."),
MEMBER_NUMBER_EXCEEDED(HttpStatus.BAD_REQUEST, "해당 선물방의 최대 인원을 초과하였습니다."),
INVITATION_CLOSED(HttpStatus.BAD_REQUEST, "초대가 마감되었습니다."),
MEMBER_GIFT_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "최대 선물 등록 개수를 초과하였습니다."),

/**
* 401 Unauthorized
Expand All @@ -32,6 +33,8 @@ public enum ErrorCode {
* 403 Forbidden
*/
FORBIDDEN(HttpStatus.FORBIDDEN, "리소스 접근 권한이 없습니다."),
MEMBER_NOT_IN_ROOM(HttpStatus.FORBIDDEN, "해당 선물 방에 존재하지 않는 멤버입니다."),
MEMBER_NOT_GIFT_OWNER(HttpStatus.FORBIDDEN, "해당 선물을 등록하지 않은 멤버입니다."),

/**
* 404 Not Found
Expand All @@ -40,6 +43,7 @@ public enum ErrorCode {
OPEN_GRAPH_NOT_FOUND(HttpStatus.NOT_FOUND, "오픈 그래프 정보를 찾을 수 없습니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 멤버를 찾을 수 없습니다."),
ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 선물방을 찾을 수 없습니다."),
GIFT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 선물을 찾을 수 없습니다."),

/**
* 405 Method Not Allowed
Expand Down