From 1cf3c72f8fe6a4a3ab93927e72452d1853142449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=95=84?= <52688527+hyeonahhh@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:33:03 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20fix:=20note=20update=20?= =?UTF-8?q?=EB=B9=88=20=EA=B0=92=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../limjang/controller/NoteControllerV2.java | 5 +- .../request/NotePatchRequestV2.java | 62 +++++++++++++ .../limjang/service/NoteCommandServiceV2.java | 32 +++++++ .../juinjang/auth/jwt/JwtExceptionFilter.java | 91 ++++++++++--------- .../domain/limjang/model/Address.java | 19 +++- .../domain/limjang/model/Limjang.java | 6 +- .../domain/limjang/model/LimjangPrice.java | 15 ++- 7 files changed, 176 insertions(+), 54 deletions(-) create mode 100644 src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java b/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java index 6d13b69a..325095de 100644 --- a/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java @@ -17,6 +17,7 @@ import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; import umc.th.juinjang.api.limjang.controller.request.NoteInitRequest; import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; +import umc.th.juinjang.api.limjang.controller.request.NotePatchRequestV2; import umc.th.juinjang.api.limjang.controller.request.NotePostRequest; import umc.th.juinjang.api.limjang.service.NoteCommandServiceV2; import umc.th.juinjang.api.limjang.service.NoteQueryServiceV2; @@ -71,9 +72,9 @@ public ApiResponse updateNote(@PathVariable(name = "noteId") Long noteId, @Operation(summary = "임장 수정 API V2 - UI/UX 리팩토링") @PatchMapping("/notes/init/{noteId}") public ApiResponse updateNoteV2(@PathVariable(name = "noteId") Long noteId, - @RequestBody @Valid NotePatchRequest request, + @RequestBody @Valid NotePatchRequestV2 request, @AuthenticationPrincipal Member member) { - noteCommandService.updateNoteV2(noteId, request); + noteCommandService.updateNoteInitV2(noteId, request); return ApiResponse.onSuccess(null); } diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java new file mode 100644 index 00000000..faffc0e4 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java @@ -0,0 +1,62 @@ +package umc.th.juinjang.api.limjang.controller.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.limjang.repository.NotePriceFactory; + +public record NotePatchRequestV2( + @NotNull + LimjangPriceType priceType, + + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String price, + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String monthlyRent, + + String roadAddress, + String addressDetail, + + String bcode, + + String nickname, + + String floor, + Integer pyong, //null 처리를 위해 Integer로 변경 + String sido, + String sigungu, + String bname1, + String bname2 +) { + + public LimjangPrice toUpdatedPrice(LimjangPurpose purpose) { + if (price == null && monthlyRent == null) { + return LimjangPrice.empty(); + } + return NotePriceFactory.create(purpose, priceType, price, monthlyRent); + } + + public Address toUpdatedAddress() { + if (isAddressAllEmpty()) { + return Address.empty(); + } + return Address.create(roadAddress, addressDetail, bcode, sido, sigungu, bname1, bname2); + } + + private boolean isAddressAllEmpty() { + return isBlank(roadAddress) + && isBlank(addressDetail) + && isBlank(bcode) + && isBlank(sido) + && isBlank(sigungu) + && isBlank(bname1) + && isBlank(bname2); + } + + private boolean isBlank(String s) { + return s == null || s.isBlank(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java b/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java index 78b3137d..04003227 100644 --- a/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java @@ -8,6 +8,7 @@ import umc.th.juinjang.api.address.service.AddressUpdater; import umc.th.juinjang.api.limjang.controller.request.NoteInitRequest; import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; +import umc.th.juinjang.api.limjang.controller.request.NotePatchRequestV2; import umc.th.juinjang.api.limjang.controller.request.NotePostRequest; import umc.th.juinjang.api.limjang.service.response.NotePostResponse; import umc.th.juinjang.common.code.status.ErrorStatus; @@ -74,6 +75,37 @@ public void updateNoteV2(Long noteId, NotePatchRequest request) { note.updateNote(request.nickname(), request.priceType(), request.floor(), request.pyong()); } + @Transactional + public void updateNoteInitV2(Long noteId, NotePatchRequestV2 request) { + try { + Limjang note = noteFinder.getNoteByIdWhereDeletedIsFalse(noteId); + + validatePriceType(note.getPurpose(), request.priceType()); + + LimjangPrice newPrice = request.toUpdatedPrice(note.getPurpose()); + Address newAddress = request.toUpdatedAddress(); + + Address currentAddress = note.getAddressEntity(); + if (newAddress.isEmpty()) { + if (currentAddress != null) { + note.setAddressEntity(null); + } + } else { + if (currentAddress != null) { + currentAddress.update(newAddress); + } else { + addressUpdater.save(newAddress); + note.setAddressEntity(newAddress); + } + } + + note.getLimjangPrice().updateLimjangPrice(newPrice); + note.updateNote(request.nickname(), request.priceType(), request.floor(), request.pyong()); + } catch (Exception e) { + e.printStackTrace(); + } + } + private void validatePriceType(LimjangPurpose purposeType, LimjangPriceType priceType) { if ( (purposeType == LimjangPurpose.RESIDENTIAL_PURPOSE && priceType == LimjangPriceType.MARKET_PRICE) || diff --git a/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java index 7ecc8ce3..17603c2a 100644 --- a/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java @@ -1,8 +1,17 @@ package umc.th.juinjang.auth.jwt; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -10,58 +19,50 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import umc.th.juinjang.common.ExceptionHandler; import umc.th.juinjang.common.code.status.ErrorStatus; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - @Slf4j @Component @RequiredArgsConstructor public class JwtExceptionFilter extends OncePerRequestFilter { - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - try{ - log.info("exception filter"); - doFilter(request,response,filterChain); - log.info("jwt success"); - } catch (NullPointerException e) { - final Map body = new HashMap<>(); - final ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 예외에 맞는 HTTP 상태 코드 설정 - response.setContentType("application/json"); - body.put("timestamp", LocalDateTime.now()); - body.put("code", ErrorStatus.TOKEN_EMPTY.getCode()); - body.put("error", "Bad Request"); - body.put("message", ErrorStatus.TOKEN_EMPTY.getMessage()); // 예외에 맞는 메시지 설정 - body.put("path", request.getRequestURI()); + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + log.info("exception filter"); + filterChain.doFilter(request, response); + log.info("jwt success"); + } catch (NullPointerException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 예외에 맞는 HTTP 상태 코드 설정 + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("code", ErrorStatus.TOKEN_EMPTY.getCode()); + body.put("error", "Bad Request"); + body.put("message", ErrorStatus.TOKEN_EMPTY.getMessage()); // 예외에 맞는 메시지 설정 + body.put("path", request.getRequestURI()); - mapper.writeValue(response.getOutputStream(), body); - logger.info("jwt exception nullpointer"); - throw new ExceptionHandler(ErrorStatus.TOKEN_EMPTY); - } - catch (ExpiredJwtException e) { - final Map body = new HashMap<>(); - final ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - response.setStatus(419); - response.setContentType("application/json"); - body.put("timestamp", LocalDateTime.now()); - body.put("code", ErrorStatus.TOKEN_UNAUTHORIZED.getCode()); - body.put("error", "Unauthorized"); - body.put("message", ErrorStatus.TOKEN_UNAUTHORIZED.getMessage()); - body.put("path", request.getRequestURI()); + mapper.writeValue(response.getOutputStream(), body); + logger.info("jwt exception nullpointer"); + // throw new ExceptionHandler(ErrorStatus.TOKEN_EMPTY); + } catch (ExpiredJwtException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(419); + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("code", ErrorStatus.TOKEN_UNAUTHORIZED.getCode()); + body.put("error", "Unauthorized"); + body.put("message", ErrorStatus.TOKEN_UNAUTHORIZED.getMessage()); + body.put("path", request.getRequestURI()); - mapper.writeValue(response.getOutputStream(), body); - } - } + mapper.writeValue(response.getOutputStream(), body); + } + } } diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java b/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java index 063f51f8..dda98913 100644 --- a/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java @@ -1,6 +1,5 @@ package umc.th.juinjang.domain.limjang.model; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -87,4 +86,22 @@ public void update(Address newAddress) { public String getFullAddress() { return this.roadAddress + " " + this.getAddressDetail(); } + + public static Address empty() { + return Address.builder().build(); // 모든 필드 null + } + + public boolean isEmpty() { + return isBlank(roadAddress) + && isBlank(addressDetail) + && isBlank(bcode) + && isBlank(sido) + && isBlank(sigungo) + && isBlank(bname1) + && isBlank(bname2); + } + + private boolean isBlank(String s) { + return s == null || s.isBlank(); + } } diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java b/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java index 445c926c..7ae36c45 100644 --- a/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java @@ -199,8 +199,10 @@ public static Limjang initNote(Member member, LimjangPrice price, LimjangPurpose .build(); } - public void updateNote(String nickname, LimjangPriceType limjangPriceType, String floor, int pyong) { - this.nickname = nickname; + public void updateNote(String nickname, LimjangPriceType limjangPriceType, String floor, Integer pyong) { + if (nickname != null) { + this.nickname = nickname; + } this.priceType = limjangPriceType; this.floor = floor; this.pyong = pyong; diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java index d6756aa6..0f95c7d0 100644 --- a/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java @@ -1,9 +1,12 @@ package umc.th.juinjang.domain.limjang.model; -import java.util.ArrayList; -import java.util.List; - -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -57,4 +60,8 @@ public String getPrice(LimjangPriceType priceType, LimjangPurpose purpose) { } return null; } + + public static LimjangPrice empty() { + return LimjangPrice.builder().build(); // 모든 price 필드 null + } } From dbe71f0b56f1128e9b54550b143cfc7b9def1ea6 Mon Sep 17 00:00:00 2001 From: hyeonahhh Date: Wed, 21 Jan 2026 17:53:11 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EC=A3=BC=EC=86=8C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/NotePatchRequestV2.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java index faffc0e4..518f175d 100644 --- a/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequestV2.java @@ -39,24 +39,30 @@ public LimjangPrice toUpdatedPrice(LimjangPurpose purpose) { return NotePriceFactory.create(purpose, priceType, price, monthlyRent); } - public Address toUpdatedAddress() { - if (isAddressAllEmpty()) { - return Address.empty(); - } - return Address.create(roadAddress, addressDetail, bcode, sido, sigungu, bname1, bname2); - } + public Address toUpdatedAddress() { + // 둘 다 없음 -> 삭제(주소 null 처리 유도) + if (isDeleteAddressIntent()) { + return Address.empty(); + } - private boolean isAddressAllEmpty() { - return isBlank(roadAddress) - && isBlank(addressDetail) - && isBlank(bcode) - && isBlank(sido) - && isBlank(sigungu) - && isBlank(bname1) - && isBlank(bname2); - } + // 상세주소만 -> 정책상 금지 + if (isDetailOnly()) { + throw new IllegalArgumentException("본주소 없이 상세주소만 입력할 수 없습니다."); + } + + // 본주소가 있으면 생성 + return Address.create(roadAddress, addressDetail, bcode, sido, sigungu, bname1, bname2); + } private boolean isBlank(String s) { return s == null || s.isBlank(); } + + private boolean isDeleteAddressIntent() { // 4번 + return isBlank(roadAddress) && isBlank(addressDetail); + } + + private boolean isDetailOnly() { // 3번 + return isBlank(roadAddress) && !isBlank(addressDetail); + } } From 1475f2219be0284aa3bcf4ee12b4e86d7d7ed906 Mon Sep 17 00:00:00 2001 From: hyeonahhh Date: Wed, 21 Jan 2026 23:35:32 +0900 Subject: [PATCH 3/4] fix: Exclude JwtExceptionFilter from PR --- .../service/command/InitNotePatchTest.java | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java diff --git a/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java b/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java new file mode 100644 index 00000000..139100d6 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java @@ -0,0 +1,271 @@ +package umc.th.juinjang.api.limjang.service.command; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.IntegrationTestSupport; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.api.limjang.controller.request.NoteInitRequest; +import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; +import umc.th.juinjang.api.limjang.service.NoteCommandServiceV2; +import umc.th.juinjang.api.limjang.service.NoteQueryServiceV2; +import umc.th.juinjang.api.limjang.service.response.NotePostResponse; +import umc.th.juinjang.api.limjang.service.response.UserNoteGetResponse; +import umc.th.juinjang.api.limjang.service.response.UserNotesGetResponse; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.repository.MemberRepository; + +@Slf4j +@DisplayName("노트 초기화 테스트") +public class InitNoteTest extends IntegrationTestSupport { + + @Autowired + private MemberRepository memberRepository; + @Autowired + private NoteCommandServiceV2 noteCommandService; + @Autowired + private NoteQueryServiceV2 noteQueryService; + @Autowired + private LimjangRepository limjangRepository; + + private Member firstMember; + private Member secondMember; + private Member thirdMember; + + @BeforeEach + void setUp() { + flushAndTestUsers(); + } + + @Nested + @DisplayName("간소화 된 생성을 했을 경우") + class InitNotes { // 괄호 제거! + + private NotePostResponse noteInitResponse; + + @BeforeEach + void setUpNote() { + NoteInitRequest request = new NoteInitRequest( + LimjangPurpose.RESIDENTIAL_PURPOSE, + LimjangPropertyType.APARTMENT, + LimjangPriceType.MONTHLY_RENT, + "50000", + "4000" + ); + noteInitResponse = noteCommandService.initNote(request, firstMember); + } + + @Test + @DisplayName("UX 리팩토링을 위해, 새로운 노트 생성이 정상적으로 작동하는 가 ?") + void initNote() { + // given + Long createNoteId = noteInitResponse.noteId(); + + // when + Limjang note = limjangRepository.findById(createNoteId).orElseThrow(); + + // then + assertAll( + () -> assertThat(note.getMemberId().getMemberId()).isEqualTo(firstMember.getMemberId()), + () -> assertThat(note.getPurpose()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), + () -> assertThat(note.getPropertyType()).isEqualTo(LimjangPropertyType.APARTMENT), + () -> assertThat(note.getPriceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), + () -> assertThat(note.getRecordCount()).isEqualTo(0), + () -> assertThat(note.getLimjangPrice()).isNotNull(), + () -> assertThat(note.getNickname()).endsWith(" 매물노트") + ); + } + + @Test + @DisplayName("initNote로 생성된 노트가 리스트에 정상 노출") + void initNoteAreVisibleInList() { + // given + log.info("=== 노트 생성 시작 ==="); + Long createdNoteId = noteInitResponse.noteId(); + log.info("생성된 노트 ID: {}", createdNoteId); + + // when + log.info("=== 노트 조회 시작 ==="); + UserNotesGetResponse response = noteQueryService.findUsersNotes( + firstMember, LimjangSortOptions.CREATED, "" + ); + log.info("조회된 노트 개수: {}", response.notes().size()); + + // then + assertThat(response.notes()).hasSize(1); + + UserNotesGetResponse.UserNoteResponse note = response.notes().get(0); + log.info("=== 노트 정보 ==="); + log.info("### notes : {} ", note); + log.info("노트 ID: {}", note.noteId()); + log.info("노트 이름: {}", note.name()); + log.info("가격: {}", note.price()); + log.info("월세: {}", note.monthlyRent()); + + assertAll( + () -> assertInitNoteBasicFields(note, createdNoteId), + () -> assertInitNoteEmptyFields(note) + ); + } + + @Test + @DisplayName("initNote로 생성된 노트의 상세가 정상 노출") + void initNoteDetailAreVisible() { + // given + Long createdNoteId = noteInitResponse.noteId(); + + // when + UserNoteGetResponse response = noteQueryService.findNote(createdNoteId); + log.info("### response : {} ", response); + + // then + assertInitNoteBasicFields(response, createdNoteId); + } + + @Test + @Transactional + @DisplayName("생성된 노트가 정상적으로 수정이 되는 가") + void noteByInited_CanbeUpdate() { + // given + Long createdNoteId = noteInitResponse.noteId(); + + NotePatchRequest updateRequest = new NotePatchRequest( + LimjangPriceType.MONTHLY_RENT, + "60000", // 보증금 변경 + "5000", // 월세 변경 + "서울특별시 강남구 테헤란로 123", // 도로명 주소 + "101동 101호", // 상세 주소 + "1168010100", // 법정동코드 + "강남 아파트 매물노트", // 닉네임 변경 + "5", // 층수 + 32, // 평수 + "서울특별시", // 시도 + "강남구", // 시군구 + "역삼동", // 법정동명1 + "" // 법정동명2 + ); + + // when + noteCommandService.updateNoteV2(createdNoteId, updateRequest); + + // then + Limjang updatedNote = limjangRepository.findById(createdNoteId).orElseThrow(); + + assertAll( + () -> assertThat(updatedNote.getNickname()).isEqualTo("강남 아파트 매물노트"), + () -> assertThat(updatedNote.getPriceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), + () -> assertThat(updatedNote.getFloor()).isEqualTo("5"), + () -> assertThat(updatedNote.getPyong()).isEqualTo(32), + () -> assertThat(updatedNote.getLimjangPrice()).isNotNull(), + () -> assertThat(updatedNote.getLimjangPrice().getMonthlyRent()).isEqualTo("5000"), + () -> assertThat(updatedNote.getAddressEntity()).isNotNull(), + () -> assertThat(updatedNote.getAddressEntity().getRoadAddress()).isEqualTo("서울특별시 강남구 테헤란로 123"), + () -> assertThat(updatedNote.getAddressEntity().getAddressDetail()).isEqualTo("101동 101호") + ); + } + + @Test + @Transactional + @DisplayName("생성된 노트가 정상적으로 삭제가 되는 가") + void noteByInited_CanbeDeleted() { + // given + Long createdNoteId = noteInitResponse.noteId(); + Limjang note = limjangRepository.findById(createdNoteId).orElseThrow(); + + // when + limjangRepository.softDeleteByIds(List.of(createdNoteId)); + limjangRepository.flush(); + + // then + Limjang deletedNote = limjangRepository.findById(createdNoteId).orElseThrow(); + + // 삭제된 노트는 리스트에서 조회되지 않아야 함 + UserNotesGetResponse response = noteQueryService.findUsersNotes( + firstMember, LimjangSortOptions.CREATED, "" + ); + assertThat(response.notes()).isEmpty(); + } + } + + private void assertInitNoteBasicFields(UserNotesGetResponse.UserNoteResponse note, Long expectedId) { + assertAll( + () -> assertThat(note.noteId()).isEqualTo(expectedId), + () -> assertThat(note.purposeType()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), + () -> assertThat(note.propertyType()).isEqualTo(LimjangPropertyType.APARTMENT), + () -> assertThat(note.priceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), + () -> assertThat(note.isScraped()).isFalse(), + () -> assertThat(note.name()).endsWith(" 매물노트"), + () -> assertThat(note.price()).isEqualTo("50000"), + () -> assertThat(note.monthlyRent()).isEqualTo("4000") + ); + } + + private void assertInitNoteBasicFields(UserNoteGetResponse note, Long expectedId) { + assertAll( + () -> assertThat(note.purposeType()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), + () -> assertThat(note.propertyType()).isEqualTo(LimjangPropertyType.APARTMENT), + () -> assertThat(note.priceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), + () -> assertThat(note.price()).isEqualTo("50000"), + () -> assertThat(note.monthlyRent()).isEqualTo("4000") + ); + } + + private void assertInitNoteEmptyFields(UserNotesGetResponse.UserNoteResponse note) { + assertAll( + () -> assertThat(note.address()).isNull(), + () -> assertThat(note.shortAddress()).isNull(), + () -> assertThat(note.pyong()).isNull(), + () -> assertThat(note.floor()).isNull(), + () -> assertThat(note.rate()).isNull(), + () -> assertThat(note.imageUrl()).isEmpty() + ); + } + + private void flushAndTestUsers() { + memberRepository.deleteAll(); + + // 첫 번째 멤버 (Apple) + firstMember = Member.createAppleMember( + "first@apple.com", + "apple_sub_001", + "첫번째유저", + "1.0.0" + ); + memberRepository.save(firstMember); + + // 두 번째 멤버 (Kakao) + secondMember = Member.createKakaoMember( + "second@kakao.com", + 12345L, + "두번째유저", + "1.0.0" + ); + memberRepository.save(secondMember); + + // 세 번째 멤버 (Apple) + thirdMember = Member.createAppleMember( + "third@apple.com", + "apple_sub_002", + "세번째유저", + "1.0.0" + ); + memberRepository.save(thirdMember); + + memberRepository.flush(); + } +} \ No newline at end of file From 5b8a593db773dddca3e5dcc81068dcb6a396e4f4 Mon Sep 17 00:00:00 2001 From: hyeonahhh Date: Wed, 21 Jan 2026 23:39:43 +0900 Subject: [PATCH 4/4] Exclude JwtExceptionFilter and InitNotePatchTest from PR --- .../juinjang/auth/jwt/JwtExceptionFilter.java | 91 +++--- .../service/command/InitNotePatchTest.java | 271 ------------------ 2 files changed, 45 insertions(+), 317 deletions(-) delete mode 100644 src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java diff --git a/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java index 17603c2a..7ecc8ce3 100644 --- a/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java @@ -1,17 +1,8 @@ package umc.th.juinjang.auth.jwt; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -19,50 +10,58 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.th.juinjang.common.ExceptionHandler; import umc.th.juinjang.common.code.status.ErrorStatus; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + @Slf4j @Component @RequiredArgsConstructor public class JwtExceptionFilter extends OncePerRequestFilter { - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - try { - log.info("exception filter"); - filterChain.doFilter(request, response); - log.info("jwt success"); - } catch (NullPointerException e) { - final Map body = new HashMap<>(); - final ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 예외에 맞는 HTTP 상태 코드 설정 - response.setContentType("application/json"); - body.put("timestamp", LocalDateTime.now()); - body.put("code", ErrorStatus.TOKEN_EMPTY.getCode()); - body.put("error", "Bad Request"); - body.put("message", ErrorStatus.TOKEN_EMPTY.getMessage()); // 예외에 맞는 메시지 설정 - body.put("path", request.getRequestURI()); + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try{ + log.info("exception filter"); + doFilter(request,response,filterChain); + log.info("jwt success"); + } catch (NullPointerException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 예외에 맞는 HTTP 상태 코드 설정 + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("code", ErrorStatus.TOKEN_EMPTY.getCode()); + body.put("error", "Bad Request"); + body.put("message", ErrorStatus.TOKEN_EMPTY.getMessage()); // 예외에 맞는 메시지 설정 + body.put("path", request.getRequestURI()); - mapper.writeValue(response.getOutputStream(), body); - logger.info("jwt exception nullpointer"); - // throw new ExceptionHandler(ErrorStatus.TOKEN_EMPTY); - } catch (ExpiredJwtException e) { - final Map body = new HashMap<>(); - final ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - response.setStatus(419); - response.setContentType("application/json"); - body.put("timestamp", LocalDateTime.now()); - body.put("code", ErrorStatus.TOKEN_UNAUTHORIZED.getCode()); - body.put("error", "Unauthorized"); - body.put("message", ErrorStatus.TOKEN_UNAUTHORIZED.getMessage()); - body.put("path", request.getRequestURI()); + mapper.writeValue(response.getOutputStream(), body); + logger.info("jwt exception nullpointer"); + throw new ExceptionHandler(ErrorStatus.TOKEN_EMPTY); + } + catch (ExpiredJwtException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(419); + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("code", ErrorStatus.TOKEN_UNAUTHORIZED.getCode()); + body.put("error", "Unauthorized"); + body.put("message", ErrorStatus.TOKEN_UNAUTHORIZED.getMessage()); + body.put("path", request.getRequestURI()); - mapper.writeValue(response.getOutputStream(), body); - } - } + mapper.writeValue(response.getOutputStream(), body); + } + } } diff --git a/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java b/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java deleted file mode 100644 index 139100d6..00000000 --- a/src/test/java/umc/th/juinjang/api/limjang/service/command/InitNotePatchTest.java +++ /dev/null @@ -1,271 +0,0 @@ -package umc.th.juinjang.api.limjang.service.command; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import jakarta.transaction.Transactional; -import lombok.extern.slf4j.Slf4j; -import umc.th.juinjang.api.IntegrationTestSupport; -import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; -import umc.th.juinjang.api.limjang.controller.request.NoteInitRequest; -import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; -import umc.th.juinjang.api.limjang.service.NoteCommandServiceV2; -import umc.th.juinjang.api.limjang.service.NoteQueryServiceV2; -import umc.th.juinjang.api.limjang.service.response.NotePostResponse; -import umc.th.juinjang.api.limjang.service.response.UserNoteGetResponse; -import umc.th.juinjang.api.limjang.service.response.UserNotesGetResponse; -import umc.th.juinjang.domain.limjang.model.Limjang; -import umc.th.juinjang.domain.limjang.model.LimjangPriceType; -import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; -import umc.th.juinjang.domain.limjang.model.LimjangPurpose; -import umc.th.juinjang.domain.limjang.repository.LimjangRepository; -import umc.th.juinjang.domain.member.model.Member; -import umc.th.juinjang.domain.member.repository.MemberRepository; - -@Slf4j -@DisplayName("노트 초기화 테스트") -public class InitNoteTest extends IntegrationTestSupport { - - @Autowired - private MemberRepository memberRepository; - @Autowired - private NoteCommandServiceV2 noteCommandService; - @Autowired - private NoteQueryServiceV2 noteQueryService; - @Autowired - private LimjangRepository limjangRepository; - - private Member firstMember; - private Member secondMember; - private Member thirdMember; - - @BeforeEach - void setUp() { - flushAndTestUsers(); - } - - @Nested - @DisplayName("간소화 된 생성을 했을 경우") - class InitNotes { // 괄호 제거! - - private NotePostResponse noteInitResponse; - - @BeforeEach - void setUpNote() { - NoteInitRequest request = new NoteInitRequest( - LimjangPurpose.RESIDENTIAL_PURPOSE, - LimjangPropertyType.APARTMENT, - LimjangPriceType.MONTHLY_RENT, - "50000", - "4000" - ); - noteInitResponse = noteCommandService.initNote(request, firstMember); - } - - @Test - @DisplayName("UX 리팩토링을 위해, 새로운 노트 생성이 정상적으로 작동하는 가 ?") - void initNote() { - // given - Long createNoteId = noteInitResponse.noteId(); - - // when - Limjang note = limjangRepository.findById(createNoteId).orElseThrow(); - - // then - assertAll( - () -> assertThat(note.getMemberId().getMemberId()).isEqualTo(firstMember.getMemberId()), - () -> assertThat(note.getPurpose()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), - () -> assertThat(note.getPropertyType()).isEqualTo(LimjangPropertyType.APARTMENT), - () -> assertThat(note.getPriceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), - () -> assertThat(note.getRecordCount()).isEqualTo(0), - () -> assertThat(note.getLimjangPrice()).isNotNull(), - () -> assertThat(note.getNickname()).endsWith(" 매물노트") - ); - } - - @Test - @DisplayName("initNote로 생성된 노트가 리스트에 정상 노출") - void initNoteAreVisibleInList() { - // given - log.info("=== 노트 생성 시작 ==="); - Long createdNoteId = noteInitResponse.noteId(); - log.info("생성된 노트 ID: {}", createdNoteId); - - // when - log.info("=== 노트 조회 시작 ==="); - UserNotesGetResponse response = noteQueryService.findUsersNotes( - firstMember, LimjangSortOptions.CREATED, "" - ); - log.info("조회된 노트 개수: {}", response.notes().size()); - - // then - assertThat(response.notes()).hasSize(1); - - UserNotesGetResponse.UserNoteResponse note = response.notes().get(0); - log.info("=== 노트 정보 ==="); - log.info("### notes : {} ", note); - log.info("노트 ID: {}", note.noteId()); - log.info("노트 이름: {}", note.name()); - log.info("가격: {}", note.price()); - log.info("월세: {}", note.monthlyRent()); - - assertAll( - () -> assertInitNoteBasicFields(note, createdNoteId), - () -> assertInitNoteEmptyFields(note) - ); - } - - @Test - @DisplayName("initNote로 생성된 노트의 상세가 정상 노출") - void initNoteDetailAreVisible() { - // given - Long createdNoteId = noteInitResponse.noteId(); - - // when - UserNoteGetResponse response = noteQueryService.findNote(createdNoteId); - log.info("### response : {} ", response); - - // then - assertInitNoteBasicFields(response, createdNoteId); - } - - @Test - @Transactional - @DisplayName("생성된 노트가 정상적으로 수정이 되는 가") - void noteByInited_CanbeUpdate() { - // given - Long createdNoteId = noteInitResponse.noteId(); - - NotePatchRequest updateRequest = new NotePatchRequest( - LimjangPriceType.MONTHLY_RENT, - "60000", // 보증금 변경 - "5000", // 월세 변경 - "서울특별시 강남구 테헤란로 123", // 도로명 주소 - "101동 101호", // 상세 주소 - "1168010100", // 법정동코드 - "강남 아파트 매물노트", // 닉네임 변경 - "5", // 층수 - 32, // 평수 - "서울특별시", // 시도 - "강남구", // 시군구 - "역삼동", // 법정동명1 - "" // 법정동명2 - ); - - // when - noteCommandService.updateNoteV2(createdNoteId, updateRequest); - - // then - Limjang updatedNote = limjangRepository.findById(createdNoteId).orElseThrow(); - - assertAll( - () -> assertThat(updatedNote.getNickname()).isEqualTo("강남 아파트 매물노트"), - () -> assertThat(updatedNote.getPriceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), - () -> assertThat(updatedNote.getFloor()).isEqualTo("5"), - () -> assertThat(updatedNote.getPyong()).isEqualTo(32), - () -> assertThat(updatedNote.getLimjangPrice()).isNotNull(), - () -> assertThat(updatedNote.getLimjangPrice().getMonthlyRent()).isEqualTo("5000"), - () -> assertThat(updatedNote.getAddressEntity()).isNotNull(), - () -> assertThat(updatedNote.getAddressEntity().getRoadAddress()).isEqualTo("서울특별시 강남구 테헤란로 123"), - () -> assertThat(updatedNote.getAddressEntity().getAddressDetail()).isEqualTo("101동 101호") - ); - } - - @Test - @Transactional - @DisplayName("생성된 노트가 정상적으로 삭제가 되는 가") - void noteByInited_CanbeDeleted() { - // given - Long createdNoteId = noteInitResponse.noteId(); - Limjang note = limjangRepository.findById(createdNoteId).orElseThrow(); - - // when - limjangRepository.softDeleteByIds(List.of(createdNoteId)); - limjangRepository.flush(); - - // then - Limjang deletedNote = limjangRepository.findById(createdNoteId).orElseThrow(); - - // 삭제된 노트는 리스트에서 조회되지 않아야 함 - UserNotesGetResponse response = noteQueryService.findUsersNotes( - firstMember, LimjangSortOptions.CREATED, "" - ); - assertThat(response.notes()).isEmpty(); - } - } - - private void assertInitNoteBasicFields(UserNotesGetResponse.UserNoteResponse note, Long expectedId) { - assertAll( - () -> assertThat(note.noteId()).isEqualTo(expectedId), - () -> assertThat(note.purposeType()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), - () -> assertThat(note.propertyType()).isEqualTo(LimjangPropertyType.APARTMENT), - () -> assertThat(note.priceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), - () -> assertThat(note.isScraped()).isFalse(), - () -> assertThat(note.name()).endsWith(" 매물노트"), - () -> assertThat(note.price()).isEqualTo("50000"), - () -> assertThat(note.monthlyRent()).isEqualTo("4000") - ); - } - - private void assertInitNoteBasicFields(UserNoteGetResponse note, Long expectedId) { - assertAll( - () -> assertThat(note.purposeType()).isEqualTo(LimjangPurpose.RESIDENTIAL_PURPOSE), - () -> assertThat(note.propertyType()).isEqualTo(LimjangPropertyType.APARTMENT), - () -> assertThat(note.priceType()).isEqualTo(LimjangPriceType.MONTHLY_RENT), - () -> assertThat(note.price()).isEqualTo("50000"), - () -> assertThat(note.monthlyRent()).isEqualTo("4000") - ); - } - - private void assertInitNoteEmptyFields(UserNotesGetResponse.UserNoteResponse note) { - assertAll( - () -> assertThat(note.address()).isNull(), - () -> assertThat(note.shortAddress()).isNull(), - () -> assertThat(note.pyong()).isNull(), - () -> assertThat(note.floor()).isNull(), - () -> assertThat(note.rate()).isNull(), - () -> assertThat(note.imageUrl()).isEmpty() - ); - } - - private void flushAndTestUsers() { - memberRepository.deleteAll(); - - // 첫 번째 멤버 (Apple) - firstMember = Member.createAppleMember( - "first@apple.com", - "apple_sub_001", - "첫번째유저", - "1.0.0" - ); - memberRepository.save(firstMember); - - // 두 번째 멤버 (Kakao) - secondMember = Member.createKakaoMember( - "second@kakao.com", - 12345L, - "두번째유저", - "1.0.0" - ); - memberRepository.save(secondMember); - - // 세 번째 멤버 (Apple) - thirdMember = Member.createAppleMember( - "third@apple.com", - "apple_sub_002", - "세번째유저", - "1.0.0" - ); - memberRepository.save(thirdMember); - - memberRepository.flush(); - } -} \ No newline at end of file