diff --git a/api/src/main/java/com/dnd/sbooky/api/docs/spec/PerformOnboardingApiSpec.java b/api/src/main/java/com/dnd/sbooky/api/docs/spec/PerformOnboardingApiSpec.java new file mode 100644 index 0000000..8923e4e --- /dev/null +++ b/api/src/main/java/com/dnd/sbooky/api/docs/spec/PerformOnboardingApiSpec.java @@ -0,0 +1,15 @@ +package com.dnd.sbooky.api.docs.spec; + +import com.dnd.sbooky.api.member.request.PerformOnboardingRequest; +import com.dnd.sbooky.api.support.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.userdetails.UserDetails; + +@Tag(name = "[Member API]", description = "회원에 관련된 API") +@SecurityRequirement(name = "access-token") +public interface PerformOnboardingApiSpec { + @Operation(summary = "회원 온보딩", description = "회원을 온보딩한다.") + ApiResponse performOnboarding(UserDetails user, PerformOnboardingRequest request); +} diff --git a/api/src/main/java/com/dnd/sbooky/api/item/ObtainItemUseCase.java b/api/src/main/java/com/dnd/sbooky/api/item/ObtainItemUseCase.java new file mode 100644 index 0000000..aa421b0 --- /dev/null +++ b/api/src/main/java/com/dnd/sbooky/api/item/ObtainItemUseCase.java @@ -0,0 +1,41 @@ +package com.dnd.sbooky.api.item; + +import static com.dnd.sbooky.api.support.error.ErrorType.*; + +import com.dnd.sbooky.api.item.exception.ItemNotFoundException; +import com.dnd.sbooky.api.member.exception.MemberNotFoundException; +import com.dnd.sbooky.core.item.ItemEntity; +import com.dnd.sbooky.core.item.ItemRepository; +import com.dnd.sbooky.core.item.MemberItemEntity; +import com.dnd.sbooky.core.item.MemberItemRepository; +import com.dnd.sbooky.core.member.MemberEntity; +import com.dnd.sbooky.core.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class ObtainItemUseCase { + + private final MemberItemRepository memberItemRepository; + private final ItemRepository itemRepository; + private final MemberRepository memberRepository; + + public void obtainItem(Long memberId, Long itemId) { + ItemEntity itemEntity = + itemRepository + .findById(itemId) + .orElseThrow(() -> new ItemNotFoundException(ITEM_NOT_FOUND)); + MemberEntity memberEntity = + memberRepository + .findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + boolean isExisted = memberItemRepository.existsByMemberIdAndItemId(memberId, itemId); + if (!isExisted) { + MemberItemEntity memberItemEntity = MemberItemEntity.obtainItem(memberEntity, itemEntity); + memberItemRepository.save(memberItemEntity); + } + } +} diff --git a/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingController.java b/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingController.java new file mode 100644 index 0000000..edcde8c --- /dev/null +++ b/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingController.java @@ -0,0 +1,31 @@ +package com.dnd.sbooky.api.member; + +import com.dnd.sbooky.api.docs.spec.PerformOnboardingApiSpec; +import com.dnd.sbooky.api.member.request.PerformOnboardingRequest; +import com.dnd.sbooky.api.support.response.ApiResponse; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +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.RestController; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class PerformOnboardingController implements PerformOnboardingApiSpec { + + private final PerformOnboardingUseCase performOnboardingUseCase; + + @PostMapping("/members/onboarding") + public ApiResponse performOnboarding( + @Parameter(hidden = true) @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody PerformOnboardingRequest request) { + Long memberId = Long.valueOf(userDetails.getUsername()); + performOnboardingUseCase.performOnboarding(memberId, request); + return ApiResponse.success(); + } +} diff --git a/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingUseCase.java b/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingUseCase.java new file mode 100644 index 0000000..8bfdf4a --- /dev/null +++ b/api/src/main/java/com/dnd/sbooky/api/member/PerformOnboardingUseCase.java @@ -0,0 +1,30 @@ +package com.dnd.sbooky.api.member; + +import static com.dnd.sbooky.api.support.error.ErrorType.MEMBER_NOT_FOUND; + +import com.dnd.sbooky.api.item.ObtainItemUseCase; +import com.dnd.sbooky.api.member.exception.MemberNotFoundException; +import com.dnd.sbooky.api.member.request.PerformOnboardingRequest; +import com.dnd.sbooky.core.member.MemberEntity; +import com.dnd.sbooky.core.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class PerformOnboardingUseCase { + public static final long BASIC_GHOST_ID = 2L; + private final MemberRepository memberRepository; + private final ObtainItemUseCase obtainItemUseCase; + + public void performOnboarding(Long memberId, PerformOnboardingRequest request) { + MemberEntity member = + memberRepository + .findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + member.updateNickname(request.nickname()); + obtainItemUseCase.obtainItem(memberId, BASIC_GHOST_ID); + } +} diff --git a/api/src/main/java/com/dnd/sbooky/api/member/request/PerformOnboardingRequest.java b/api/src/main/java/com/dnd/sbooky/api/member/request/PerformOnboardingRequest.java new file mode 100644 index 0000000..cd0827b --- /dev/null +++ b/api/src/main/java/com/dnd/sbooky/api/member/request/PerformOnboardingRequest.java @@ -0,0 +1,6 @@ +package com.dnd.sbooky.api.member.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record PerformOnboardingRequest(@NotBlank @Size(min = 1, max = 10) String nickname) {} diff --git a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemEntity.java b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemEntity.java index f962cf8..fd32488 100644 --- a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemEntity.java +++ b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemEntity.java @@ -34,7 +34,7 @@ public class MemberItemEntity extends BaseEntity { @Column(name = ENTITY_PREFIX + "_equipped", nullable = false) private boolean equipped; - @Builder + @Builder(access = AccessLevel.PRIVATE) private MemberItemEntity(MemberEntity memberEntity, ItemEntity itemEntity, boolean equipped) { this.memberEntity = memberEntity; this.itemEntity = itemEntity; @@ -48,4 +48,12 @@ public static MemberItemEntity newInstance(MemberEntity memberEntity, ItemEntity .equipped(true) .build(); } + + public static MemberItemEntity obtainItem(MemberEntity memberEntity, ItemEntity itemEntity) { + return MemberItemEntity.builder() + .memberEntity(memberEntity) + .itemEntity(itemEntity) + .equipped(false) + .build(); + } } diff --git a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryCustom.java b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryCustom.java index 9c92f76..293ccea 100644 --- a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryCustom.java +++ b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryCustom.java @@ -9,4 +9,6 @@ public interface MemberItemRepositoryCustom { List findItemsByMemberId(Long memberId); List findEquippedItemsByMemberId(Long memberId); + + boolean existsByMemberIdAndItemId(Long memberId, Long itemId); } diff --git a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryImpl.java b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryImpl.java index 306341b..0e2199e 100644 --- a/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryImpl.java +++ b/core/src/main/java/com/dnd/sbooky/core/item/MemberItemRepositoryImpl.java @@ -33,6 +33,16 @@ public List findEquippedItemsByMemberId(Long memberId) { .fetch(); } + @Override + public boolean existsByMemberIdAndItemId(Long memberId, Long itemId) { + return queryFactory.selectOne().from(memberItem).where(hasItem(memberId, itemId)).fetchFirst() + != null; + } + + public BooleanExpression hasItem(Long memberId, Long itemId) { + return memberItem.memberEntity.id.eq(memberId).and(memberItem.itemEntity.id.eq(itemId)); + } + private BooleanExpression isEquipped() { return memberItem.equipped.isTrue(); } diff --git a/core/src/main/java/com/dnd/sbooky/core/member/MemberEntity.java b/core/src/main/java/com/dnd/sbooky/core/member/MemberEntity.java index e70ddec..8504fd0 100644 --- a/core/src/main/java/com/dnd/sbooky/core/member/MemberEntity.java +++ b/core/src/main/java/com/dnd/sbooky/core/member/MemberEntity.java @@ -61,4 +61,8 @@ private MemberEntity( public static MemberEntity newInstance(String nickname, String introduction) { return MemberEntity.builder().nickname(nickname).introduction(introduction).build(); } + + public void updateNickname(String nickname) { + this.nickname = nickname; + } }