diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 58c892f9..59b402f6 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -103,31 +103,31 @@ include::{snippets}/auth-controller-test/kakao-logout/response-body.adoc[] == 토큰 상태 확인 -쿠키에 담긴 토큰의 유효성을 확인합니다 +쿠키에 담긴 토큰의 유효성을 확인합니다. === Example -include::{snippets}/auth-controller-test/check-auth_-success/http-request.adoc[] +include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[] === HTTP ==== 요청 -include::{snippets}/auth-controller-test/check-auth_-success/curl-request.adoc[] +include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[] ==== 응답 -include::{snippets}/auth-controller-test/check-auth_-success/http-response.adoc[] +include::{snippets}/auth-controller-test/check-auth-success/http-response.adoc[] === Body ==== 응답 (valid) -include::{snippets}/auth-controller-test/check-auth_-success/response-body.adoc[] +include::{snippets}/auth-controller-test/check-auth-success/response-body.adoc[] ==== 응답 (invalid) -include::{snippets}/auth-controller-test/check-auth_-expired/response-body.adoc[] +include::{snippets}/auth-controller-test/check-auth-expired/response-body.adoc[] -include::{snippets}/auth-controller-test/check-auth_-invalid-token/response-body.adoc[] +include::{snippets}/auth-controller-test/check-auth-invalid-token/response-body.adoc[] diff --git a/src/docs/asciidoc/settlement.adoc b/src/docs/asciidoc/settlement.adoc index 24c1198b..bb553f75 100644 --- a/src/docs/asciidoc/settlement.adoc +++ b/src/docs/asciidoc/settlement.adoc @@ -146,20 +146,20 @@ user가 속한 정산의 공유 링크 리스트를 조회할 수 있다. === Example -include::{snippets}/settlement-controller-test/get-share-link-list_-success/curl-request.adoc[] +include::{snippets}/settlement-controller-test/get-share-link-list-success/curl-request.adoc[] === HTTP ==== 요청 -include::{snippets}/settlement-controller-test/get-share-link-list_-success/http-request.adoc[] +include::{snippets}/settlement-controller-test/get-share-link-list-success/http-request.adoc[] ==== 응답 -include::{snippets}/settlement-controller-test/get-share-link-list_-success/http-response.adoc[] +include::{snippets}/settlement-controller-test/get-share-link-list-success/http-response.adoc[] === Body ==== 응답 -include::{snippets}/settlement-controller-test/get-share-link-list_-success/response-body.adoc[] \ No newline at end of file +include::{snippets}/settlement-controller-test/get-share-link-list-success/response-body.adoc[] \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/auth/presentation/AuthController.java b/src/main/java/com/dnd/moddo/auth/presentation/AuthController.java index 2453bdb7..a2040812 100644 --- a/src/main/java/com/dnd/moddo/auth/presentation/AuthController.java +++ b/src/main/java/com/dnd/moddo/auth/presentation/AuthController.java @@ -1,5 +1,7 @@ package com.dnd.moddo.auth.presentation; +import static com.dnd.moddo.auth.infrastructure.security.JwtConstants.*; + import java.io.IOException; import java.util.Collections; @@ -100,8 +102,8 @@ public ResponseEntity checkAuth( try { Claims claims = jwtProvider.parseClaims(token); - Long userId = jwtProvider.getUserId(token); - String role = jwtProvider.getRole(token); + Long userId = claims.get(AUTH_ID.getMessage(), Long.class); + String role = claims.get(ROLE.getMessage(), String.class); return ResponseEntity.ok( AuthCheckResponse.success(userId, role) diff --git a/src/main/java/com/dnd/moddo/auth/presentation/response/AuthCheckResponse.java b/src/main/java/com/dnd/moddo/auth/presentation/response/AuthCheckResponse.java index 6cfa4b67..67bce163 100644 --- a/src/main/java/com/dnd/moddo/auth/presentation/response/AuthCheckResponse.java +++ b/src/main/java/com/dnd/moddo/auth/presentation/response/AuthCheckResponse.java @@ -1,5 +1,8 @@ package com.dnd.moddo.auth.presentation.response; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) public record AuthCheckResponse( boolean authenticated, UserInfo user, diff --git a/src/main/java/com/dnd/moddo/common/exception/GlobalExceptionHandler.java b/src/main/java/com/dnd/moddo/common/exception/GlobalExceptionHandler.java index e0406976..d74c08da 100644 --- a/src/main/java/com/dnd/moddo/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/dnd/moddo/common/exception/GlobalExceptionHandler.java @@ -70,12 +70,9 @@ public ResponseEntity handleValidation( MethodArgumentNotValidException e ) { - String message = e.getBindingResult() - .getFieldErrors() - .stream() - .findFirst() - .map(error -> error.getField() + " : " + error.getDefaultMessage()) - .orElse("잘못된 요청입니다."); + LoggingUtils.warn(e); + + String message = e.getMessage(); return ResponseEntity.badRequest() .body(new ErrorResponse(400, message)); diff --git a/src/main/java/com/dnd/moddo/event/application/query/QuerySettlementService.java b/src/main/java/com/dnd/moddo/event/application/query/QuerySettlementService.java index edeb54d8..222ec6dc 100644 --- a/src/main/java/com/dnd/moddo/event/application/query/QuerySettlementService.java +++ b/src/main/java/com/dnd/moddo/event/application/query/QuerySettlementService.java @@ -9,6 +9,7 @@ import com.dnd.moddo.event.application.impl.SettlementValidator; import com.dnd.moddo.event.domain.member.Member; import com.dnd.moddo.event.domain.settlement.Settlement; +import com.dnd.moddo.event.domain.settlement.type.SettlementSortType; import com.dnd.moddo.event.domain.settlement.type.SettlementStatus; import com.dnd.moddo.event.presentation.request.SearchSettlementListRequest; import com.dnd.moddo.event.presentation.response.SettlementDetailResponse; @@ -51,9 +52,12 @@ public List search( SettlementStatus effectiveStatus = request.status() == null ? SettlementStatus.ALL : request.status(); + SettlementSortType effectiveSort = + request.sort() == null ? SettlementSortType.LATEST : request.sort(); + int limit = request.limit() == null ? 10 : request.limit(); - return settlementReader.findListByUserIdAndStatus(userId, effectiveStatus, request.sort(), limit); + return settlementReader.findListByUserIdAndStatus(userId, effectiveStatus, effectiveSort, limit); } public List findSettlementShareList(Long userId) { diff --git a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java index aa346ccd..15226fae 100644 --- a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java +++ b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Repository; +import com.dnd.moddo.event.domain.expense.QExpense; import com.dnd.moddo.event.domain.member.QMember; import com.dnd.moddo.event.domain.settlement.QSettlement; import com.dnd.moddo.event.domain.settlement.type.SettlementSortType; @@ -15,6 +16,8 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -35,6 +38,7 @@ public List findByUserAndStatus( ) { QSettlement settlement = QSettlement.settlement; QMember member = QMember.member; + QExpense expense = QExpense.expense; BooleanExpression userCondition = member.user.id.eq(userId); @@ -61,6 +65,12 @@ public List findByUserAndStatus( member.isPaid ); + JPQLQuery totalAmount = + JPAExpressions + .select(expense.amount.sum().coalesce(0L)) + .from(expense) + .where(expense.settlement.id.eq(settlement.id)); + OrderSpecifier orderSpecifier = getOrderSpecifier(sortType, settlement, memberCount, completedCount); @@ -70,6 +80,7 @@ public List findByUserAndStatus( settlement.id, settlement.code, settlement.name, + totalAmount, memberCount, completedCount.coalesce(0L), settlement.createdAt, diff --git a/src/main/java/com/dnd/moddo/event/presentation/request/SearchSettlementListRequest.java b/src/main/java/com/dnd/moddo/event/presentation/request/SearchSettlementListRequest.java index f62bbafa..aebcccd3 100644 --- a/src/main/java/com/dnd/moddo/event/presentation/request/SearchSettlementListRequest.java +++ b/src/main/java/com/dnd/moddo/event/presentation/request/SearchSettlementListRequest.java @@ -15,6 +15,7 @@ public record SearchSettlementListRequest( @NotNull(message = "정렬 방식은 필수입니다.") SettlementSortType sort, + @NotNull(message = "limit은 필수입니다.") @Min(value = 1, message = "limit은 1 이상이어야 합니다.") @Max(value = 100, message = "limit은 최대 100까지 가능합니다.") Integer limit diff --git a/src/main/java/com/dnd/moddo/event/presentation/response/SettlementListResponse.java b/src/main/java/com/dnd/moddo/event/presentation/response/SettlementListResponse.java index e2df0ae0..aa7c4490 100644 --- a/src/main/java/com/dnd/moddo/event/presentation/response/SettlementListResponse.java +++ b/src/main/java/com/dnd/moddo/event/presentation/response/SettlementListResponse.java @@ -6,6 +6,7 @@ public record SettlementListResponse( Long groupId, String groupCode, String name, + Long totalAmount, Long totalMemberCount, Long completedMemberCount, LocalDateTime createdAt, diff --git a/src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java b/src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java index a15333a3..438cfe03 100644 --- a/src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java +++ b/src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java @@ -1,6 +1,7 @@ package com.dnd.moddo.domain.Member.entity; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -10,6 +11,7 @@ import com.dnd.moddo.event.domain.member.Member; import com.dnd.moddo.event.domain.settlement.Settlement; import com.dnd.moddo.global.support.GroupTestFactory; +import com.dnd.moddo.user.domain.User; class MemberTest { @@ -74,4 +76,65 @@ void testUpdatePaymentStatus() { assertThat(member.isPaid()).isTrue(); assertThat(member.getPaidAt()).isNotNull(); } + + @DisplayName("사용자를 정상적으로 할당할 수 있다.") + @Test + void assignUser_success() { + // given + Member member = Member.builder() + .name("기존이름") + .settlement(mockSettlement) + .role(ExpenseRole.PARTICIPANT) + .isPaid(false) + .build(); + + User user = mock(User.class); + when(user.getName()).thenReturn("새이름"); + + // when + member.assignUser(user); + + // then + assertThat(member.getUser()).isEqualTo(user); + assertThat(member.getName()).isEqualTo("새이름"); // 동기화 확인 + } + + @DisplayName("이미 사용자가 연결되어 있으면 예외가 발생한다.") + @Test + void assignUser_throwException_whenUserAlreadyAssigned() { + // given + Member member = Member.builder() + .name("기존이름") + .settlement(mockSettlement) + .role(ExpenseRole.PARTICIPANT) + .isPaid(false) + .build(); + + User firstUser = mock(User.class); + User secondUser = mock(User.class); + + when(firstUser.getName()).thenReturn("첫번째"); + when(secondUser.getName()).thenReturn("두번째"); + + member.assignUser(firstUser); + + // when & then + assertThatThrownBy(() -> member.assignUser(secondUser)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("이미 사용자와 연결된 멤버입니다."); + } + + @DisplayName("null 사용자를 할당하면 예외가 발생한다.") + @Test + void assignUser_throwException_whenUserIsNull() { + Member member = Member.builder() + .name("기존이름") + .settlement(mockSettlement) + .role(ExpenseRole.PARTICIPANT) + .isPaid(false) + .build(); + + assertThatThrownBy(() -> member.assignUser(null)) + .isInstanceOf(NullPointerException.class); + } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java index fa8d82fc..a7feaea8 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java @@ -1,5 +1,6 @@ package com.dnd.moddo.domain.auth.controller; +import static com.dnd.moddo.auth.infrastructure.security.JwtConstants.*; import static org.mockito.BDDMockito.*; import static org.springframework.restdocs.cookies.CookieDocumentation.*; import static org.springframework.restdocs.headers.HeaderDocumentation.*; @@ -141,14 +142,14 @@ void kakaoLogout() throws Exception { @Test @DisplayName("토큰이 유효하면 인증 성공 응답을 반환한다.") - void checkAuth_Success() throws Exception { + void checkAuthSuccess() throws Exception { // given String token = "valid-token"; Claims claims = mock(Claims.class); given(jwtProvider.parseClaims(token)).willReturn(claims); - given(jwtProvider.getUserId(token)).willReturn(1L); - given(jwtProvider.getRole(token)).willReturn("USER"); + given(claims.get(AUTH_ID.getMessage(), Long.class)).willReturn(1L); + given(claims.get(ROLE.getMessage(), String.class)).willReturn("USER"); // when & then mockMvc.perform(get("/api/v1/auth/check") @@ -163,15 +164,14 @@ void checkAuth_Success() throws Exception { fieldWithPath("authenticated").description("인증 여부"), fieldWithPath("user").description("사용자 정보").optional(), fieldWithPath("user.id").description("사용자 ID").optional(), - fieldWithPath("user.role").description("사용자 권한").optional(), - fieldWithPath("reason").description("실패 사유 (인증 실패 시)").optional() + fieldWithPath("user.role").description("사용자 권한").optional() ) )); } @Test @DisplayName("토큰이 없으면 NO_TOKEN 응답을 반환한다.") - void checkAuth_NoToken() throws Exception { + void checkAuthNoToken() throws Exception { mockMvc.perform(get("/api/v1/auth/check")) .andExpect(status().isOk()) @@ -180,7 +180,6 @@ void checkAuth_NoToken() throws Exception { .andDo(restDocs.document( responseFields( fieldWithPath("authenticated").description("인증 여부"), - fieldWithPath("user").description("사용자 정보").optional(), fieldWithPath("reason").description("실패 사유 (NO_TOKEN)") ) )); @@ -188,7 +187,7 @@ void checkAuth_NoToken() throws Exception { @Test @DisplayName("토큰이 만료되면 TOKEN_EXPIRED 응답을 반환한다.") - void checkAuth_Expired() throws Exception { + void checkAuthExpired() throws Exception { // given String token = "expired-token"; @@ -203,7 +202,6 @@ void checkAuth_Expired() throws Exception { .andDo(restDocs.document( responseFields( fieldWithPath("authenticated").description("인증 여부"), - fieldWithPath("user").description("사용자 정보").optional(), fieldWithPath("reason").description("실패 사유 (TOKEN_EXPIRED)") ) )); @@ -211,7 +209,7 @@ void checkAuth_Expired() throws Exception { @Test @DisplayName("토큰이 유효하지 않으면 INVALID_TOKEN 응답을 반환한다.") - void checkAuth_InvalidToken() throws Exception { + void checkAuthInvalidToken() throws Exception { // given String token = "invalid-token"; @@ -226,7 +224,6 @@ void checkAuth_InvalidToken() throws Exception { .andDo(restDocs.document( responseFields( fieldWithPath("authenticated").description("인증 여부"), - fieldWithPath("user").description("사용자 정보").optional(), fieldWithPath("reason").description("실패 사유 (INVALID_TOKEN)") ) )); diff --git a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java index 713394d1..6a90dcc7 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java @@ -139,6 +139,7 @@ void searchSettlementList() throws Exception { 1L, "groupCode", "모또 모임", + 10000L, 5L, 3L, fixedTime, @@ -167,6 +168,7 @@ void searchSettlementList() throws Exception { .andExpect(jsonPath("$[0].groupId").value(1L)) .andExpect(jsonPath("$[0].groupCode").value("groupCode")) .andExpect(jsonPath("$[0].name").value("모또 모임")) + .andExpect(jsonPath("$[0].totalAmount").value(10000L)) .andExpect(jsonPath("$[0].totalMemberCount").value(5L)) .andExpect(jsonPath("$[0].completedMemberCount").value(3L)) .andExpect(jsonPath("$[0].createdAt").value(fixedTime.toString())) @@ -187,6 +189,7 @@ void searchSettlementList() throws Exception { fieldWithPath("[].groupId").description("정산 ID"), fieldWithPath("[].groupCode").description("정산 코드"), fieldWithPath("[].name").description("정산 이름"), + fieldWithPath("[].totalAmount").description("총 정산 금"), fieldWithPath("[].totalMemberCount").description("총 참여자 수"), fieldWithPath("[].completedMemberCount").description("입금 완료 참여자 수"), fieldWithPath("[].createdAt").description("정산 생성일시"), @@ -197,7 +200,7 @@ void searchSettlementList() throws Exception { @Test @DisplayName("공유용 정산 리스트를 정상적으로 조회할 수 있다.") - void getShareLinkList_Success() throws Exception { + void getShareLinkListSuccess() throws Exception { // given Long userId = 1L; @@ -233,7 +236,7 @@ void getShareLinkList_Success() throws Exception { .andExpect(jsonPath("$[0].settlementId").value(1L)) .andExpect(jsonPath("$[0].name").value("모또 모임")) .andExpect(jsonPath("$[0].groupCode").value("groupCode")) - .andExpect(jsonPath("$[0].completedAt").isEmpty()) + .andExpect(jsonPath("$[0].completedAt").value(Matchers.nullValue())) .andDo(restDocs.document( responseFields( fieldWithPath("[].settlementId").description("정산 ID"), diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/QuerySettlementServiceTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/QuerySettlementServiceTest.java index df0b8e06..976e7b1e 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/QuerySettlementServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/QuerySettlementServiceTest.java @@ -256,6 +256,7 @@ void search_WhenStatusExists_ShouldUseGivenStatus() { 1L, "groupCode", "모또 모임", + 30000L, 5L, 3L, LocalDateTime.now(), diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementReaderTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementReaderTest.java index 818b49bc..3c349b0a 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementReaderTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementReaderTest.java @@ -149,19 +149,21 @@ void findListByUserAndStatus_Success() { 1L, "groupCode", "모또 모임", + 10000L, 5L, 3L, LocalDateTime.now(), - LocalDateTime.now() + null ), new SettlementListResponse( 2L, "groupCode2", "두번째 모임", + 50000L, 4L, 4L, LocalDateTime.now(), - LocalDateTime.now() + null ) ); @@ -208,7 +210,7 @@ void findShareListByUserId_Success() { "두번째 모임", "groupCode2", LocalDateTime.now(), - LocalDateTime.now() + null ) );