From 54a9436731351acbf5abdee04e79517b78067507 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 11 Feb 2026 20:00:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20api=20=EC=9E=91=EC=97=85=20-=20?= =?UTF-8?q?=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=ED=94=BC=EC=A7=80=EC=BB=AC=20=EC=9D=B8?= =?UTF-8?q?=ED=8F=AC=20=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/api/MemberController.java | 10 ++++++++++ .../api/request/UpdatePhysicalinfo.java | 20 +++++++++++++++++++ .../api/response/MemberInfoResponse.java | 3 +++ .../member/application/MemberService.java | 3 +++ .../member/application/MemberServiceImpl.java | 12 +++++++++++ .../climingoApi/member/domain/Member.java | 10 ++++++++++ .../member/domain/PhysicalInfo.java | 8 +++++--- src/main/resources/application-local.yml | 4 ++-- 8 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/climingo/climingoApi/member/api/request/UpdatePhysicalinfo.java diff --git a/src/main/java/com/climingo/climingoApi/member/api/MemberController.java b/src/main/java/com/climingo/climingoApi/member/api/MemberController.java index d12743d..1310e8a 100644 --- a/src/main/java/com/climingo/climingoApi/member/api/MemberController.java +++ b/src/main/java/com/climingo/climingoApi/member/api/MemberController.java @@ -2,9 +2,11 @@ import com.climingo.climingoApi.global.auth.RequestMember; import com.climingo.climingoApi.member.api.request.UpdateNicknameRequest; +import com.climingo.climingoApi.member.api.request.UpdatePhysicalinfo; import com.climingo.climingoApi.member.api.response.MemberInfoResponse; import com.climingo.climingoApi.member.application.MemberService; import com.climingo.climingoApi.member.domain.Member; +import com.climingo.climingoApi.member.domain.PhysicalInfo; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -32,6 +34,14 @@ public ResponseEntity findMemberInfo(@PathVariable(value = " return ResponseEntity.ok().body(memberInfoResponse); } + @PatchMapping("/member/{memberId}") + public ResponseEntity updatePhysicalInfo(@RequestMember Member member, @PathVariable(value = "memberId") Long memberId, + @RequestBody @Valid UpdatePhysicalinfo request){ + memberService.updatePhysicalInfo(member,memberId,request.getPhysicalInfo()); + return ResponseEntity.ok().build(); + + } + @PatchMapping("/members/{memberId}/nickname") public ResponseEntity updateNickname(@RequestMember Member member, @PathVariable(value = "memberId") Long memberId, @RequestBody @Valid UpdateNicknameRequest request) { diff --git a/src/main/java/com/climingo/climingoApi/member/api/request/UpdatePhysicalinfo.java b/src/main/java/com/climingo/climingoApi/member/api/request/UpdatePhysicalinfo.java new file mode 100644 index 0000000..d16c0c3 --- /dev/null +++ b/src/main/java/com/climingo/climingoApi/member/api/request/UpdatePhysicalinfo.java @@ -0,0 +1,20 @@ +package com.climingo.climingoApi.member.api.request; + +import com.climingo.climingoApi.member.domain.PhysicalInfo; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@JsonDeserialize(builder = UpdatePhysicalinfo.UpdatePhysicalinfoBuilder.class) +public class UpdatePhysicalinfo { + + @NotNull(message = "physicalInfo는 필수입니다") + @Valid + @JsonProperty("physicalInfo") + private final PhysicalInfo physicalInfo; +} diff --git a/src/main/java/com/climingo/climingoApi/member/api/response/MemberInfoResponse.java b/src/main/java/com/climingo/climingoApi/member/api/response/MemberInfoResponse.java index 8e83298..950622a 100644 --- a/src/main/java/com/climingo/climingoApi/member/api/response/MemberInfoResponse.java +++ b/src/main/java/com/climingo/climingoApi/member/api/response/MemberInfoResponse.java @@ -1,6 +1,7 @@ package com.climingo.climingoApi.member.api.response; import com.climingo.climingoApi.member.domain.Member; +import com.climingo.climingoApi.member.domain.PhysicalInfo; import lombok.Getter; @Getter @@ -11,6 +12,7 @@ public class MemberInfoResponse { private String email; private String profileUrl; private String providerType; + private PhysicalInfo physicalInfo; public MemberInfoResponse(Member member) { this.memberId = member.getId(); @@ -18,6 +20,7 @@ public MemberInfoResponse(Member member) { this.email = member.getEmail(); this.profileUrl = member.getProfileUrl(); this.providerType = member.getProviderType(); + this.physicalInfo = member.getPhysicalInfo(); } } diff --git a/src/main/java/com/climingo/climingoApi/member/application/MemberService.java b/src/main/java/com/climingo/climingoApi/member/application/MemberService.java index 88bebb1..16e0f57 100644 --- a/src/main/java/com/climingo/climingoApi/member/application/MemberService.java +++ b/src/main/java/com/climingo/climingoApi/member/application/MemberService.java @@ -2,6 +2,8 @@ import com.climingo.climingoApi.member.api.response.MemberInfoResponse; import com.climingo.climingoApi.member.domain.Member; +import com.climingo.climingoApi.member.domain.PhysicalInfo; +import org.springframework.transaction.annotation.Transactional; public interface MemberService { @@ -9,4 +11,5 @@ public interface MemberService { void updateNickname(Member member, Long memberId, String nickname); + void updatePhysicalInfo(Member member, Long memberId, PhysicalInfo physicalInfo); } diff --git a/src/main/java/com/climingo/climingoApi/member/application/MemberServiceImpl.java b/src/main/java/com/climingo/climingoApi/member/application/MemberServiceImpl.java index 22692a3..a64dc39 100644 --- a/src/main/java/com/climingo/climingoApi/member/application/MemberServiceImpl.java +++ b/src/main/java/com/climingo/climingoApi/member/application/MemberServiceImpl.java @@ -6,6 +6,7 @@ import com.climingo.climingoApi.member.api.response.MemberInfoResponse; import com.climingo.climingoApi.member.domain.Member; import com.climingo.climingoApi.member.domain.MemberRepository; +import com.climingo.climingoApi.member.domain.PhysicalInfo; import jakarta.persistence.EntityNotFoundException; import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; @@ -93,4 +94,15 @@ private boolean isDuplicated(String nickname) { return memberRepository.existsByNickname(nickname); } + + @Override + @Transactional + public void updatePhysicalInfo(Member member, Long memberId, PhysicalInfo physicalInfo){ + + + + member.updatePhysicalinfo(physicalInfo); + memberRepository.save(member); + } + } diff --git a/src/main/java/com/climingo/climingoApi/member/domain/Member.java b/src/main/java/com/climingo/climingoApi/member/domain/Member.java index cb02a43..cbcb297 100644 --- a/src/main/java/com/climingo/climingoApi/member/domain/Member.java +++ b/src/main/java/com/climingo/climingoApi/member/domain/Member.java @@ -95,6 +95,16 @@ public void updateNickname(String nickname) { this.nickname = nickname; } + // 소수점 관련 처리 로직 + public boolean isPhysicalinfoDouble(PhysicalInfo physicalInfo){ + + return true; + } + + public void updatePhysicalinfo(PhysicalInfo physicalInfo){ + this.physicalInfo = physicalInfo; + } + public boolean isSameMember(Member member) { return this.isSameMember(member.getId()); } diff --git a/src/main/java/com/climingo/climingoApi/member/domain/PhysicalInfo.java b/src/main/java/com/climingo/climingoApi/member/domain/PhysicalInfo.java index db5e213..9bfb893 100644 --- a/src/main/java/com/climingo/climingoApi/member/domain/PhysicalInfo.java +++ b/src/main/java/com/climingo/climingoApi/member/domain/PhysicalInfo.java @@ -6,6 +6,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.math.BigDecimal; + @Embeddable @Getter @NoArgsConstructor @@ -13,11 +15,11 @@ public class PhysicalInfo { @JsonProperty("height") - private Double height; + private BigDecimal height; @JsonProperty("weight") - private Double weight; + private BigDecimal weight; @JsonProperty("armSpan") - private Double armSpan; + private BigDecimal armSpan; } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 03aa121..b3e90a0 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -33,8 +33,8 @@ oauth2: path: ENC(UQVIwN53EBnE0TcwgTT79HYM2KUrEcRDhlx72+W+anbO87WiWJbCSJQKOPqGBNGaze2oi9obarWiRuzW5UdJtLUzNe7icfKNPepJrHZXhVSD5UUO5eHsvbFVeWhVScgeIPxvtbdsk0LMsLH7EbzSuC852HHy0/KQ6j8gJREw/RkyFuHFYJR2VupYASz9jpdabzwA6q9Lh9v7BPqLB2nFzub5xmHMU7HWICvzSHbnTsgXqyQXr7XtvHxQVZN5z4lEAKbk0BB53vm3ammzkxhTo656ZFAgK69n) ffmpeg: - ffmpeg-path: /usr/local/bin/ffmpeg - ffprobe-path: /usr/local/bin/ffprobe + ffmpeg-path: /opt/homebrew/bin/ffmpeg + ffprobe-path: /opt/homebrew/bin/ffprobe app: version: local_temp_version From 6dc7be4f599581f9d47862a557821236175ceac8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 11 Feb 2026 20:00:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=82=B4=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=ED=94=BC=EC=A7=80=EC=BB=AC=EC=9D=B8?= =?UTF-8?q?=ED=8F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/api/MemberControllerTest.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/test/java/com/climingo/climingoApi/member/api/MemberControllerTest.java diff --git a/src/test/java/com/climingo/climingoApi/member/api/MemberControllerTest.java b/src/test/java/com/climingo/climingoApi/member/api/MemberControllerTest.java new file mode 100644 index 0000000..0c806ef --- /dev/null +++ b/src/test/java/com/climingo/climingoApi/member/api/MemberControllerTest.java @@ -0,0 +1,135 @@ +package com.climingo.climingoApi.member.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.climingo.climingoApi.global.auth.RequestMember; +import com.climingo.climingoApi.global.exception.GlobalExceptionHandler; +import com.climingo.climingoApi.member.api.response.MemberInfoResponse; +import com.climingo.climingoApi.member.application.MemberService; +import com.climingo.climingoApi.member.domain.Member; +import com.climingo.climingoApi.member.domain.PhysicalInfo; +import com.climingo.climingoApi.member.domain.UserRole; +import com.climingo.climingoApi.message.error.ErrorAlertMessageProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityNotFoundException; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@DisplayName("MemberController 단위 테스트") +class MemberControllerTest { + + private MockMvc mockMvc; + private MemberService memberService; + private ObjectMapper objectMapper; + private Member loginMember; + + @BeforeEach + void setUp() { + memberService = mock(MemberService.class); + objectMapper = new ObjectMapper(); + + loginMember = Member.builder() + .id(1L) + .authId("auth123") + .providerType("kakao") + .nickname("testUser") + .email("test@test.com") + .profileUrl("http://profile.url") + .physicalInfo(new PhysicalInfo(new BigDecimal("175.5"), new BigDecimal("70.0"), new BigDecimal("180.0"))) + .role(UserRole.USER) + .build(); + + HandlerMethodArgumentResolver requestMemberResolver = new HandlerMethodArgumentResolver() { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(RequestMember.class) != null + && Member.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + return loginMember; + } + }; + + mockMvc = MockMvcBuilders.standaloneSetup(new MemberController(memberService)) + .setCustomArgumentResolvers(requestMemberResolver) + .setControllerAdvice(new GlobalExceptionHandler(mock(ErrorAlertMessageProvider.class))) + .build(); + } + + @Test + @DisplayName("GET /members - 로그인한 회원이 자신의 정보를 정상 조회한다") + void findMyInfo_success() throws Exception { + MemberInfoResponse response = new MemberInfoResponse(loginMember); + when(memberService.findMemberInfo(eq(1L))).thenReturn(response); + + mockMvc.perform(get("/members")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.memberId").value(1L)) + .andExpect(jsonPath("$.nickname").value("testUser")) + .andExpect(jsonPath("$.email").value("test@test.com")) + .andExpect(jsonPath("$.profileUrl").value("http://profile.url")) + .andExpect(jsonPath("$.providerType").value("kakao")) + .andExpect(jsonPath("$.physicalInfo.height").value(175.5)) + .andExpect(jsonPath("$.physicalInfo.weight").value(70.0)) + .andExpect(jsonPath("$.physicalInfo.armSpan").value(180.0)); + } + + @Test + @DisplayName("GET /members - 존재하지 않는 회원 조회 시 EntityNotFoundException이 발생한다") + void findMyInfo_notFound() throws Exception { + when(memberService.findMemberInfo(eq(1L))) + .thenThrow(new EntityNotFoundException("id가 1인 회원은 존재하지 않습니다")); + + mockMvc.perform(get("/members")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("PATCH /member/{memberId} - 유효한 요청으로 신체 정보를 업데이트한다") + void updatePhysicalInfo_success() throws Exception { + doNothing().when(memberService).updatePhysicalInfo(any(Member.class), eq(1L), any(PhysicalInfo.class)); + + String requestBody = objectMapper.writeValueAsString( + java.util.Map.of("physicalInfo", java.util.Map.of( + "height", 180.0, + "weight", 75.5, + "armSpan", 185.0 + )) + ); + + mockMvc.perform(patch("/member/{memberId}", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("PATCH /member/{memberId} - physicalInfo 없이 호출 시 400 에러가 발생한다") + void updatePhysicalInfo_missingBody() throws Exception { + mockMvc.perform(patch("/member/{memberId}", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest()); + } +}