diff --git a/src/main/java/com/sillim/recordit/global/exception/ErrorCode.java b/src/main/java/com/sillim/recordit/global/exception/ErrorCode.java index a063eea7..c6136e3b 100644 --- a/src/main/java/com/sillim/recordit/global/exception/ErrorCode.java +++ b/src/main/java/com/sillim/recordit/global/exception/ErrorCode.java @@ -68,6 +68,7 @@ public enum ErrorCode { INVALID_DIFFERENCE_OF_DATE("ERR_WEEKLY_GOAL_003", "주 목표 기간의 시작일과 종료일은 6일 차이여야 합니다."), WEEKLY_GOAL_ACCESS_DENIED("ERR_WEEKLY_GOAL_004", "해당 사용자는 접근할 수 없는 주 목표입니다."), WEEK_NOT_CONTAINS_DATE("ERR_WEEKLY_GOAL_005", "해당 주차에 존재하지 않는 날짜입니다."), + RELATED_GOAL_NOT_FOUND("ERR_WEEKLY_GOAL_006", "해당 주 목표에 연관 목표가 존재하지 않습니다."), // goal NULL_GOAL_TITLE("ERR_GOAL_001", "목표 제목은 null일 수 없습니다."), diff --git a/src/main/java/com/sillim/recordit/global/exception/goal/InvalidWeeklyGoalException.java b/src/main/java/com/sillim/recordit/global/exception/goal/InvalidWeeklyGoalException.java new file mode 100644 index 00000000..079d768f --- /dev/null +++ b/src/main/java/com/sillim/recordit/global/exception/goal/InvalidWeeklyGoalException.java @@ -0,0 +1,15 @@ +package com.sillim.recordit.global.exception.goal; + +import com.sillim.recordit.global.exception.ErrorCode; +import com.sillim.recordit.global.exception.common.ApplicationException; + +public class InvalidWeeklyGoalException extends ApplicationException { + + public InvalidWeeklyGoalException(ErrorCode errorCode) { + super(errorCode); + } + + public InvalidWeeklyGoalException(ErrorCode errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/com/sillim/recordit/goal/controller/MonthlyGoalController.java b/src/main/java/com/sillim/recordit/goal/controller/MonthlyGoalController.java index 35865e1d..d652efb7 100644 --- a/src/main/java/com/sillim/recordit/goal/controller/MonthlyGoalController.java +++ b/src/main/java/com/sillim/recordit/goal/controller/MonthlyGoalController.java @@ -72,7 +72,7 @@ public ResponseEntity monthlyGoalDetails( monthlyGoalQueryService.searchByIdAndCheckAuthority(id, member.getId()))); } - @PatchMapping("/{id}") + @PatchMapping("/{id}/achieve") public ResponseEntity monthlyGoalChangeAchieveStatus( @PathVariable final Long id, @RequestParam final Boolean status, diff --git a/src/main/java/com/sillim/recordit/goal/controller/WeeklyGoalController.java b/src/main/java/com/sillim/recordit/goal/controller/WeeklyGoalController.java index e936e00d..4e6d88d4 100644 --- a/src/main/java/com/sillim/recordit/goal/controller/WeeklyGoalController.java +++ b/src/main/java/com/sillim/recordit/goal/controller/WeeklyGoalController.java @@ -87,7 +87,7 @@ public ResponseEntity modifyWeeklyGoal( return ResponseEntity.noContent().build(); } - @PatchMapping("/{id}") + @PatchMapping("/{id}/achieve") public ResponseEntity changeWeeklyGoalAchieveStatus( @PathVariable final Long id, @RequestParam final Boolean status, @@ -105,6 +105,26 @@ public ResponseEntity removeWeeklyGoal( return ResponseEntity.noContent().build(); } + @PatchMapping("/{id}/link") + public ResponseEntity linkRelatedMonthlyGoal( + @PathVariable final Long id, + @RequestParam final Long relatedGoalId, + @CurrentMember final Member member) { + + weeklyGoalUpdateService.linkRelatedMonthlyGoal(id, relatedGoalId, member.getId()); + + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/{id}/unlink") + public ResponseEntity unlinkRelatedMonthlyGoal( + @PathVariable final Long id, @CurrentMember final Member member) { + + weeklyGoalUpdateService.unlinkRelatedMonthlyGoal(id, member.getId()); + + return ResponseEntity.noContent().build(); + } + private Integer changeWeekIfMonthOfStartDateIsNotEqual( final WeeklyGoal weeklyGoal, final Integer currentMonth) { if (weeklyGoal.getStartDate().getMonthValue() == currentMonth) { diff --git a/src/main/java/com/sillim/recordit/goal/domain/WeeklyGoal.java b/src/main/java/com/sillim/recordit/goal/domain/WeeklyGoal.java index 60a3bf72..4e44de89 100644 --- a/src/main/java/com/sillim/recordit/goal/domain/WeeklyGoal.java +++ b/src/main/java/com/sillim/recordit/goal/domain/WeeklyGoal.java @@ -119,6 +119,14 @@ public void modify( this.colorHex = new GoalColorHex(colorHex); } + public void linkRelatedMonthlyGoal(final MonthlyGoal relatedMonthlyGoal) { + this.relatedMonthlyGoal = relatedMonthlyGoal; + } + + public void unlinkRelatedMonthlyGoal() { + this.relatedMonthlyGoal = null; + } + public void remove() { this.deleted = true; } diff --git a/src/main/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateService.java b/src/main/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateService.java index b1f053ce..f7807b9e 100644 --- a/src/main/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateService.java +++ b/src/main/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateService.java @@ -1,5 +1,7 @@ package com.sillim.recordit.goal.service; +import com.sillim.recordit.global.exception.ErrorCode; +import com.sillim.recordit.global.exception.goal.InvalidWeeklyGoalException; import com.sillim.recordit.goal.domain.MonthlyGoal; import com.sillim.recordit.goal.domain.WeeklyGoal; import com.sillim.recordit.goal.dto.request.WeeklyGoalUpdateRequest; @@ -78,4 +80,24 @@ public void remove(final Long weeklyGoalId, final Long memberId) { weeklyGoalQueryService.searchByIdAndCheckAuthority(weeklyGoalId, memberId); weeklyGoal.remove(); } + + public void linkRelatedMonthlyGoal( + final Long weeklyGoalId, final Long monthlyGoalId, final Long memberId) { + + WeeklyGoal weeklyGoal = + weeklyGoalQueryService.searchByIdAndCheckAuthority(weeklyGoalId, memberId); + MonthlyGoal monthlyGoal = + monthlyGoalQueryService.searchByIdAndCheckAuthority(monthlyGoalId, memberId); + weeklyGoal.linkRelatedMonthlyGoal(monthlyGoal); + } + + public void unlinkRelatedMonthlyGoal(final Long weeklyGoalId, final Long memberId) { + + WeeklyGoal weeklyGoal = + weeklyGoalQueryService.searchByIdAndCheckAuthority(weeklyGoalId, memberId); + if (weeklyGoal.getRelatedMonthlyGoal().isEmpty()) { + throw new InvalidWeeklyGoalException(ErrorCode.RELATED_GOAL_NOT_FOUND); + } + weeklyGoal.unlinkRelatedMonthlyGoal(); + } } diff --git a/src/test/java/com/sillim/recordit/goal/controller/MonthlyGoalControllerTest.java b/src/test/java/com/sillim/recordit/goal/controller/MonthlyGoalControllerTest.java index dd92daf2..8a1f3c3d 100644 --- a/src/test/java/com/sillim/recordit/goal/controller/MonthlyGoalControllerTest.java +++ b/src/test/java/com/sillim/recordit/goal/controller/MonthlyGoalControllerTest.java @@ -261,7 +261,7 @@ void monthlyGoalChangeAchieveStatusTest() throws Exception { ResultActions perform = mockMvc.perform( - patch("/api/v1/goals/months/{id}", 1L) + patch("/api/v1/goals/months/{id}/achieve", 1L) .headers(authorizationHeader()) .queryParam("status", "true")); @@ -291,7 +291,7 @@ void monthlyGoalChangeAchieveStatusNotFoundTest() throws Exception { ResultActions perform = mockMvc.perform( - patch("/api/v1/goals/months/{id}", 1L) + patch("/api/v1/goals/months/{id}/achieve", 1L) .headers(authorizationHeader()) .queryParam("status", "true")); diff --git a/src/test/java/com/sillim/recordit/goal/controller/WeeklyGoalControllerTest.java b/src/test/java/com/sillim/recordit/goal/controller/WeeklyGoalControllerTest.java index 5321b5b6..9e1792df 100644 --- a/src/test/java/com/sillim/recordit/goal/controller/WeeklyGoalControllerTest.java +++ b/src/test/java/com/sillim/recordit/goal/controller/WeeklyGoalControllerTest.java @@ -248,7 +248,7 @@ void weeklyGoalChangeAchieveStatus() throws Exception { ResultActions perform = mockMvc.perform( - patch("/api/v1/goals/weeks/{id}", 1L) + patch("/api/v1/goals/weeks/{id}/achieve", 1L) .headers(authorizationHeader()) .queryParam("status", "true")); @@ -288,4 +288,53 @@ void removeWeeklyGoal() throws Exception { pathParameters( parameterWithName("id").description("삭제할 주 목표 id")))); } + + @Test + @DisplayName("id에 해당하는 주 목표를 월 목표와 연결한다.") + void linkRelatedGoal() throws Exception { + + ResultActions perform = + mockMvc.perform( + patch("/api/v1/goals/weeks/{id}/link", 1L) + .headers(authorizationHeader()) + .queryParam("relatedGoalId", "1")); + + perform.andExpect(status().isNoContent()); + + perform.andDo(print()) + .andDo( + document( + "weekly-goal-link-related-goal", + getDocumentRequest(), + getDocumentResponse(), + requestHeaders(authorizationDesc()), + pathParameters( + parameterWithName("id").description("연관 목표를 연결할 주 목표 id")), + queryParameters( + parameterWithName("relatedGoalId") + .description("주 목표와 연결할 월 목표 id")))); + } + + @Test + @DisplayName("id에 해당하는 주 목표와 연관 목표의 연결을 해제할 수 있다.") + void unlinkRelatedGoal() throws Exception { + + ResultActions perform = + mockMvc.perform( + patch("/api/v1/goals/weeks/{id}/unlink", 1L) + .headers(authorizationHeader())); + + perform.andExpect(status().isNoContent()); + + perform.andDo(print()) + .andDo( + document( + "weekly-goal-unlink-related-goal", + getDocumentRequest(), + getDocumentResponse(), + requestHeaders(authorizationDesc()), + pathParameters( + parameterWithName("id") + .description("연관 목표를 연결 해제할 주 목표 id")))); + } } diff --git a/src/test/java/com/sillim/recordit/goal/domain/WeeklyGoalTest.java b/src/test/java/com/sillim/recordit/goal/domain/WeeklyGoalTest.java index d2fb1378..e5d804f8 100644 --- a/src/test/java/com/sillim/recordit/goal/domain/WeeklyGoalTest.java +++ b/src/test/java/com/sillim/recordit/goal/domain/WeeklyGoalTest.java @@ -9,6 +9,7 @@ import com.sillim.recordit.global.exception.ErrorCode; import com.sillim.recordit.global.exception.common.InvalidRequestException; +import com.sillim.recordit.goal.fixture.MonthlyGoalFixture; import com.sillim.recordit.goal.fixture.WeeklyGoalFixture; import com.sillim.recordit.member.domain.Member; import com.sillim.recordit.member.fixture.MemberFixture; @@ -94,4 +95,31 @@ void remove() { assertThat(weeklyGoal.isDeleted()).isTrue(); } + + @Test + @DisplayName("주 목표와 월 목표를 연결할 수 있다.") + void linkRelatedMonthlyGoal() { + + WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); + MonthlyGoal relatedMonthlyGoal = MonthlyGoalFixture.DEFAULT.getWithMember(member); + weeklyGoal.linkRelatedMonthlyGoal(relatedMonthlyGoal); + + assertThat(weeklyGoal.getRelatedMonthlyGoal()).isNotEmpty(); + assertThat(weeklyGoal.getRelatedMonthlyGoal().get()) + .usingRecursiveComparison() + .isEqualTo(relatedMonthlyGoal); + } + + @Test + @DisplayName("주 목표의 연관 목표를 연결 해제 할 수 있다.") + void unlinkRelatedMonthlyGoal() { + + WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); + MonthlyGoal relatedMonthlyGoal = MonthlyGoalFixture.DEFAULT.getWithMember(member); + weeklyGoal.linkRelatedMonthlyGoal(relatedMonthlyGoal); + + weeklyGoal.unlinkRelatedMonthlyGoal(); + + assertThat(weeklyGoal.getRelatedMonthlyGoal()).isEmpty(); + } } diff --git a/src/test/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateServiceTest.java b/src/test/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateServiceTest.java index 7a9f2efe..ad5046e1 100644 --- a/src/test/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateServiceTest.java +++ b/src/test/java/com/sillim/recordit/goal/service/WeeklyGoalUpdateServiceTest.java @@ -1,6 +1,7 @@ package com.sillim.recordit.goal.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -9,9 +10,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; +import com.sillim.recordit.global.exception.ErrorCode; +import com.sillim.recordit.global.exception.goal.InvalidWeeklyGoalException; import com.sillim.recordit.goal.domain.MonthlyGoal; import com.sillim.recordit.goal.domain.WeeklyGoal; import com.sillim.recordit.goal.dto.request.WeeklyGoalUpdateRequest; +import com.sillim.recordit.goal.fixture.MonthlyGoalFixture; import com.sillim.recordit.goal.fixture.WeeklyGoalFixture; import com.sillim.recordit.goal.repository.WeeklyGoalRepository; import com.sillim.recordit.member.domain.Member; @@ -168,13 +172,13 @@ void modifyWeeklyGoalWithRelatedMonthlyGoal() { @DisplayName("id에 해당하는 주 목표의 달성 상태를 변경한다.") void changeAchieveStatus() { Long memberId = 1L; - Long monthlyGoalId = 2L; + Long weeklyGoalId = 2L; Boolean status = true; WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); - given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(monthlyGoalId), eq(memberId))) + given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(weeklyGoalId), eq(memberId))) .willReturn(weeklyGoal); - weeklyGoalUpdateService.changeAchieveStatus(monthlyGoalId, status, memberId); + weeklyGoalUpdateService.changeAchieveStatus(weeklyGoalId, status, memberId); assertThat(weeklyGoal.isAchieved()).isTrue(); } @@ -183,13 +187,67 @@ void changeAchieveStatus() { @DisplayName("id에 해당하는 주 목표를 삭제한다.") void removeTest() { Long memberId = 1L; - Long monthlyGoalId = 2L; + Long weeklyGoalId = 2L; WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); - given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(monthlyGoalId), eq(memberId))) + given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(weeklyGoalId), eq(memberId))) .willReturn(weeklyGoal); - weeklyGoalUpdateService.remove(monthlyGoalId, memberId); + weeklyGoalUpdateService.remove(weeklyGoalId, memberId); assertThat(weeklyGoal.isDeleted()).isTrue(); } + + @Test + @DisplayName("id에 해당하는 주 목표를 월 목표와 연결한다.") + void linkRelatedMonthlyGoal() { + Long memberId = 1L; + Long weeklyGoalId = 2L; + Long relatedGoalId = 3L; + WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); + given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(weeklyGoalId), eq(memberId))) + .willReturn(weeklyGoal); + MonthlyGoal relatedMonthlyGoal = MonthlyGoalFixture.DEFAULT.getWithMember(member); + given(monthlyGoalQueryService.searchByIdAndCheckAuthority(eq(relatedGoalId), eq(memberId))) + .willReturn(relatedMonthlyGoal); + + weeklyGoalUpdateService.linkRelatedMonthlyGoal(weeklyGoalId, relatedGoalId, memberId); + + assertThat(weeklyGoal.getRelatedMonthlyGoal()).isNotEmpty(); + assertThat(weeklyGoal.getRelatedMonthlyGoal().get()) + .usingRecursiveComparison() + .isEqualTo(relatedMonthlyGoal); + } + + @Test + @DisplayName("id에 해당하는 주 목표를 월 목표와 연결한다.") + void unlinkRelatedMonthlyGoal() { + Long memberId = 1L; + Long weeklyGoalId = 2L; + WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); + weeklyGoal.linkRelatedMonthlyGoal(MonthlyGoalFixture.DEFAULT.getWithMember(member)); + given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(weeklyGoalId), eq(memberId))) + .willReturn(weeklyGoal); + + weeklyGoalUpdateService.unlinkRelatedMonthlyGoal(weeklyGoalId, memberId); + + assertThat(weeklyGoal.getRelatedMonthlyGoal()).isEmpty(); + } + + @Test + @DisplayName("주 목표의 연관 목표가 없는 상태에서 연결 해제를 시도할 경우, InvalidWeeklyGoalException이 발생한다.") + void unlinkRelatedMonthlyGoalThrowsInvalidWeeklyGoalExceptionIfRelatedGoalIsNotExists() { + Long memberId = 1L; + Long weeklyGoalId = 2L; + WeeklyGoal weeklyGoal = WeeklyGoalFixture.DEFAULT.getWithMember(member); + given(weeklyGoalQueryService.searchByIdAndCheckAuthority(eq(weeklyGoalId), eq(memberId))) + .willReturn(weeklyGoal); + + assertThatCode( + () -> { + weeklyGoalUpdateService.unlinkRelatedMonthlyGoal( + weeklyGoalId, memberId); + }) + .isInstanceOf(InvalidWeeklyGoalException.class) + .hasMessage(ErrorCode.RELATED_GOAL_NOT_FOUND.getDescription()); + } }