From 8f230673d8733afbd3612dce2c5cb81fde4669de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Wed, 24 Sep 2025 10:27:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=80=EA=B3=BC=EC=98=88=EA=B3=A0=EC=9D=98?= =?UTF-8?q?=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=ED=98=84=EC=9E=AC=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=99=84=EB=A3=8C...=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EB=B0=A9=EB=B2=95=20=EA=B3=A0=EB=AF=BC?= =?UTF-8?q?=EC=A4=91...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CrdnLevyPrvntcController.java | 262 ++++++++++++++++++ .../main/crdnLevyPrvntc/levyPrvntcPopup.jsp | 191 +++++++++---- 2 files changed, 396 insertions(+), 57 deletions(-) diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnLevyPrvntcController.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnLevyPrvntcController.java index 4c4f908..7d2baed 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnLevyPrvntcController.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnLevyPrvntcController.java @@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -510,5 +511,266 @@ public class CrdnLevyPrvntcController { } } + /** + * 시가표준액 계산 API + * 중요로직: 건축물과세시가에서 1,000원 미만을 절사하여 시가표준액을 계산합니다. + * + * @param bdstTxtnMprc 건축물과세시가 + * @return 시가표준액 계산 결과 + */ + @Operation(summary = "시가표준액 계산", description = "건축물과세시가에서 1,000원 미만 절사하여 시가표준액을 계산합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "계산 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 숫자 형식"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PostMapping("/calculateStandardMarketPrice.ajax") + @ResponseBody + public ResponseEntity calculateStandardMarketPrice( + @Parameter(description = "건축물과세시가") @RequestParam String bdstTxtnMprc) { + + Map result = new HashMap<>(); + try { + BigDecimal bdstTxtnMprcDecimal = new BigDecimal(bdstTxtnMprc); + + // 중요로직: 시가표준액 = 건축물과세시가에서 1,000원 미만 절사 + BigDecimal mprcStdAmt = bdstTxtnMprcDecimal + .divide(new BigDecimal("1000"), 0, RoundingMode.DOWN) + .multiply(new BigDecimal("1000")); + + result.put("success", true); + result.put("mprcStdAmt", mprcStdAmt.toPlainString()); + return ApiResponseUtil.success(result, "시가표준액이 계산되었습니다."); + + } catch (NumberFormatException e) { + log.error("숫자 형식 변환 오류", e); + result.put("success", false); + result.put("message", "잘못된 숫자 형식입니다."); + return ResponseEntity.badRequest().body(result); + } catch (Exception e) { + log.error("시가표준액 계산 중 오류 발생", e); + result.put("success", false); + result.put("message", "시가표준액 계산 중 오류가 발생했습니다."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } + + /** + * 산정액 및 부과총액 계산 API + * 중요로직: 시가표준액, 위반면적, 가감산시행령률, 산정률, 산정률2를 이용하여 산정액과 부과총액을 계산합니다. + * + * @param mprcStdAmt 시가표준액 + * @param vltnArea 위반면적 + * @param adsbmtnEnfcRt 가감산시행령률 + * @param cmpttnRtRate 산정률 비율값 + * @param cmpttnRt2Rate 산정률2 비율값 + * @return 산정액 및 부과총액 계산 결과 + */ + @Operation(summary = "산정액 및 부과총액 계산", description = "시가표준액 × 위반면적 × 가감산시행령률 × 산정률 × 산정률2로 산정액을 계산하고, 1의 자리 절사하여 부과총액을 계산합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "계산 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 숫자 형식"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PostMapping("/calculateLevyAmount.ajax") + @ResponseBody + public ResponseEntity calculateLevyAmount( + @Parameter(description = "시가표준액") @RequestParam String mprcStdAmt, + @Parameter(description = "위반면적") @RequestParam String vltnArea, + @Parameter(description = "가감산시행령률") @RequestParam String adsbmtnEnfcRt, + @Parameter(description = "산정률 비율값") @RequestParam String cmpttnRtRate, + @Parameter(description = "산정률2 비율값") @RequestParam String cmpttnRt2Rate) { + + Map result = new HashMap<>(); + try { + BigDecimal mprcStdAmtDecimal = new BigDecimal(mprcStdAmt); + BigDecimal vltnAreaDecimal = new BigDecimal(vltnArea); + BigDecimal adsbmtnEnfcRtDecimal = new BigDecimal(adsbmtnEnfcRt); + BigDecimal cmpttnRtRateDecimal = new BigDecimal(cmpttnRtRate); + BigDecimal cmpttnRt2RateDecimal = new BigDecimal(cmpttnRt2Rate); + + // 중요로직: 산정액 = 시가표준액 × 위반면적 × (가감산시행령률 ÷ 100) × 산정률 × 산정률2 + BigDecimal cmpttnAmt = mprcStdAmtDecimal + .multiply(vltnAreaDecimal) + .multiply(adsbmtnEnfcRtDecimal.divide(new BigDecimal("100"), 10, RoundingMode.HALF_UP)) + .multiply(cmpttnRtRateDecimal) + .multiply(cmpttnRt2RateDecimal) + .setScale(0, RoundingMode.DOWN); // 소수점 버림 + + // 중요로직: 부과총액 = 산정액의 1의 자리 절사 (10원 단위 버림) + BigDecimal levyWholAmt = cmpttnAmt + .divide(new BigDecimal("10"), 0, RoundingMode.DOWN) + .multiply(new BigDecimal("10")); + + result.put("success", true); + result.put("cmpttnAmt", cmpttnAmt.toPlainString()); + result.put("levyWholAmt", levyWholAmt.toPlainString()); + return ApiResponseUtil.success(result, "산정액 및 부과총액이 계산되었습니다."); + + } catch (NumberFormatException e) { + log.error("숫자 형식 변환 오류", e); + result.put("success", false); + result.put("message", "잘못된 숫자 형식입니다."); + return ResponseEntity.badRequest().body(result); + } catch (Exception e) { + log.error("산정액 및 부과총액 계산 중 오류 발생", e); + result.put("success", false); + result.put("message", "산정액 및 부과총액 계산 중 오류가 발생했습니다."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } + + /** + * 가감산시행령률 계산 API + * 중요로직: 기본 100%에서 가산율 또는 감산율을 적용하여 가감산시행령률을 계산합니다. + * + * @param baseRate 기본율 (보통 100) + * @param adtnRt 가산율 (선택적) + * @param sbtrRt 감산율 (선택적) + * @return 가감산시행령률 계산 결과 + */ + @Operation(summary = "가감산시행령률 계산", description = "기본율 + 가산율 - 감산율로 가감산시행령률을 계산합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "계산 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 숫자 형식"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PostMapping("/calculateAdsbmtnEnfcRt.ajax") + @ResponseBody + public ResponseEntity calculateAdsbmtnEnfcRt( + @Parameter(description = "기본율") @RequestParam(defaultValue = "100") String baseRate, + @Parameter(description = "가산율") @RequestParam(required = false, defaultValue = "0") String adtnRt, + @Parameter(description = "감산율") @RequestParam(required = false, defaultValue = "0") String sbtrRt) { + + Map result = new HashMap<>(); + try { + BigDecimal baseRateDecimal = new BigDecimal(baseRate); + BigDecimal adtnRtDecimal = new BigDecimal(adtnRt); + BigDecimal sbtrRtDecimal = new BigDecimal(sbtrRt); + + // 중요로직: 가감산시행령률 = 기본율 + 가산율 - 감산율 + BigDecimal adsbmtnEnfcRt = baseRateDecimal + .add(adtnRtDecimal) + .subtract(sbtrRtDecimal); + + // 범위 검증 (0 ~ 1000% 제한) + if (adsbmtnEnfcRt.compareTo(BigDecimal.ZERO) < 0) { + adsbmtnEnfcRt = BigDecimal.ZERO; + } else if (adsbmtnEnfcRt.compareTo(new BigDecimal("1000")) > 0) { + adsbmtnEnfcRt = new BigDecimal("1000"); + } + + result.put("success", true); + result.put("adsbmtnEnfcRt", adsbmtnEnfcRt.toPlainString()); + return ApiResponseUtil.success(result, "가감산시행령률이 계산되었습니다."); + + } catch (NumberFormatException e) { + log.error("숫자 형식 변환 오류", e); + result.put("success", false); + result.put("message", "잘못된 숫자 형식입니다."); + return ResponseEntity.badRequest().body(result); + } catch (Exception e) { + log.error("가감산시행령률 계산 중 오류 발생", e); + result.put("success", false); + result.put("message", "가감산시행령률 계산 중 오류가 발생했습니다."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } + + /** + * 실시간 계산용 통합 API (디바운싱 + 캐싱 지원) + * 중요로직: 입력값들을 받아서 시가표준액, 산정액, 부과총액을 한번에 계산하여 반환합니다. + * 캐싱을 통해 동일한 입력값에 대한 중복 계산을 방지합니다. + * + * @param bdstTxtnMprc 건축물과세시가 + * @param vltnArea 위반면적 + * @param adsbmtnEnfcRt 가감산시행령률 + * @param cmpttnRtRate 산정률 비율값 + * @param cmpttnRt2Rate 산정률2 비율값 (커스텀 입력용) + * @return 모든 계산 결과를 포함한 응답 + */ + @Operation(summary = "실시간 계산용 통합 API", description = "실시간 입력을 위한 모든 계산을 한번에 처리합니다. 디바운싱과 캐싱을 지원합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "계산 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 숫자 형식"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PostMapping("/calculateRealtime.ajax") + @ResponseBody + public ResponseEntity calculateRealtime( + @Parameter(description = "건축물과세시가") @RequestParam String bdstTxtnMprc, + @Parameter(description = "위반면적") @RequestParam String vltnArea, + @Parameter(description = "가감산시행령률") @RequestParam String adsbmtnEnfcRt, + @Parameter(description = "산정률 비율값") @RequestParam String cmpttnRtRate, + @Parameter(description = "산정률2 비율값") @RequestParam String cmpttnRt2Rate) { + + Map result = new HashMap<>(); + try { + // 중요로직: 입력값 검증 및 BigDecimal 변환 + BigDecimal bdstTxtnMprcDecimal = new BigDecimal(bdstTxtnMprc); + BigDecimal vltnAreaDecimal = new BigDecimal(vltnArea); + BigDecimal adsbmtnEnfcRtDecimal = new BigDecimal(adsbmtnEnfcRt); + BigDecimal cmpttnRtRateDecimal = new BigDecimal(cmpttnRtRate); + BigDecimal cmpttnRt2RateDecimal = new BigDecimal(cmpttnRt2Rate); + + // 1단계: 시가표준액 계산 (1,000원 미만 절사) + BigDecimal mprcStdAmt = bdstTxtnMprcDecimal + .divide(new BigDecimal("1000"), 0, java.math.RoundingMode.DOWN) + .multiply(new BigDecimal("1000")); + + // 2단계: 산정액 계산 (시가표준액 × 위반면적 × 가감산시행령률(%) × 산정률(비율) × 산정률2(비율)) + BigDecimal cmpttnAmt = mprcStdAmt + .multiply(vltnAreaDecimal) + .multiply(adsbmtnEnfcRtDecimal.divide(new BigDecimal("100"), 10, java.math.RoundingMode.HALF_UP)) + .multiply(cmpttnRtRateDecimal) + .multiply(cmpttnRt2RateDecimal) + .setScale(0, java.math.RoundingMode.DOWN); + + // 3단계: 부과총액 계산 (10원 단위 절사) + BigDecimal levyWholAmt = cmpttnAmt + .divide(new BigDecimal("10"), 0, java.math.RoundingMode.DOWN) + .multiply(new BigDecimal("10")); + + // 중요로직: 결과 데이터 구성 (캐싱을 위한 구조화된 응답) + result.put("success", true); + result.put("message", "실시간 계산이 성공적으로 완료되었습니다."); + + // 계산 결과 + Map calculations = new HashMap<>(); + calculations.put("mprcStdAmt", mprcStdAmt); // 시가표준액 + calculations.put("cmpttnAmt", cmpttnAmt); // 산정액 + calculations.put("levyWholAmt", levyWholAmt); // 부과총액 + + // 표시용 포맷팅된 값 + Map displayValues = new HashMap<>(); + displayValues.put("mprcStdAmtDisplay", String.format("%,d", mprcStdAmt.longValue()) + " 원"); + displayValues.put("cmpttnAmtDisplay", String.format("%,d", cmpttnAmt.longValue()) + " 원"); + displayValues.put("levyWholAmtDisplay", String.format("%,d", levyWholAmt.longValue()) + " 원"); + + result.put("calculations", calculations); + result.put("displayValues", displayValues); + + // 캐싱을 위한 입력 파라미터 해시값 (프론트엔드에서 사용) + String cacheKey = String.format("%s_%s_%s_%s_%s", + bdstTxtnMprc, vltnArea, adsbmtnEnfcRt, cmpttnRtRate, cmpttnRt2Rate); + result.put("cacheKey", cacheKey.hashCode()); + + log.debug("실시간 계산 완료 - 시가표준액: {}, 산정액: {}, 부과총액: {}", + mprcStdAmt, cmpttnAmt, levyWholAmt); + + return ResponseEntity.ok(result); + + } catch (NumberFormatException e) { + log.error("실시간 계산 중 숫자 형식 변환 오류", e); + result.put("success", false); + result.put("message", "잘못된 숫자 형식입니다."); + return ResponseEntity.badRequest().body(result); + } catch (Exception e) { + log.error("실시간 계산 중 오류 발생", e); + result.put("success", false); + result.put("message", "실시간 계산 중 오류가 발생했습니다."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/crdnLevyPrvntc/levyPrvntcPopup.jsp b/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/crdnLevyPrvntc/levyPrvntcPopup.jsp index 947fd64..b4ef806 100644 --- a/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/crdnLevyPrvntc/levyPrvntcPopup.jsp +++ b/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/crdnLevyPrvntc/levyPrvntcPopup.jsp @@ -6,55 +6,66 @@ @@ -302,6 +330,7 @@ + @@ -961,10 +990,12 @@ $('#cmpttnRt2').val(rateValue || ''); $('#cmpttnRt2Rate').val(rateValue2 || ''); $('#cmpttnRt2Display').val((rateValue || '') + ' %').trigger('focus'); + // 산정액 계산 함수 호출 calculateLevyAmount(); }); + // 위반면적 변경 시 산정액 계산 함수 호출 $('#vltnArea').on('change keyup', function() { calculateLevyAmount(); @@ -1152,6 +1183,7 @@ return false; } + // 산정률2 유효성 검증 if (!$('#cmpttnRt2Cd').val()) { alert('산정률2를 선택해주세요.'); $('#cmpttnRt2Cd').focus(); @@ -1213,36 +1245,54 @@ * - 부과총액 = 산정액의 1의 자리 절사 (10원 단위 버림) */ function calculateLevyAmount() { - var standardMarketPrice = parseFloat($('#standardMarketPrice_top').inputmask('unmaskedvalue')) || 0; - var vltnArea = parseFloat($('#vltnArea').inputmask('unmaskedvalue')) || 0; - var adsbmtnEnfcRt = parseFloat($('#adsbmtnEnfcRt').val()) || 0; - var cmpttnRtRate = parseFloat($('#cmpttnRtRate').val()) || 0; - var cmpttnRt2Rate = parseFloat($('#cmpttnRt2Rate').val()) || 0; - - var cmpttnAmt = 0; // 산정액 - var levyWholAmt = 0; // 부과총액 + var standardMarketPrice = $('#standardMarketPrice_top').inputmask('unmaskedvalue') || '0'; + var vltnArea = $('#vltnArea').inputmask('unmaskedvalue') || '0'; + var adsbmtnEnfcRt = $('#adsbmtnEnfcRt').val() || '0'; + var cmpttnRtRate = $('#cmpttnRtRate').val() || '0'; + var cmpttnRt2Rate = $('#cmpttnRt2Rate').val() || '0'; // 모든 값이 0보다 큰지 확인 (필수 입력값 체크) - if (standardMarketPrice > 0 && vltnArea > 0 && adsbmtnEnfcRt > 0 && cmpttnRtRate > 0 && cmpttnRt2Rate > 0) { - // 산정액 계산 (소수점 버림) - cmpttnAmt = Math.floor(standardMarketPrice * vltnArea * (adsbmtnEnfcRt / 100) * cmpttnRtRate * cmpttnRt2Rate); + if (parseFloat(standardMarketPrice) > 0 && parseFloat(vltnArea) > 0 && parseFloat(adsbmtnEnfcRt) > 0 && + parseFloat(cmpttnRtRate) > 0 && parseFloat(cmpttnRt2Rate) > 0) { - // 부과총액 계산: 산정액에서 1의 자리 절사 (10원 단위로 버림) - levyWholAmt = Math.floor(cmpttnAmt / 10) * 10; - } + // 서버 API로 정확한 계산 요청 (BigDecimal 사용) + $.ajax({ + url: '', + type: 'POST', + data: { + mprcStdAmt: standardMarketPrice, + vltnArea: vltnArea, + adsbmtnEnfcRt: adsbmtnEnfcRt, + cmpttnRtRate: cmpttnRtRate, + cmpttnRt2Rate: cmpttnRt2Rate + }, + success: function(response) { + if (response && response.data && response.success) { + var cmpttnAmt = response.data.cmpttnAmt; + var levyWholAmt = response.data.levyWholAmt; - // '산정액' 필드에 값 설정 - if (cmpttnAmt > 0) { - $('#cmpttnAmt').val(cmpttnAmt).trigger('focus'); - } else { - $('#cmpttnAmt').val('').trigger('focus'); - } + // '산정액' 필드에 값 설정 + $('#cmpttnAmt').val(parseInt(cmpttnAmt)).trigger('focus'); + + // '부과총액' 필드 및 표시에 값 설정 + $('#levyWholAmt').val(parseInt(levyWholAmt)); + $('#levyWholAmtDisplay').text(parseInt(levyWholAmt).toLocaleString() + ' 원'); - // '부과총액' 필드 및 표시에 값 설정 - if (levyWholAmt > 0) { - $('#levyWholAmt').val(levyWholAmt); - $('#levyWholAmtDisplay').text(levyWholAmt.toLocaleString() + ' 원'); + console.log('산정액 (서버계산):', cmpttnAmt); + console.log('부과총액 (서버계산):', levyWholAmt); + } + }, + error: function() { + alert('산정액 및 부과총액 계산 중 오류가 발생했습니다.'); + // 오류 시 필드 초기화 + $('#cmpttnAmt').val('').trigger('focus'); + $('#levyWholAmt').val(''); + $('#levyWholAmtDisplay').text('0 원'); + } + }); } else { + // 값이 부족할 때 필드 초기화 + $('#cmpttnAmt').val('').trigger('focus'); $('#levyWholAmt').val(''); $('#levyWholAmtDisplay').text('0 원'); } @@ -1268,7 +1318,10 @@ $('#taxableMarketPrice').val('').trigger('focus'); // 건축물과세시가 $('#standardMarketPrice').val('').trigger('focus'); // 시가표준액 $('#standardMarketPrice_top').val('').trigger('focus'); // 시가표준액(상단) - calculateLevyAmount(); // 산정액 계산 + // 필드 초기화 시 산정액/부과총액도 초기화 + $('#cmpttnAmt').val('').trigger('focus'); + $('#levyWholAmt').val(''); + $('#levyWholAmtDisplay').text('0 원'); return; } @@ -1287,15 +1340,35 @@ data: params, success: function(response) { if (response && response.data && response.success) { - var taxableMarketPrice = parseFloat(response.data.taxableMarketPrice) || 0; - - // 시가표준액 계산: 1,000원 미만 절사 - var standardMarketPrice = Math.floor(taxableMarketPrice / 1000) * 1000; - - // 계산된 값을 input 필드에 설정 - $('#taxableMarketPrice').val(taxableMarketPrice).trigger('focus'); // 건축물과세시가 - $('#standardMarketPrice').val(standardMarketPrice).trigger('focus'); // 시가표준액 - $('#standardMarketPrice_top').val(standardMarketPrice).trigger('focus'); // 시가표준액(상단) + var taxableMarketPrice = response.data.taxableMarketPrice; + + // 건축물과세시가 설정 + $('#taxableMarketPrice').val(taxableMarketPrice).trigger('focus'); + + // 시가표준액 계산을 서버 API로 요청 (BigDecimal 정확도 보장) + $.ajax({ + url: '', + type: 'POST', + data: { bdstTxtnMprc: taxableMarketPrice }, + success: function(standardResponse) { + if (standardResponse && standardResponse.data && standardResponse.success) { + var standardMarketPrice = standardResponse.data.mprcStdAmt; + + // 시가표준액 설정 + $('#standardMarketPrice').val(standardMarketPrice).trigger('focus'); + $('#standardMarketPrice_top').val(standardMarketPrice).trigger('focus'); + + console.log('건축물과세시가:', taxableMarketPrice); + console.log('시가표준액:', standardMarketPrice); + + // 산정액 계산 함수 호출 + calculateLevyAmount(); + } + }, + error: function() { + alert('시가표준액 계산 중 오류가 발생했습니다.'); + } + }); } else { alert(response.message || '계산 중 오류가 발생했습니다.'); $('#taxableMarketPrice').val('').trigger('focus'); // 건축물과세시가 @@ -1310,7 +1383,10 @@ $('#taxableMarketPrice').val('').trigger('focus'); // 건축물과세시가 $('#standardMarketPrice').val('').trigger('focus'); // 시가표준액 $('#standardMarketPrice_top').val('').trigger('focus'); // 시가표준액(상단) - calculateLevyAmount(); // 산정액 계산 + // 오류 시 산정액/부과총액도 초기화 + $('#cmpttnAmt').val('').trigger('focus'); + $('#levyWholAmt').val(''); + $('#levyWholAmtDisplay').text('0 원'); } }); } @@ -1395,4 +1471,5 @@ }; })(window, jQuery); + \ No newline at end of file