From a67b05e0682dd4988c80b764a690a9ade32f45ac Mon Sep 17 00:00:00 2001 From: Jansoon Date: Tue, 23 Dec 2025 00:11:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=A4=EB=A5=98=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=B3=B4=EA=B3=A0=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/report/controller/ReportApi.java | 13 +++ .../report/controller/ReportController.java | 28 +++++++ .../domain/report/cotroller/ReportApi.java | 7 -- .../report/cotroller/ReportController.java | 15 ---- .../domain/report/dto/request/ReportReq.java | 13 +++ .../report/service/ReportCommandService.java | 11 +++ .../domain/report/service/ReportService.java | 6 ++ .../integration/ReportApiIntegrationTest.java | 82 +++++++++++++++++++ .../service/ReportCommandServiceTest.java | 54 ++++++++++++ .../report/service/ReportServiceTest.java | 36 ++++++++ 10 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/sofa/linkiving/domain/report/controller/ReportApi.java create mode 100644 src/main/java/com/sofa/linkiving/domain/report/controller/ReportController.java delete mode 100644 src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportApi.java delete mode 100644 src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportController.java create mode 100644 src/main/java/com/sofa/linkiving/domain/report/dto/request/ReportReq.java create mode 100644 src/test/java/com/sofa/linkiving/domain/report/integration/ReportApiIntegrationTest.java create mode 100644 src/test/java/com/sofa/linkiving/domain/report/service/ReportCommandServiceTest.java create mode 100644 src/test/java/com/sofa/linkiving/domain/report/service/ReportServiceTest.java diff --git a/src/main/java/com/sofa/linkiving/domain/report/controller/ReportApi.java b/src/main/java/com/sofa/linkiving/domain/report/controller/ReportApi.java new file mode 100644 index 00000000..da6d0391 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/report/controller/ReportApi.java @@ -0,0 +1,13 @@ +package com.sofa.linkiving.domain.report.controller; + +import com.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.domain.report.dto.request.ReportReq; +import com.sofa.linkiving.global.common.BaseResponse; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "Report", description = "오류 및 버그 제보 관리 API") +public interface ReportApi { + BaseResponse createReport(Member member, @Valid ReportReq res); +} diff --git a/src/main/java/com/sofa/linkiving/domain/report/controller/ReportController.java b/src/main/java/com/sofa/linkiving/domain/report/controller/ReportController.java new file mode 100644 index 00000000..71e997b1 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/report/controller/ReportController.java @@ -0,0 +1,28 @@ +package com.sofa.linkiving.domain.report.controller; + +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; + +import com.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.domain.report.dto.request.ReportReq; +import com.sofa.linkiving.domain.report.service.ReportService; +import com.sofa.linkiving.global.common.BaseResponse; +import com.sofa.linkiving.security.annotation.AuthMember; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/v1/report") +@RequiredArgsConstructor +public class ReportController implements ReportApi { + private final ReportService reportService; + + @Override + @PostMapping + public BaseResponse createReport(@AuthMember Member member, @RequestBody ReportReq res) { + reportService.create(member, res.content()); + return BaseResponse.noContent("제보가 성공적으로 접수되었습니다."); + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportApi.java b/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportApi.java deleted file mode 100644 index 0b3c634d..00000000 --- a/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportApi.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sofa.linkiving.domain.report.cotroller; - -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Report", description = "오류 및 버그 제보 관리 API") -public interface ReportApi { -} diff --git a/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportController.java b/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportController.java deleted file mode 100644 index 33551821..00000000 --- a/src/main/java/com/sofa/linkiving/domain/report/cotroller/ReportController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sofa.linkiving.domain.report.cotroller; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.sofa.linkiving.domain.report.service.ReportService; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequestMapping("/v1/report") -@RequiredArgsConstructor -public class ReportController implements ReportApi { - private final ReportService reportService; -} diff --git a/src/main/java/com/sofa/linkiving/domain/report/dto/request/ReportReq.java b/src/main/java/com/sofa/linkiving/domain/report/dto/request/ReportReq.java new file mode 100644 index 00000000..23ee8ceb --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/report/dto/request/ReportReq.java @@ -0,0 +1,13 @@ +package com.sofa.linkiving.domain.report.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record ReportReq( + @NotBlank(message = "제보 내용은 필수입니다.") + @Schema(description = "버그 및 오류 내용") + @Size(max = 1000, message = "제보 내용은 1000자를 초과할 수 없습니다") + String content +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/report/service/ReportCommandService.java b/src/main/java/com/sofa/linkiving/domain/report/service/ReportCommandService.java index 348bbe74..bb75cfc9 100644 --- a/src/main/java/com/sofa/linkiving/domain/report/service/ReportCommandService.java +++ b/src/main/java/com/sofa/linkiving/domain/report/service/ReportCommandService.java @@ -2,6 +2,8 @@ import org.springframework.stereotype.Service; +import com.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.domain.report.entity.Report; import com.sofa.linkiving.domain.report.repository.ReportRepository; import lombok.RequiredArgsConstructor; @@ -10,4 +12,13 @@ @RequiredArgsConstructor public class ReportCommandService { private final ReportRepository reportRepository; + + public Report save(Member member, String content) { + Report report = Report.builder() + .member(member) + .content(content) + .build(); + + return reportRepository.save(report); + } } diff --git a/src/main/java/com/sofa/linkiving/domain/report/service/ReportService.java b/src/main/java/com/sofa/linkiving/domain/report/service/ReportService.java index 9dcf61a4..fac27df1 100644 --- a/src/main/java/com/sofa/linkiving/domain/report/service/ReportService.java +++ b/src/main/java/com/sofa/linkiving/domain/report/service/ReportService.java @@ -3,6 +3,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.sofa.linkiving.domain.member.entity.Member; + import lombok.RequiredArgsConstructor; @Service @@ -10,4 +12,8 @@ @RequiredArgsConstructor public class ReportService { private final ReportCommandService reportCommandService; + + public void create(Member member, String content) { + reportCommandService.save(member, content); + } } diff --git a/src/test/java/com/sofa/linkiving/domain/report/integration/ReportApiIntegrationTest.java b/src/test/java/com/sofa/linkiving/domain/report/integration/ReportApiIntegrationTest.java new file mode 100644 index 00000000..c0d85161 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/report/integration/ReportApiIntegrationTest.java @@ -0,0 +1,82 @@ +package com.sofa.linkiving.domain.report.integration; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.domain.member.enums.Role; +import com.sofa.linkiving.domain.member.repository.MemberRepository; +import com.sofa.linkiving.domain.report.dto.request.ReportReq; +import com.sofa.linkiving.domain.report.entity.Report; +import com.sofa.linkiving.domain.report.repository.ReportRepository; +import com.sofa.linkiving.security.userdetails.CustomMemberDetail; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +public class ReportApiIntegrationTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ReportRepository reportRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member testMember; + private UserDetails testUserDetails; + + @BeforeEach + void setUp() { + testMember = memberRepository.save(Member.builder() + .email("reportUser@test.com") + .password("password") + .build()); + + testUserDetails = new CustomMemberDetail(testMember, Role.USER); + } + + @Test + @DisplayName("제보 생성 요청 시 DB에 저장되고 204 No Content를 반환한다") + void shouldCreateReportSuccessfully() throws Exception { + // given + ReportReq requestDto = new ReportReq("잘못된 링크 정보를 수정해주세요."); + + // when & then + mockMvc.perform( + post("/v1/report") + .with(csrf()) + .with(user(testUserDetails)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + ) + .andExpect(status().isOk()); + + List reports = reportRepository.findAll(); + assertThat(reports).hasSize(1); + assertThat(reports.get(0).getContent()).isEqualTo("잘못된 링크 정보를 수정해주세요."); + assertThat(reports.get(0).getMember().getId()).isEqualTo(testMember.getId()); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/report/service/ReportCommandServiceTest.java b/src/test/java/com/sofa/linkiving/domain/report/service/ReportCommandServiceTest.java new file mode 100644 index 00000000..7488fa48 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/report/service/ReportCommandServiceTest.java @@ -0,0 +1,54 @@ +package com.sofa.linkiving.domain.report.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +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.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.domain.report.entity.Report; +import com.sofa.linkiving.domain.report.repository.ReportRepository; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ReportCommandService 단위 테스트") +public class ReportCommandServiceTest { + + @InjectMocks + private ReportCommandService reportCommandService; + + @Mock + private ReportRepository reportRepository; + + @Test + @DisplayName("제보 내용을 저장할 수 있다") + void shouldSaveReport() { + // given + Member member = Member.builder() + .email("test@example.com") + .build(); + String content = "버그 제보합니다."; + + Report report = Report.builder() + .member(member) + .content(content) + .build(); + + given(reportRepository.save(any(Report.class))).willReturn(report); + + // when + Report savedReport = reportCommandService.save(member, content); + + // then + assertThat(savedReport).isNotNull(); + assertThat(savedReport.getContent()).isEqualTo(content); + assertThat(savedReport.getMember()).isEqualTo(member); + + verify(reportRepository, times(1)).save(any(Report.class)); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/report/service/ReportServiceTest.java b/src/test/java/com/sofa/linkiving/domain/report/service/ReportServiceTest.java new file mode 100644 index 00000000..90608b83 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/report/service/ReportServiceTest.java @@ -0,0 +1,36 @@ +package com.sofa.linkiving.domain.report.service; + +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.sofa.linkiving.domain.member.entity.Member; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ReportService 단위 테스트") +public class ReportServiceTest { + @InjectMocks + private ReportService reportService; + + @Mock + private ReportCommandService reportCommandService; + + @Test + @DisplayName("제보 생성 요청 시 CommandService에게 저장을 위임한다") + void shouldDelegateToCommandService() { + // given + Member member = mock(Member.class); + String content = "불건전한 링크가 포함되어 있습니다."; + + // when + reportService.create(member, content); + + // then + verify(reportCommandService, times(1)).save(member, content); + } +}