diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7362d363..a5e5b65d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,11 @@ ## πŸ“‹ Checklist + - [ ] πŸ”€ PR 제λͺ©μ˜ ν˜•μ‹μ„ 잘 μž‘μ„±ν–ˆλ‚˜μš”? (e.g. `feat: μœ μ € 쑰회 κΈ°λŠ₯ κ΅¬ν˜„`) - [ ] 🏷️ 라벨, ν”„λ‘œμ νŠΈ, λ§ˆμΌμŠ€ν†€μ€ λ“±λ‘ν–ˆλ‚˜μš”? - [ ] 🧹 μ½”λ“œ μŠ€λ©œμ€ ν•΄κ²°ν–ˆλ‚˜μš”? ## 🧩 이슈 번호 -- #이슈번호 +- close #이슈번호 ## πŸ‘©β€πŸ’» 곡유 포인트 및 λ…Όμ˜ 사항 diff --git a/src/main/java/com/moabam/api/application/BugService.java b/src/main/java/com/moabam/api/application/BugService.java new file mode 100644 index 00000000..74f98623 --- /dev/null +++ b/src/main/java/com/moabam/api/application/BugService.java @@ -0,0 +1,24 @@ +package com.moabam.api.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.moabam.api.domain.entity.Member; +import com.moabam.api.dto.BugMapper; +import com.moabam.api.dto.BugResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class BugService { + + private final MemberService memberService; + + public BugResponse getBug(Long memberId) { + Member member = memberService.getById(memberId); + + return BugMapper.toBugResponse(member.getBug()); + } +} diff --git a/src/main/java/com/moabam/api/application/MemberService.java b/src/main/java/com/moabam/api/application/MemberService.java new file mode 100644 index 00000000..112fcd7e --- /dev/null +++ b/src/main/java/com/moabam/api/application/MemberService.java @@ -0,0 +1,25 @@ +package com.moabam.api.application; + +import static com.moabam.global.error.model.ErrorMessage.*; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.moabam.api.domain.entity.Member; +import com.moabam.api.domain.repository.MemberRepository; +import com.moabam.global.error.exception.NotFoundException; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + public Member getById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Bug.java b/src/main/java/com/moabam/api/domain/entity/Bug.java new file mode 100644 index 00000000..12fd020b --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Bug.java @@ -0,0 +1,47 @@ +package com.moabam.api.domain.entity; + +import static com.moabam.global.error.model.ErrorMessage.*; + +import org.hibernate.annotations.ColumnDefault; + +import com.moabam.global.error.exception.BadRequestException; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Bug { + + @Column(name = "morning_bug", nullable = false) + @ColumnDefault("0") + private int morningBug; + + @Column(name = "night_bug", nullable = false) + @ColumnDefault("0") + private int nightBug; + + @Column(name = "golden_bug", nullable = false) + @ColumnDefault("0") + private int goldenBug; + + @Builder + private Bug(int morningBug, int nightBug, int goldenBug) { + this.morningBug = validateBugCount(morningBug); + this.nightBug = validateBugCount(nightBug); + this.goldenBug = validateBugCount(goldenBug); + } + + private int validateBugCount(int bug) { + if (bug < 0) { + throw new BadRequestException(INVALID_BUG_COUNT); + } + + return bug; + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Member.java b/src/main/java/com/moabam/api/domain/entity/Member.java index a0a9e749..f7108bac 100644 --- a/src/main/java/com/moabam/api/domain/entity/Member.java +++ b/src/main/java/com/moabam/api/domain/entity/Member.java @@ -13,6 +13,7 @@ import com.moabam.global.common.util.BaseImageUrl; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -25,8 +26,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; -@Getter @Entity +@Getter @Table(name = "member") @SQLDelete(sql = "UPDATE member SET deleted_at = CURRENT_TIMESTAMP where participant_id = ?") @Where(clause = "deleted_at IS NOT NULL") @@ -66,17 +67,8 @@ public class Member extends BaseTimeEntity { @ColumnDefault("0") private int currentMorningCount; - @Column(name = "morning_bug", nullable = false) - @ColumnDefault("0") - private int morningBug; - - @Column(name = "night_bug", nullable = false) - @ColumnDefault("0") - private int nightBug; - - @Column(name = "golden_bug", nullable = false) - @ColumnDefault("0") - private int goldenBug; + @Embedded + private Bug bug; @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) @@ -87,11 +79,12 @@ public class Member extends BaseTimeEntity { private LocalDateTime deletedAt; @Builder - private Member(Long id, String socialId, String nickname, String profileImage) { + private Member(Long id, String socialId, String nickname, String profileImage, Bug bug) { this.id = id; this.socialId = requireNonNull(socialId); this.nickname = requireNonNull(nickname); this.profileImage = requireNonNullElse(profileImage, BaseImageUrl.PROFILE_URL); + this.bug = requireNonNull(bug); this.role = Role.USER; } } diff --git a/src/main/java/com/moabam/api/domain/repository/MemberRepository.java b/src/main/java/com/moabam/api/domain/repository/MemberRepository.java new file mode 100644 index 00000000..095c71b4 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Member; + +public interface MemberRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/dto/BugMapper.java b/src/main/java/com/moabam/api/dto/BugMapper.java new file mode 100644 index 00000000..57bea850 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/BugMapper.java @@ -0,0 +1,18 @@ +package com.moabam.api.dto; + +import com.moabam.api.domain.entity.Bug; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public final class BugMapper { + + public static BugResponse toBugResponse(Bug bug) { + return BugResponse.builder() + .morningBug(bug.getMorningBug()) + .nightBug(bug.getNightBug()) + .goldenBug(bug.getGoldenBug()) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/dto/BugResponse.java b/src/main/java/com/moabam/api/dto/BugResponse.java new file mode 100644 index 00000000..256c18cb --- /dev/null +++ b/src/main/java/com/moabam/api/dto/BugResponse.java @@ -0,0 +1,12 @@ +package com.moabam.api.dto; + +import lombok.Builder; + +@Builder +public record BugResponse( + int morningBug, + int nightBug, + int goldenBug +) { + +} diff --git a/src/main/java/com/moabam/api/dto/OAuthMapper.java b/src/main/java/com/moabam/api/dto/OAuthMapper.java index dac14930..8b82483b 100644 --- a/src/main/java/com/moabam/api/dto/OAuthMapper.java +++ b/src/main/java/com/moabam/api/dto/OAuthMapper.java @@ -1,6 +1,5 @@ package com.moabam.api.dto; -import com.moabam.api.dto.AuthorizationCodeRequest; import com.moabam.global.config.OAuthConfig; import lombok.AccessLevel; diff --git a/src/main/java/com/moabam/api/presentation/BugController.java b/src/main/java/com/moabam/api/presentation/BugController.java new file mode 100644 index 00000000..d17dfa32 --- /dev/null +++ b/src/main/java/com/moabam/api/presentation/BugController.java @@ -0,0 +1,26 @@ +package com.moabam.api.presentation; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.moabam.api.application.BugService; +import com.moabam.api.dto.BugResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/bugs") +@RequiredArgsConstructor +public class BugController { + + private final BugService bugService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public BugResponse getBug() { + return bugService.getBug(1L); + } +} diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index 82942270..1e381ef0 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -13,7 +13,11 @@ public enum ErrorMessage { ROOM_MODIFY_UNAUTHORIZED_REQUEST("λ°©μž₯이 μ•„λ‹Œ μ‚¬μš©μžλŠ” 방을 μˆ˜μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€."), PARTICIPANT_NOT_FOUND("방에 λŒ€ν•œ μ°Έμ—¬μžμ˜ 정보가 μ—†μŠ΅λ‹ˆλ‹€."), LOGIN_FAILED("λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."), - REQUEST_FAILD("λ„€νŠΈμš°ν¬ μ ‘κ·Ό μ‹€νŒ¨μž…λ‹ˆλ‹€."); + REQUEST_FAILD("λ„€νŠΈμš°ν¬ μ ‘κ·Ό μ‹€νŒ¨μž…λ‹ˆλ‹€."), + + MEMBER_NOT_FOUND("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νšŒμ›μž…λ‹ˆλ‹€."), + + INVALID_BUG_COUNT("벌레 κ°œμˆ˜λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€."); private final String message; } diff --git a/src/test/java/com/moabam/api/application/BugServiceTest.java b/src/test/java/com/moabam/api/application/BugServiceTest.java new file mode 100644 index 00000000..416ba5fb --- /dev/null +++ b/src/test/java/com/moabam/api/application/BugServiceTest.java @@ -0,0 +1,44 @@ +package com.moabam.api.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.moabam.api.domain.entity.Bug; +import com.moabam.api.domain.entity.Member; +import com.moabam.api.dto.BugResponse; +import com.moabam.fixture.MemberFixture; + +@ExtendWith(MockitoExtension.class) +class BugServiceTest { + + @InjectMocks + BugService bugService; + + @Mock + MemberService memberService; + + @DisplayName("벌레λ₯Ό μ‘°νšŒν•œλ‹€.") + @Test + void get_bug_success() { + // given + Long memberId = 1L; + Member member = MemberFixture.member(); + given(memberService.getById(memberId)).willReturn(member); + + // when + BugResponse response = bugService.getBug(memberId); + + // then + Bug bug = member.getBug(); + assertThat(response.morningBug()).isEqualTo(bug.getMorningBug()); + assertThat(response.nightBug()).isEqualTo(bug.getNightBug()); + assertThat(response.goldenBug()).isEqualTo(bug.getGoldenBug()); + } +} diff --git a/src/test/java/com/moabam/api/domain/entity/BugTest.java b/src/test/java/com/moabam/api/domain/entity/BugTest.java new file mode 100644 index 00000000..c356fd58 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/entity/BugTest.java @@ -0,0 +1,30 @@ +package com.moabam.api.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.moabam.global.error.exception.BadRequestException; + +class BugTest { + + @DisplayName("벌레 κ°œμˆ˜κ°€ 음수이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + @ParameterizedTest + @CsvSource({ + "-10, 10, 10", + "10, -10, 10", + "10, 10, -10", + }) + void validate_bug_count_exception(int morningBug, int nightBug, int goldenBug) { + Bug.BugBuilder bugBuilder = Bug.builder() + .morningBug(morningBug) + .nightBug(nightBug) + .goldenBug(goldenBug); + + assertThatThrownBy(bugBuilder::build) + .isInstanceOf(BadRequestException.class) + .hasMessage("벌레 κ°œμˆ˜λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€."); + } +} diff --git a/src/test/java/com/moabam/api/domain/entity/MemberTest.java b/src/test/java/com/moabam/api/domain/entity/MemberTest.java index dbc695fc..8255d843 100644 --- a/src/test/java/com/moabam/api/domain/entity/MemberTest.java +++ b/src/test/java/com/moabam/api/domain/entity/MemberTest.java @@ -23,6 +23,7 @@ void create_member_success() { .socialId(socialId) .nickname(nickname) .profileImage(profileImage) + .bug(Bug.builder().build()) .build()); } @@ -35,14 +36,15 @@ void create_member_noImage_success() { .socialId(socialId) .nickname(nickname) .profileImage(null) + .bug(Bug.builder().build()) .build(); assertAll( () -> assertThat(member.getProfileImage()).isEqualTo(BaseImageUrl.PROFILE_URL), () -> assertThat(member.getRole()).isEqualTo(Role.USER), - () -> assertThat(member.getNightBug()).isZero(), - () -> assertThat(member.getGoldenBug()).isZero(), - () -> assertThat(member.getMorningBug()).isZero(), + () -> assertThat(member.getBug().getNightBug()).isZero(), + () -> assertThat(member.getBug().getGoldenBug()).isZero(), + () -> assertThat(member.getBug().getMorningBug()).isZero(), () -> assertThat(member.getTotalCertifyCount()).isZero(), () -> assertThat(member.getReportCount()).isZero(), () -> assertThat(member.getCurrentMorningCount()).isZero(), diff --git a/src/test/java/com/moabam/api/presentation/BugControllerTest.java b/src/test/java/com/moabam/api/presentation/BugControllerTest.java new file mode 100644 index 00000000..4bfedb04 --- /dev/null +++ b/src/test/java/com/moabam/api/presentation/BugControllerTest.java @@ -0,0 +1,55 @@ +package com.moabam.api.presentation; + +import static java.nio.charset.StandardCharsets.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.application.BugService; +import com.moabam.api.dto.BugMapper; +import com.moabam.api.dto.BugResponse; +import com.moabam.fixture.BugFixture; + +@SpringBootTest +@AutoConfigureMockMvc +class BugControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @MockBean + BugService bugService; + + @DisplayName("벌레λ₯Ό μ‘°νšŒν•œλ‹€.") + @Test + void get_bug_success() throws Exception { + // given + Long memberId = 1L; + BugResponse expected = BugMapper.toBugResponse(BugFixture.bug()); + given(bugService.getBug(memberId)).willReturn(expected); + + // expected + String content = mockMvc.perform(get("/bugs")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(UTF_8); + BugResponse actual = objectMapper.readValue(content, BugResponse.class); + Assertions.assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/com/moabam/fixture/BugFixture.java b/src/test/java/com/moabam/fixture/BugFixture.java new file mode 100644 index 00000000..8d2b8703 --- /dev/null +++ b/src/test/java/com/moabam/fixture/BugFixture.java @@ -0,0 +1,18 @@ +package com.moabam.fixture; + +import com.moabam.api.domain.entity.Bug; + +public final class BugFixture { + + public static final int MORNING_BUG = 10; + public static final int NIGHT_BUG = 20; + public static final int GOLDEN_BUG = 30; + + public static Bug bug() { + return Bug.builder() + .morningBug(MORNING_BUG) + .nightBug(NIGHT_BUG) + .goldenBug(GOLDEN_BUG) + .build(); + } +} diff --git a/src/test/java/com/moabam/fixture/MemberFixture.java b/src/test/java/com/moabam/fixture/MemberFixture.java new file mode 100644 index 00000000..02c5d84a --- /dev/null +++ b/src/test/java/com/moabam/fixture/MemberFixture.java @@ -0,0 +1,19 @@ +package com.moabam.fixture; + +import com.moabam.api.domain.entity.Member; + +public final class MemberFixture { + + public static final String SOCIAL_ID = "test123"; + public static final String NICKNAME = "λͺ¨μ•„λ°€"; + public static final String PROFILE_IMAGE = "/profile/moabam.png"; + + public static Member member() { + return Member.builder() + .socialId(SOCIAL_ID) + .nickname(NICKNAME) + .profileImage(PROFILE_IMAGE) + .bug(BugFixture.bug()) + .build(); + } +}