Skip to content

Commit

Permalink
fix: ✏️ 사용자 정의 지출 카테고리 아이콘 반영 (#107)
Browse files Browse the repository at this point in the history
* rename: 커스텀 카테고리 상수 0->custom, 12->other 수정

* style: spending key 상수화

* docs: spending category api 예시 요청 파라미터 other 추가

* fix: spending_controller ...아이콘 validation 체크

* fix: spending_category_dto validation 추가

* fix: spending entity 생성자 validation 추가

* fix: spending_category_dto validation 추가

* test: spending category 등록 시 other -> custom 거부 테스트로 수정

* test: 지출 내역 등록 시 카테고리 validation 체크 수정

* fix: 지출 등록 시 category validation 체크 조건식 수정

* docs: spending api 스웨거 문서 수정
  • Loading branch information
psychology50 authored Jun 5, 2024
1 parent 772b23f commit 07c6057
Show file tree
Hide file tree
Showing 13 changed files with 59 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
public interface SpendingApi {
@Operation(summary = "지출 내역 추가", method = "POST", description = """
사용자의 지출 내역을 추가하고 추가된 지출 내역을 반환합니다. <br/>
서비스에서 제공하는 지출 카테고리를 사용하는 경우 categoryId는 -1이어야 하며, icon은 OTHER가 될 수 없습니다. <br/>
사용자가 정의한 지출 카테고리를 사용하는 경우 categoryId는 -1이 아니어야 하며, icon은 OTHER여야 합니다.
서비스에서 제공하는 지출 카테고리를 사용하는 경우 categoryId는 -1이어야 하며, icon은 CUSTOM 혹은 OTHER이 될 수 없습니다. <br/>
사용자가 정의한 지출 카테고리를 사용하는 경우 categoryId는 -1이 아니어야 하며, icon은 CUSTOM이여야 합니다.
""")
@ApiResponses({
@ApiResponse(responseCode = "200", content = @Content(schemaProperties = @SchemaProperty(name = "spending", schema = @Schema(implementation = SpendingSearchRes.Individual.class)))),
@ApiResponse(responseCode = "400", description = "지출 카테고리 ID와 아이콘의 조합이 올바르지 않습니다.", content = @Content(examples = {
@ExampleObject(name = "카테고리 id, 아이콘 조합 오류", description = "categoryId가 -1인데 icon이 OTHER이거나, categoryId가 -1이 아닌데 icon이 OTHER가 아닙니다.",
@ExampleObject(name = "카테고리 id, 아이콘 조합 오류", description = "categoryId가 -1인데 icon이 CUSTOM/OTHER이거나, categoryId가 -1이 아닌데 icon이 CUSTOM이 아닙니다.",
value = """
{
"code": "4005",
Expand Down Expand Up @@ -85,7 +85,7 @@ public interface SpendingApi {
""")
@ApiResponse(responseCode = "200", content = @Content(schemaProperties = @SchemaProperty(name = "spending", schema = @Schema(implementation = SpendingSearchRes.Individual.class))))
ResponseEntity<?> updateSpending(@PathVariable Long spendingId, @RequestBody @Validated SpendingReq request, @AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "지출 내역 삭제", method = "DELETE", description = "지출 내역의 ID값으로 해당 지출 내역을 삭제 합니다.")
@Parameter(name = "spendingId", description = "지출 내역 ID", example = "1", required = true, in = ParameterIn.PATH)
@ApiResponse(responseCode = "403", description = "지출 카테고리에 대한 권한이 없습니다.", content = @Content(examples = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public interface SpendingCategoryApi {
@ExampleObject(name = "식사", value = "FOOD"), @ExampleObject(name = "교통", value = "TRANSPORTATION"), @ExampleObject(name = "뷰티/패션", value = "BEAUTY_OR_FASHION"),
@ExampleObject(name = "편의점/마트", value = "CONVENIENCE_STORE"), @ExampleObject(name = "교육", value = "EDUCATION"), @ExampleObject(name = "생활", value = "LIVING"),
@ExampleObject(name = "건강", value = "HEALTH"), @ExampleObject(name = "취미/여가", value = "HOBBY"), @ExampleObject(name = "여행/숙박", value = "TRAVEL"),
@ExampleObject(name = "술/유흥", value = "ALCOHOL_OR_ENTERTAINMENT"), @ExampleObject(name = "회비/경조사", value = "MEMBERSHIP_OR_FAMILY_EVENT")
@ExampleObject(name = "술/유흥", value = "ALCOHOL_OR_ENTERTAINMENT"), @ExampleObject(name = "회비/경조사", value = "MEMBERSHIP_OR_FAMILY_EVENT"), @ExampleObject(name = "기타", value = "OTHER")
}),
@Parameter(name = "param", hidden = true)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class SpendingCategoryController implements SpendingCategoryApi {
@PostMapping("")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> postSpendingCategory(@Validated SpendingCategoryDto.CreateParamReq param, @AuthenticationPrincipal SecurityUserDetails user) {
if (param.icon().equals(SpendingCategory.OTHER)) {
if (param.icon().equals(SpendingCategory.CUSTOM)) {
throw new SpendingErrorException(SpendingErrorCode.INVALID_ICON);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
@RequiredArgsConstructor
@RequestMapping("/v2/spendings")
public class SpendingController implements SpendingApi {
private static final String SPENDING = "spending";

private final SpendingUseCase spendingUseCase;

@Override
Expand All @@ -31,21 +33,21 @@ public ResponseEntity<?> postSpending(@RequestBody @Validated SpendingReq reques
throw new SpendingErrorException(SpendingErrorCode.INVALID_ICON_WITH_CATEGORY_ID);
}

return ResponseEntity.ok(SuccessResponse.from("spending", spendingUseCase.createSpending(user.getUserId(), request)));
return ResponseEntity.ok(SuccessResponse.from(SPENDING, spendingUseCase.createSpending(user.getUserId(), request)));
}

@Override
@GetMapping("")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getSpendingListAtYearAndMonth(@RequestParam("year") int year, @RequestParam("month") int month, @AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from("spending", spendingUseCase.getSpendingsAtYearAndMonth(user.getUserId(), year, month)));
return ResponseEntity.ok(SuccessResponse.from(SPENDING, spendingUseCase.getSpendingsAtYearAndMonth(user.getUserId(), year, month)));
}

@Override
@GetMapping("/{spendingId}")
@PreAuthorize("isAuthenticated() and @spendingManager.hasPermission(#user.getUserId(), #spendingId)")
public ResponseEntity<?> getSpendingDetail(@PathVariable Long spendingId, @AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from("spending", spendingUseCase.getSpedingDetail(spendingId)));
return ResponseEntity.ok(SuccessResponse.from(SPENDING, spendingUseCase.getSpedingDetail(spendingId)));
}

@Override
Expand All @@ -56,7 +58,7 @@ public ResponseEntity<?> updateSpending(@PathVariable Long spendingId, @RequestB
throw new SpendingErrorException(SpendingErrorCode.INVALID_ICON_WITH_CATEGORY_ID);
}

return ResponseEntity.ok(SuccessResponse.from("spending", spendingUseCase.updateSpending(spendingId, request)));
return ResponseEntity.ok(SuccessResponse.from(SPENDING, spendingUseCase.updateSpending(spendingId, request)));
}

@Override
Expand All @@ -69,13 +71,13 @@ public ResponseEntity<?> deleteSpending(@PathVariable Long spendingId, @Authenti
}

/**
* categoryId가 -1이면 서비스에서 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 OTHER가 될 수 없고, <br/>
* categoryId가 -1이 아니면 사용자가 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 OTHER임을 확인한다.
* categoryId가 -1이면 서비스에서 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 CUSTOM이나 OTHER이 될 수 없고, <br/>
* categoryId가 -1이 아니면 사용자가 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 CUSTOM임을 확인한다.
*
* @param categoryId : 사용자가 정의한 카테고리 ID
* @param icon : 지출 내역으로 저장하려는 카테고리의 아이콘
*/
private boolean isValidCategoryIdAndIcon(Long categoryId, SpendingCategory icon) {
return (categoryId.equals(-1L) && !icon.equals(SpendingCategory.OTHER) || categoryId > 0 && icon.equals(SpendingCategory.OTHER));
return (categoryId.equals(-1L) && (!icon.equals(SpendingCategory.CUSTOM) && !icon.equals(SpendingCategory.OTHER))) || (categoryId > 0 && icon.equals(SpendingCategory.CUSTOM));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ public record Res(
throw new IllegalArgumentException("isCustom과 id 정보가 일치하지 않습니다.");
}

if (isCustom && icon.equals(SpendingCategory.OTHER)) {
throw new IllegalArgumentException("사용자 정의 카테고리는 OTHER가 될 수 없습니다.");
if (isCustom && icon.equals(SpendingCategory.CUSTOM)) {
throw new IllegalArgumentException("사용자 정의 카테고리는 CUSTOM이 될 수 없습니다.");
}

if (!isCustom && (icon.equals(SpendingCategory.CUSTOM) || icon.equals(SpendingCategory.OTHER))) {
throw new IllegalArgumentException("서비스에서 제공하는 카테고리는 CUSTOM 혹은 OTHER이 될 수 없습니다.");
}

if (!StringUtils.hasText(name)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ void postSpendingCategoryWithInvalidIcon() throws Exception {
}

@Test
@DisplayName("OTHER 아이콘을 입력하면 400 BAD_REQUEST 에러 응답을 반환한다.")
@DisplayName("CUSTOM 아이콘을 입력하면 400 BAD_REQUEST 에러 응답을 반환한다.")
@WithSecurityMockUser
void postSpendingCategoryWithOtherIcon() throws Exception {
// given
String name = "식비";
String icon = "OTHER";
String icon = SpendingCategory.CUSTOM.name();

// when
ResultActions result = performPostSpendingCategory(name, icon);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,26 @@ void whenAmountIsZeroOrNegative() throws Exception {
}

@Test
@DisplayName("아이콘이 OTHER이면서 categoryId가 -1인 경우 400 Bad Request를 반환한다.")
@DisplayName("아이콘이 CUSTOM이면서 categoryId가 -1인 경우 400 Bad Request를 반환한다.")
@WithSecurityMockUser
void whenCategoryIsNotDefined() throws Exception {
// given
Long categoryId = -1L;
SpendingCategory icon = SpendingCategory.CUSTOM;
SpendingReq request = new SpendingReq(10000, categoryId, icon, LocalDate.now(), "소비처", "메모");
given(spendingUseCase.createSpending(1L, request)).willReturn(SpendingSearchRes.Individual.builder().build());

// when
ResultActions result = performPostSpending(request);

// then
result.andDo(print()).andExpect(status().isBadRequest());
}

@Test
@DisplayName("아이콘이 OTHER이면서 categoryId가 -1인 경우 400 Bad Request를 반환한다.")
@WithSecurityMockUser
void whenCategoryIsInvalidIcon() throws Exception {
// given
Long categoryId = -1L;
SpendingCategory icon = SpendingCategory.OTHER;
Expand All @@ -88,6 +105,7 @@ void whenCategoryIsNotDefined() throws Exception {
result.andDo(print()).andExpect(status().isBadRequest());
}


@Test
@DisplayName("지출일이 현재보다 미래인 경우 422 Unprocessable Entity를 반환한다.")
@WithSecurityMockUser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void createSpendingWithCustomCategorySuccess() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
SpendingCustomCategory category = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategory.of("잉여비", SpendingCategory.LIVING, user));
SpendingReq request = new SpendingReq(10000, category.getId(), SpendingCategory.OTHER, LocalDate.now(), "소비처", "메모");
SpendingReq request = new SpendingReq(10000, category.getId(), SpendingCategory.CUSTOM, LocalDate.now(), "소비처", "메모");

// when
ResultActions result = performCreateSpendingSuccess(request, user);
Expand All @@ -103,7 +103,7 @@ void createSpendingWithCustomCategorySuccess() throws Exception {
void createSpendingWithInvalidCustomCategory() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
SpendingReq request = new SpendingReq(10000, 1000L, SpendingCategory.OTHER, LocalDate.now(), "소비처", "메모");
SpendingReq request = new SpendingReq(10000, 1000L, SpendingCategory.CUSTOM, LocalDate.now(), "소비처", "메모");

// when
ResultActions result = performCreateSpendingSuccess(request, user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void setUp() {
spendingUpdateService = new SpendingUpdateService(spendingCustomCategoryService);

request = new SpendingReq(10000, -1L, SpendingCategory.FOOD, LocalDate.now(), "소비처", "메모");
requestWithCustomCategory = new SpendingReq(10000, 1L, SpendingCategory.OTHER, LocalDate.now(), "소비처", "메모");
requestWithCustomCategory = new SpendingReq(10000, 1L, SpendingCategory.CUSTOM, LocalDate.now(), "소비처", "메모");

user = UserFixture.GENERAL_USER.toUser();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ public class Spending extends DateAuditable {

@Builder
private Spending(Integer amount, SpendingCategory category, LocalDateTime spendAt, String accountName, String memo, User user, SpendingCustomCategory spendingCustomCategory) {
if (category.equals(SpendingCategory.OTHER) && spendingCustomCategory == null) {
throw new IllegalArgumentException("OTHER 아이콘의 경우 SpendingCustomCategory는 null일 수 없습니다.");
} else if (!category.equals(SpendingCategory.OTHER) && spendingCustomCategory != null) {
throw new IllegalArgumentException("OTHER 아이콘이 아닌 경우 SpendingCustomCategory는 null이어야 합니다.");
if (spendingCustomCategory == null && (category.equals(SpendingCategory.CUSTOM) || category.equals(SpendingCategory.OTHER))) {
throw new IllegalArgumentException("서비스 제공 아이콘을 등록할 때는 CUSTOM, OHTER 아이콘을 사용할 수 없습니다.");
} else if (spendingCustomCategory != null && !category.equals(SpendingCategory.CUSTOM)) {
throw new IllegalArgumentException("사용자 정의 아이콘을 등록할 때는 CUSTOM 아이콘이어야 합니다.");
}

this.amount = amount;
Expand All @@ -71,7 +71,7 @@ public int getDay() {
* @return {@link CategoryInfo}
*/
public CategoryInfo getCategory() {
if (this.category.equals(SpendingCategory.OTHER)) {
if (this.category.equals(SpendingCategory.CUSTOM)) {
SpendingCustomCategory category = getSpendingCustomCategory();
return CategoryInfo.of(category.getId(), category.getName(), category.getIcon());
}
Expand All @@ -80,10 +80,10 @@ public CategoryInfo getCategory() {
}

public void updateSpendingCustomCategory(SpendingCustomCategory spendingCustomCategory) {
if (this.category.equals(SpendingCategory.OTHER) && spendingCustomCategory == null) {
throw new IllegalArgumentException("OTHER 아이콘의 경우 SpendingCustomCategory는 null일 수 없습니다.");
} else if (!this.category.equals(SpendingCategory.OTHER) && spendingCustomCategory != null) {
throw new IllegalArgumentException("OTHER 아이콘이 아닌 경우 SpendingCustomCategory는 null이어야 합니다.");
if (spendingCustomCategory == null && (category.equals(SpendingCategory.CUSTOM) || category.equals(SpendingCategory.OTHER))) {
throw new IllegalArgumentException("서비스 제공 아이콘을 등록할 때는 CUSTOM, OHTER 아이콘을 사용할 수 없습니다.");
} else if (spendingCustomCategory != null && !category.equals(SpendingCategory.CUSTOM)) {
throw new IllegalArgumentException("사용자 정의 아이콘을 등록할 때는 CUSTOM 아이콘이어야 합니다.");
}

this.spendingCustomCategory = spendingCustomCategory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class SpendingCustomCategory extends DateAuditable {
private User user;

private SpendingCustomCategory(String name, SpendingCategory icon, User user) {
if (icon.equals(SpendingCategory.OTHER)) {
if (icon.equals(SpendingCategory.CUSTOM)) {
throw new IllegalArgumentException("OTHER 아이콘은 커스텀 카테고리의 icon으로 사용할 수 없습니다.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public record CategoryInfo(
throw new IllegalArgumentException("isCustom이 " + isCustom + "일 때 id는 " + (isCustom ? "0 이상" : "-1") + "이어야 합니다.");
}

if (isCustom && icon.equals(SpendingCategory.OTHER)) {
if (isCustom && icon.equals(SpendingCategory.CUSTOM)) {
throw new IllegalArgumentException("사용자 정의 카테고리는 OTHER가 될 수 없습니다.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@Getter
@RequiredArgsConstructor
public enum SpendingCategory implements LegacyCommonType {
OTHER("0", "사용자 정의"),
CUSTOM("0", "사용자 정의"),
FOOD("1", "식비"),
TRANSPORTATION("2", "교통비"),
BEAUTY_OR_FASHION("3", "뷰티/패션"),
Expand All @@ -18,7 +18,8 @@ public enum SpendingCategory implements LegacyCommonType {
HOBBY("8", "취미/여가"),
TRAVEL("9", "여행/숙박"),
ALCOHOL_OR_ENTERTAINMENT("10", "술/유흥"),
MEMBERSHIP_OR_FAMILY_EVENT("11", "회비/경조사");
MEMBERSHIP_OR_FAMILY_EVENT("11", "회비/경조사"),
OTHER("12", "기타");

private final String code;
private final String type;
Expand Down

0 comments on commit 07c6057

Please sign in to comment.