-
Notifications
You must be signed in to change notification settings - Fork 4
feat: 문의하기 기능 추가 #87
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: 문의하기 기능 추가 #87
Changes from all commits
624ff52
233cbcb
528e5d4
bec3cfb
0df812a
24f3be9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package org.myteam.server.inquiry.controller; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.myteam.server.global.page.request.PageInfoRequest; | ||
| import org.myteam.server.global.page.response.PageCustomResponse; | ||
| import org.myteam.server.global.web.response.ResponseDto; | ||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
| import org.myteam.server.inquiry.dto.request.InquiryRequest; | ||
| import org.myteam.server.inquiry.dto.response.InquiryResponse; | ||
| import org.myteam.server.inquiry.service.InquiryReadService; | ||
| import org.myteam.server.inquiry.service.InquiryWriteService; | ||
| import org.myteam.server.util.ClientUtils; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| import static org.myteam.server.global.web.response.ResponseStatus.SUCCESS; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/inquiries") | ||
| public class InquiryController { | ||
| private final InquiryReadService inquiryReadService; | ||
| private final InquiryWriteService inquiryWriteService; | ||
|
|
||
| @PostMapping() | ||
| public ResponseEntity<ResponseDto<String>> createInquiry(@Valid @RequestBody InquiryRequest inquiryRequest, | ||
| HttpServletRequest request) { | ||
| String clientIp = ClientUtils.getRemoteIP(request); | ||
| String content = inquiryWriteService.createInquiry(inquiryRequest.getContent(), inquiryRequest.getMemberPublicId(), clientIp); | ||
|
|
||
| return ResponseEntity.ok(new ResponseDto<>( | ||
| SUCCESS.name(), | ||
| "Successfully upload inquiry", | ||
| content | ||
| )); | ||
| } | ||
|
|
||
| @GetMapping("/my") | ||
| public ResponseEntity<ResponseDto<PageCustomResponse<InquiryResponse>>> getMyInquiries(@RequestParam UUID memberPublicId, PageInfoRequest pageInfoRequest) { | ||
| PageCustomResponse<InquiryResponse> content = inquiryReadService.getInquiriesByMember(memberPublicId, pageInfoRequest); | ||
|
|
||
| return ResponseEntity.ok(new ResponseDto<>( | ||
| SUCCESS.name(), | ||
| "Successfully find inquiries", | ||
| content | ||
| )); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package org.myteam.server.inquiry.domain; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.*; | ||
| import org.myteam.server.member.entity.Member; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.UUID; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class Inquiry { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| private String content; | ||
|
|
||
| @ManyToOne(optional = true, cascade = CascadeType.REMOVE) | ||
| @JoinColumn(name = "member_id") | ||
| private Member member; | ||
|
|
||
| private String clientIp; | ||
|
|
||
| @Column(name = "created_at", nullable = false, updatable = false) | ||
| private LocalDateTime createdAt; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package org.myteam.server.inquiry.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| @Getter | ||
| public class InquiryRequest { | ||
| @NotNull(message = "문의 내용이 없으면 안됩니다.") | ||
| private String content; | ||
| private UUID memberPublicId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package org.myteam.server.inquiry.dto.response; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.AllArgsConstructor; | ||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class InquiryResponse { | ||
| private Long id; | ||
| private String content; | ||
| private String memberNickname; | ||
| private String clientIp; | ||
| private LocalDateTime createdAt; | ||
|
|
||
| // Entity -> DTO 변환 메서드 | ||
| public static InquiryResponse createInquiryResponse(Inquiry inquiry) { | ||
| return InquiryResponse.builder() | ||
| .id(inquiry.getId()) | ||
| .content(inquiry.getContent()) | ||
| .memberNickname(inquiry.getMember().getNickname()) | ||
| .createdAt(inquiry.getCreatedAt()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.myteam.server.inquiry.repository; | ||
|
|
||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
| import org.myteam.server.member.entity.Member; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
|
|
||
| @Repository | ||
| public interface InquiryRepository extends JpaRepository<Inquiry, Long> { | ||
| Page<Inquiry> findByMember(Member member, Pageable pageable); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.myteam.server.inquiry.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.myteam.server.global.exception.ErrorCode; | ||
| import org.myteam.server.global.exception.PlayHiveException; | ||
| import org.myteam.server.global.page.request.PageInfoRequest; | ||
| import org.myteam.server.global.page.response.PageCustomResponse; | ||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
| import org.myteam.server.inquiry.dto.response.InquiryResponse; | ||
| import org.myteam.server.inquiry.repository.InquiryRepository; | ||
| import org.myteam.server.member.entity.Member; | ||
| import org.myteam.server.member.repository.MemberRepository; | ||
| import org.myteam.server.member.service.MemberReadService; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| @Service | ||
| @Transactional(readOnly = true) | ||
| public class InquiryReadService { | ||
| private final MemberReadService memberReadService; | ||
| private final InquiryRepository inquiryRepository; | ||
|
|
||
| public PageCustomResponse<InquiryResponse> getInquiriesByMember(UUID memberPublicId, PageInfoRequest pageInfoRequest) { | ||
| Member member = memberReadService.findById(memberPublicId); | ||
|
|
||
| Pageable pageable = PageRequest.of(pageInfoRequest.getPage() - 1, pageInfoRequest.getSize()); | ||
| Page<Inquiry> inquiries = inquiryRepository.findByMember(member, pageable); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Entity를 바로 Response로 내려주는건 안좋습니다ㅠ InquiryResponse를 만들어 변환해줘서 내려주는게 좋습니다!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 죄송합니다 저번에 피드백 주신 부분인데 또 반영을 못했네요,, 수정하도록 하겠습니다. |
||
| Page<InquiryResponse> inquiryResponses = inquiries.map(InquiryResponse::createInquiryResponse); | ||
|
|
||
| return PageCustomResponse.of(inquiryResponses); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package org.myteam.server.inquiry.service; | ||
|
|
||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
| import org.myteam.server.inquiry.repository.InquiryRepository; | ||
| import org.myteam.server.member.entity.Member; | ||
| import org.myteam.server.member.repository.MemberRepository; | ||
| import org.myteam.server.util.slack.service.SlackService; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| @Service | ||
| @Transactional | ||
| public class InquiryWriteService { | ||
|
|
||
| private final MemberRepository memberRepository; | ||
| private final InquiryRepository inquiryRepository; | ||
| private final SlackService slackService; | ||
|
|
||
| public String createInquiry(String content, UUID memberPublicId, String clientIP) { | ||
| Optional<Member> member = memberRepository.findByPublicId(memberPublicId); | ||
|
|
||
| Inquiry inquiry = Inquiry.builder() | ||
| .content(content) | ||
| .member(member.orElse(null)) | ||
| .clientIp(clientIP) | ||
| .createdAt(LocalDateTime.now()) | ||
| .build(); | ||
|
|
||
| String slackMessage = String.format( | ||
| "📩 새로운 문의가 접수되었습니다!\n\n" + | ||
| "🔹 문의 내용: %s\n" + | ||
| "🔹 작성자: %s (%s)\n" + | ||
| "🔹 요청 IP: %s\n" + | ||
| "🔹 작성 시간: %s\n\n" + | ||
| "확인 후 답변해 주세요. ✅", | ||
| content, | ||
| member.map(Member::getNickname).orElse("익명 사용자"), | ||
| member.map(Member::getEmail).orElse("익명"), | ||
| clientIP, | ||
| LocalDateTime.now() | ||
| ); | ||
|
|
||
| inquiryRepository.save(inquiry); | ||
| slackService.sendSlackNotification(slackMessage); | ||
|
|
||
| log.info("문의 내용이 접수되었습니다."); | ||
|
|
||
| return inquiry.getContent(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package org.myteam.server.inquiry.service; | ||
|
|
||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.myteam.server.global.page.request.PageInfoRequest; | ||
| import org.myteam.server.global.page.response.PageCustomResponse; | ||
| import org.myteam.server.inquiry.domain.Inquiry; | ||
| import org.myteam.server.inquiry.dto.response.InquiryResponse; | ||
| import org.myteam.server.member.dto.MemberSaveRequest; | ||
| import org.myteam.server.member.entity.Member; | ||
| import org.myteam.server.member.repository.MemberRepository; | ||
| import org.myteam.server.member.service.MemberService; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.*; | ||
| import java.util.UUID; | ||
|
|
||
| @SpringBootTest | ||
| class InquiryReadServiceTest { | ||
|
|
||
| @Autowired | ||
| private MemberService memberService; | ||
| @Autowired | ||
| private MemberRepository memberRepository; | ||
|
|
||
| @Autowired | ||
| private InquiryWriteService inquiryWriteService; | ||
| @Autowired | ||
| private InquiryReadService inquiryReadService; | ||
|
|
||
| private Member testMember; | ||
| private Member otherMember; | ||
|
|
||
| private UUID testMemberPublicId; | ||
| private UUID otherMemberPublicId; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| testMemberPublicId = memberService.create(MemberSaveRequest.builder() | ||
| .email("[email protected]") | ||
| .tel("01012345678") | ||
| .nickname("testUser") | ||
| .password("teamPlayHive12#") | ||
| .build()).getPublicId(); | ||
| otherMemberPublicId = memberService.create(MemberSaveRequest.builder() | ||
| .email("[email protected]") | ||
| .tel("01087654321") | ||
| .nickname("otherUser") | ||
| .password("otherMember!@#") | ||
| .build()).getPublicId(); | ||
|
|
||
| System.out.println("testMemberPublicId = " + testMemberPublicId); | ||
| System.out.println("otherMemberPublicId = " + otherMemberPublicId); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("회원의 문의 내역을 정상적으로 조회한다.") | ||
| void shouldReturnPagedInquiriesForMember() { | ||
| // Given | ||
| testMember = memberRepository.findByPublicId(testMemberPublicId).get(); | ||
| for (int i = 1; i <= 15; i++) { | ||
| inquiryWriteService.createInquiry("문의내역 " + i, testMemberPublicId, "127.0.0.1"); | ||
| } | ||
| otherMember = memberRepository.findByPublicId(otherMemberPublicId).get(); | ||
| for (int i = 1; i <= 15; i++) { | ||
| inquiryWriteService.createInquiry("건의사항 " + i, otherMemberPublicId, "127.0.0.1"); | ||
| } | ||
|
|
||
| // When | ||
| PageCustomResponse<InquiryResponse> response = inquiryReadService.getInquiriesByMember(testMember.getPublicId(), new PageInfoRequest(2, 5)); | ||
|
|
||
| // Then | ||
| System.out.println(response); | ||
| assertThat("문의내역 6").isEqualTo(response.getContent().get(0).getContent()); | ||
| assertThat(response.getContent()).hasSize(5); | ||
| assertThat(response.getPageInfo().getCurrentPage()).isEqualTo(2); | ||
| assertThat(response.getPageInfo().getTotalPage()).isEqualTo(3); | ||
| assertThat(response.getPageInfo().getTotalElement()).isEqualTo(15); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. assertThat(pageInfo)
.extracting("currentPage", "totalPage", "totalElement")
.containsExactlyInAnyOrder(
1, 2, 3L
)이런식으로 한번에 처리도 가능합니다! 참고해주세요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. assertThat(pageInfo) |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid체크를 Entity에 하신이유가 있으실까요?
Valid 체크는 Controller Request단에서 하는게 맞는거 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 반영하겠습니다