diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index 1b4b8f5..7b91eb4 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -31,7 +31,7 @@ services: APPLE_TEAM_ID: ${APPLE_TEAM_ID} APPLE_KEY_ID: ${APPLE_KEY_ID} APPLE_CLIENT_ID: ${APPLE_CLIENT_ID} - + KAKAO_ADMIN_KEY: ${KAKAO_ADMIN_KEY} redis: container_name: moneymong-redis image: redis diff --git a/src/main/java/com/moneymong/domain/agency/entity/Agency.java b/src/main/java/com/moneymong/domain/agency/entity/Agency.java index 7b91201..60bc072 100644 --- a/src/main/java/com/moneymong/domain/agency/entity/Agency.java +++ b/src/main/java/com/moneymong/domain/agency/entity/Agency.java @@ -43,7 +43,7 @@ public class Agency extends BaseEntity { @Enumerated(EnumType.STRING) private AgencyType agencyType; - @OneToMany(mappedBy = "agency", cascade = CascadeType.PERSIST) + @OneToMany(mappedBy = "agency", cascade = CascadeType.ALL) private List agencyUsers = new ArrayList<>(); @Column( diff --git a/src/main/java/com/moneymong/domain/agency/service/AgencyUserService.java b/src/main/java/com/moneymong/domain/agency/service/AgencyUserService.java index 7412878..9b91aa2 100644 --- a/src/main/java/com/moneymong/domain/agency/service/AgencyUserService.java +++ b/src/main/java/com/moneymong/domain/agency/service/AgencyUserService.java @@ -47,12 +47,10 @@ public void blockUser(Long userId, Long agencyId, BlockAgencyUserRequest request validateStaffUserRole(staffUser); AgencyUser targetUser = getAgencyUser(request.getUserId(), agencyId); - - targetUser.updateAgencyUserRole(BLOCKED); - InvitationCodeCertification certification = getCertification(agencyId, request); - - certification.revoke(); + + agencyUserRepository.delete(targetUser); + invitationCodeCertificationRepository.delete(certification); Agency agency = getAgency(agencyId); diff --git a/src/main/java/com/moneymong/domain/ledger/api/LedgerControllerV2.java b/src/main/java/com/moneymong/domain/ledger/api/LedgerControllerV2.java index c804513..c813185 100644 --- a/src/main/java/com/moneymong/domain/ledger/api/LedgerControllerV2.java +++ b/src/main/java/com/moneymong/domain/ledger/api/LedgerControllerV2.java @@ -1,7 +1,9 @@ package com.moneymong.domain.ledger.api; import com.moneymong.domain.ledger.api.request.*; +import com.moneymong.domain.ledger.api.response.LedgerDetailInfoView; import com.moneymong.domain.ledger.api.response.ledger.LedgerInfoView; +import com.moneymong.domain.ledger.service.manager.LedgerDetailService; import com.moneymong.domain.ledger.service.reader.LedgerReader; import com.moneymong.global.security.token.dto.jwt.JwtAuthentication; import io.swagger.v3.oas.annotations.Operation; @@ -18,6 +20,7 @@ @RequiredArgsConstructor public class LedgerControllerV2 { private final LedgerReader ledgerReader; + private final LedgerDetailService ledgerDetailService; @Operation(summary = " 장부 내역 조회 API") @GetMapping("/{id}") @@ -57,4 +60,18 @@ public LedgerInfoView searchByFilter( searchLedgerFilterRequest.getFundType() ); } + + @Operation(summary = "장부 상세 내역 수정 API") + @PutMapping("/ledger-detail/{detailId}") + public LedgerDetailInfoView updateLedger( + @AuthenticationPrincipal JwtAuthentication user, + @PathVariable("detailId") final Long ledgerDetailId, + @RequestBody @Valid final UpdateLedgerRequestV2 updateLedgerRequest + ) { + return ledgerDetailService.updateLedgerDetailV2( + user.getId(), + ledgerDetailId, + updateLedgerRequest + ); + } } diff --git a/src/main/java/com/moneymong/domain/ledger/api/request/UpdateLedgerRequestV2.java b/src/main/java/com/moneymong/domain/ledger/api/request/UpdateLedgerRequestV2.java new file mode 100644 index 0000000..a283241 --- /dev/null +++ b/src/main/java/com/moneymong/domain/ledger/api/request/UpdateLedgerRequestV2.java @@ -0,0 +1,33 @@ +package com.moneymong.domain.ledger.api.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +public class UpdateLedgerRequestV2 { + @NotBlank(message = "storeInfo를 입력해주세요.") + @Size(min = 1, max = 20, message = "storeInfo 1자 - 20자 입력해주세요.") + private String storeInfo; + + @NotNull(message = "amount를 입력해주세요.") + private Integer amount; + + @NotBlank(message = "description를 입력해주세요.") + private String description; + + @NotNull(message = "paymentDate를 입력해주세요.") + private ZonedDateTime paymentDate; + + @Size(max = 12, message = "영수증 12개 이하 입력해주세요.") + private List receiptImageUrls; + + @Size(max = 12, message = "증빙 자료 12개 이하 입력해주세요.") + private List documentImageUrls; +} diff --git a/src/main/java/com/moneymong/domain/ledger/entity/Ledger.java b/src/main/java/com/moneymong/domain/ledger/entity/Ledger.java index 968c3f8..f94416b 100644 --- a/src/main/java/com/moneymong/domain/ledger/entity/Ledger.java +++ b/src/main/java/com/moneymong/domain/ledger/entity/Ledger.java @@ -47,15 +47,23 @@ public void updateTotalBalance(int newAmount) { .add(new BigDecimal(newAmount)); // 장부 금액 최소 초과 검증 + validateTotalBalance(expectedAmount); + + this.totalBalance = expectedAmount.intValue(); + } + + private void validateTotalBalance(BigDecimal expectedAmount) { BigDecimal minValue = new BigDecimal("-999999999"); BigDecimal maxValue = new BigDecimal("999999999"); - if (!(expectedAmount.compareTo(minValue) >= 0 && - expectedAmount.compareTo(maxValue) <= 0) - ) { - throw new BadRequestException(ErrorCode.INVALID_LEDGER_AMOUNT); + + if (expectedAmount.compareTo(minValue) < 0) { + throw new BadRequestException(ErrorCode.LEDGER_AMOUNT_UNDERFLOW); } - this.totalBalance = expectedAmount.intValue(); + if (expectedAmount.compareTo(maxValue) > 0) { + throw new BadRequestException(ErrorCode.LEDGER_AMOUNT_OVERFLOW); + + } } public static Ledger of( diff --git a/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDetailService.java b/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDetailService.java index 44ef8b4..d4999ea 100644 --- a/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDetailService.java +++ b/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDetailService.java @@ -4,6 +4,7 @@ import com.moneymong.domain.agency.entity.enums.AgencyUserRole; import com.moneymong.domain.agency.repository.AgencyUserRepository; import com.moneymong.domain.ledger.api.request.UpdateLedgerRequest; +import com.moneymong.domain.ledger.api.request.UpdateLedgerRequestV2; import com.moneymong.domain.ledger.api.response.LedgerDetailInfoView; import com.moneymong.domain.ledger.entity.Ledger; import com.moneymong.domain.ledger.entity.LedgerDetail; @@ -13,7 +14,6 @@ import com.moneymong.domain.ledger.repository.LedgerDetailRepository; import com.moneymong.domain.ledger.repository.LedgerDocumentRepository; import com.moneymong.domain.ledger.repository.LedgerReceiptRepository; -import com.moneymong.domain.ledger.repository.LedgerRepository; import com.moneymong.domain.ledger.service.mapper.LedgerAssembler; import com.moneymong.domain.ledger.service.reader.LedgerDocumentReader; import com.moneymong.domain.ledger.service.reader.LedgerReceiptReader; @@ -38,7 +38,6 @@ @Slf4j @RequiredArgsConstructor public class LedgerDetailService { - private final LedgerRepository ledgerRepository; private final LedgerAssembler ledgerAssembler; private final LedgerReceiptReader ledgerReceiptReader; private final LedgerDocumentReader ledgerDocumentReader; @@ -47,6 +46,8 @@ public class LedgerDetailService { private final LedgerDetailRepository ledgerDetailRepository; private final LedgerReceiptRepository ledgerReceiptRepository; private final LedgerDocumentRepository ledgerDocumentRepository; + private final LedgerReceiptManager ledgerReceiptManager; + private final LedgerDocumentManager ledgerDocumentManager; @Transactional public LedgerDetail createLedgerDetail( @@ -124,6 +125,57 @@ public LedgerDetailInfoView updateLedgerDetail( ); } + @Transactional + public LedgerDetailInfoView updateLedgerDetailV2( + Long userId, + Long ledgerDetailId, + UpdateLedgerRequestV2 updateLedgerRequest + ) { + + User user = getUser(userId); + + LedgerDetail ledgerDetail = getLedgerDetail(ledgerDetailId); + + Ledger ledger = ledgerDetail.getLedger(); + + AgencyUser agencyUser = getAgencyUser(user, ledgerDetail); + + validateStaffUserRole(agencyUser.getAgencyUserRole()); + + int newAmount = AmountCalculatorByFundType.calculate( + ledgerDetail.getFundType(), + ledgerDetail.getAmount() - updateLedgerRequest.getAmount() + ); + + ledger.updateTotalBalance(-newAmount); + + ledgerDetail.updateLedgerDetailInfo( + updateLedgerRequest.getStoreInfo(), + updateLedgerRequest.getAmount(), + updateLedgerRequest.getDescription(), + updateLedgerRequest.getPaymentDate() + ); + + updateBalance(ledger); + + ledgerReceiptRepository.deleteByLedgerDetail(ledgerDetail); + ledgerDocumentRepository.deleteByLedgerDetail(ledgerDetail); + + List receipts = ledgerReceiptManager.createReceipts(ledgerDetailId, updateLedgerRequest.getReceiptImageUrls()); + List ledgerDocuments = ledgerDocumentManager.createLedgerDocuments(ledgerDetailId, updateLedgerRequest.getDocumentImageUrls()); + + ledgerReceiptRepository.saveAll(receipts); + ledgerDocumentRepository.saveAll(ledgerDocuments); + + return LedgerDetailInfoView.of( + ledgerDetail, + receipts, + ledgerDocuments, + user + ); + } + + @Transactional public void removeLedgerDetail( final Long userId, diff --git a/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDocumentManager.java b/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDocumentManager.java index 4161a7e..65884b1 100644 --- a/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDocumentManager.java +++ b/src/main/java/com/moneymong/domain/ledger/service/manager/LedgerDocumentManager.java @@ -72,7 +72,7 @@ public void removeLedgerDocuments( LedgerDocument ledgerDocument = ledgerDocumentRepository .findById(documentId) - .orElseThrow(() -> new NotFoundException(ErrorCode.LEDGER_DOCUUMENT_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(ErrorCode.LEDGER_DOCUMENT_NOT_FOUND)); ledgerDocumentRepository.delete(ledgerDocument); } diff --git a/src/main/java/com/moneymong/domain/user/service/UserService.java b/src/main/java/com/moneymong/domain/user/service/UserService.java index 425f9d8..959a8ba 100644 --- a/src/main/java/com/moneymong/domain/user/service/UserService.java +++ b/src/main/java/com/moneymong/domain/user/service/UserService.java @@ -49,7 +49,7 @@ public User save(User unsavedUser) { public User registerUser(OAuthUserInfo oauthUserInfo) { User newUser = User.of( oauthUserInfo.getEmail(), - oauthUserInfo.getNickname(), + oauthUserInfo.getNickname() == null ? "유저" : oauthUserInfo.getNickname(), oauthUserInfo.getProvider(), oauthUserInfo.getOauthId() ); diff --git a/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java b/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java index c8c3511..c9098b9 100644 --- a/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java +++ b/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java @@ -31,9 +31,10 @@ public enum ErrorCode { INVALID_LEDGER_ACCESS(MoneymongConstant.FORBIDDEN, "LEDGER-002", "유효하지 않은 접근입니다."), LEDGER_NOT_FOUND(MoneymongConstant.NOT_FOUND, "LEDGER-003", "장부가 존재하지 않습니다."), LEDGER_DETAIL_NOT_FOUND(MoneymongConstant.NOT_FOUND, "LEDGER-004", "장부 상세 내역이 존재하지 않습니다."), + LEDGER_AMOUNT_OVERFLOW(MoneymongConstant.BAD_REQUEST,"LEDGER-005", "총 잔액은 최대 999,999,999원까지 기록이 가능합니다."), LEDGER_RECEIPT_NOT_FOUND(MoneymongConstant.NOT_FOUND, "LEDGER-006", "장부 영수증 내역이 존재하지 않습니다."), - INVALID_LEDGER_AMOUNT(MoneymongConstant.BAD_REQUEST,"LEDGER-005", "0원 ~ 999,999,999원까지 기입 가능합니다."), - LEDGER_DOCUUMENT_NOT_FOUND(MoneymongConstant.NOT_FOUND, "LEDGER-007", "장부 증빙 자료 내역이 존재하지 않습니다."), + LEDGER_DOCUMENT_NOT_FOUND(MoneymongConstant.NOT_FOUND, "LEDGER-007", "장부 증빙 자료 내역이 존재하지 않습니다."), + LEDGER_AMOUNT_UNDERFLOW(MoneymongConstant.BAD_REQUEST,"LEDGER-008", "총 잔액은 최소 -999,999,999원까지 기록이 가능합니다."), // ---- 이미지 ---- // IMAGE_NOT_EXISTS(MoneymongConstant.NOT_FOUND, "IMAGE-001", "이미지를 찾을 수 없습니다."), diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java index a3e4720..33b13d5 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java @@ -100,7 +100,6 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { String refreshToken = userData.getRefreshToken(); String idToken = userData.getIdToken(); - log.info("[AppleService] refreshToken = {}", refreshToken); return decodePayload(idToken, request.getName(), refreshToken); } catch (RestClientException e) { log.warn("[AppleService] failed to get OAuth User Data = {}", request.getAccessToken()); @@ -178,7 +177,7 @@ private OAuthUserDataResponse decodePayload(String idToken, String nickname, Str Map claims = decoded.getClaims(); String providerUid = decoded.getSubject(); - String email = claims.get("sub").asString(); + String email = claims.get("email").asString(); return OAuthUserDataResponse.builder() .provider(getAuthProvider().toString()) diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java index e889907..9687b9e 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java @@ -1,5 +1,8 @@ package com.moneymong.global.security.oauth.handler; +import com.moneymong.domain.user.entity.User; +import com.moneymong.domain.user.repository.UserRepository; +import com.moneymong.global.exception.custom.NotFoundException; import com.moneymong.global.exception.enums.ErrorCode; import com.moneymong.global.security.oauth.dto.KakaoUserData; import com.moneymong.global.security.oauth.dto.OAuthUserDataRequest; @@ -11,6 +14,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; @@ -21,10 +26,14 @@ public class KakaoService implements OAuthAuthenticationHandler { private final RestTemplate restTemplate; + private final UserRepository userRepository; @Value("${spring.security.oauth2.kakao.host}") private String host; + @Value("${spring.security.oauth2.kakao.admin-key}") + private String adminKey; + @Override public OAuthProvider getAuthProvider() { return OAuthProvider.KAKAO; @@ -70,6 +79,39 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { @Override public void unlink(Long userId) { + String oauthId = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)) + .getOauthId(); + + String url = host + "/v1/user/unlink"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.add("Authorization", "KakaoAK " + adminKey); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("target_id_type", "user_id"); + body.add("target_id", oauthId); + HttpEntity httpRequest = new HttpEntity<>(body, httpHeaders); + + try { + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + httpRequest, + KakaoUserData.class + ); + assert response.getBody() != null; + + } catch (RestClientException e) { + log.warn("[KakaoService] failed to unlink User = {}", oauthId); + + if (e instanceof RestClientResponseException) { + throw new HttpClientException(ErrorCode.INVALID_OAUTH_TOKEN); + } + + throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED); + } } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0659603..7f72306 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: oauth2: kakao: host: https://kapi.kakao.com + admin-key: ${KAKAO_ADMIN_KEY} apple: host: https://appleid.apple.com grant-type: authorization_code diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 0659603..7f72306 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -11,6 +11,7 @@ spring: oauth2: kakao: host: https://kapi.kakao.com + admin-key: ${KAKAO_ADMIN_KEY} apple: host: https://appleid.apple.com grant-type: authorization_code