diff --git a/src/main/java/go/kr/project/levy/levyRelevy/controller/LevyRelevyController.java b/src/main/java/go/kr/project/levy/levyRelevy/controller/LevyRelevyController.java index de43b8a..5b5efd8 100644 --- a/src/main/java/go/kr/project/levy/levyRelevy/controller/LevyRelevyController.java +++ b/src/main/java/go/kr/project/levy/levyRelevy/controller/LevyRelevyController.java @@ -1,7 +1,11 @@ package go.kr.project.levy.levyRelevy.controller; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import egovframework.constant.TilesConstants; +import egovframework.exception.MessageException; import egovframework.util.ApiResponseUtil; +import egovframework.util.SessionUtil; import go.kr.project.common.model.CmmnCodeSearchVO; import go.kr.project.common.service.CommonCodeService; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; @@ -9,6 +13,7 @@ import go.kr.project.crdn.crndRegistAndView.main.service.CrdnImpltTaskService; import go.kr.project.levy.levyRelevy.model.LevyRelevyVO; import go.kr.project.levy.levyRelevy.service.LevyRelevyService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -17,12 +22,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; /** * packageName : go.kr.project.levy.levyRelevy.controller @@ -49,8 +52,12 @@ public class LevyRelevyController { /** 이행정보 서비스 */ private final CrdnImpltTaskService crdnImpltTaskService; + /** 공통코드 서비스 */ private final CommonCodeService commonCodeService; + /** JSON 파싱을 위한 ObjectMapper */ + private final ObjectMapper objectMapper; + /** * 재부과 대상 목록 화면을 제공하고, 검색 조건에 사용될 공통코드를 모델에 추가한다. * @param model 뷰에 전달할 데이터를 담는 모델 객체 @@ -151,4 +158,104 @@ public class LevyRelevyController { return ApiResponseUtil.successWithGrid(list, paramVO); } + /** + * 재부과 팝업 화면을 제공합니다 (복수 건 처리) + * @param crdnList 재부과 대상 목록 (JSON 문자열) + * @param model 뷰에 전달할 데이터를 담는 모델 객체 + * @return 재부과 팝업 화면 경로 + */ + @GetMapping("/relevyPopup.do") + @Operation(summary = "재부과 팝업 화면", description = "재부과 등록을 위한 팝업 화면을 제공합니다 (복수 건 처리 가능).") + public String relevyPopup( + @Parameter(description = "재부과 대상 목록 (JSON)", required = true) @RequestParam String crdnList, + Model model) { + log.debug("재부과 팝업 화면 요청 - crdnList: {}", crdnList); + model.addAttribute("crdnList", crdnList); + return "levy/levyRelevy/relevyPopup" + TilesConstants.POPUP; + } + + /** + * 단속 정보를 조회하는 AJAX 메소드 + * @param crdnYr 단속 연도 + * @param crdnNo 단속 번호 + * @return 단속 정보와 성공 상태를 담은 ResponseEntity 객체 + */ + @GetMapping("/selectCrdnInfo.ajax") + @Operation(summary = "단속 정보 조회 (AJAX)", description = "재부과를 위한 단속 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "단속 정보 조회 성공"), + @ApiResponse(responseCode = "400", description = "단속 정보 조회 실패"), + @ApiResponse(description = "오류로 인한 실패") + }) + public ResponseEntity selectCrdnInfo( + @Parameter(description = "단속 연도", required = true) @RequestParam String crdnYr, + @Parameter(description = "단속 번호", required = true) @RequestParam String crdnNo) { + + log.debug("단속 정보 조회 요청: crdnYr={}, crdnNo={}", crdnYr, crdnNo); + + LevyRelevyVO searchVO = LevyRelevyVO.builder() + .crdnYr(crdnYr) + .crdnNo(crdnNo) + .build(); + + LevyRelevyVO crdnInfo = service.selectCrdnInfo(searchVO); + + if (crdnInfo == null) { + throw new MessageException("단속 정보가 존재하지 않습니다."); + } + + return ApiResponseUtil.success(crdnInfo); + } + + /** + * 일괄 재부과를 저장하는 AJAX 메소드 + * 중요로직: 복수 건의 단속 정보를 재부과 처리하여 새로운 단속 건으로 등록 + * + * @param crdnListJson 재부과 대상 목록 (JSON 문자열) + * @param relevyRsn 재부과 사유 + * @return 저장 결과와 성공 상태를 담은 ResponseEntity 객체 + */ + @PostMapping("/saveBatchRelevy.ajax") + @Operation(summary = "일괄 재부과 저장 (AJAX)", description = "복수 건의 재부과를 일괄 처리하여 저장합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "재부과 저장 성공"), + @ApiResponse(responseCode = "400", description = "재부과 저장 실패"), + @ApiResponse(description = "오류로 인한 실패") + }) + public ResponseEntity saveBatchRelevy( + @Parameter(description = "재부과 대상 목록 (JSON)", required = true) @RequestParam String crdnListJson, + @Parameter(description = "재부과 사유", required = true) @RequestParam String relevyRsn) { + + log.debug("일괄 재부과 저장 요청 - crdnListJson: {}, relevyRsn: {}", crdnListJson, relevyRsn); + + // 필수값 검증 + if (relevyRsn == null || relevyRsn.trim().isEmpty()) { + throw new MessageException("재부과 사유를 입력해주세요."); + } + + try { + // 중요로직: JSON 문자열을 List으로 파싱 + List> crdnList = objectMapper.readValue( + crdnListJson, + new TypeReference>>() {} + ); + + if (crdnList == null || crdnList.isEmpty()) { + throw new MessageException("재부과 대상 정보가 없습니다."); + } + + // 등록자 정보 설정 + String userId = SessionUtil.getUserId(); + + // 일괄 재부과 처리 + List> resultList = service.processBatchRelevy(crdnList, relevyRsn, userId); + + return ApiResponseUtil.success(resultList); + + } catch (Exception e) { + log.error("일괄 재부과 저장 중 오류 발생", e); + throw new MessageException("재부과 저장 중 오류가 발생했습니다: " + e.getMessage()); + } + } + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/levy/levyRelevy/mapper/LevyRelevyMapper.java b/src/main/java/go/kr/project/levy/levyRelevy/mapper/LevyRelevyMapper.java index 10d121b..9ab5c84 100644 --- a/src/main/java/go/kr/project/levy/levyRelevy/mapper/LevyRelevyMapper.java +++ b/src/main/java/go/kr/project/levy/levyRelevy/mapper/LevyRelevyMapper.java @@ -41,4 +41,11 @@ public interface LevyRelevyMapper { */ List selectRelevyChainList(LevyRelevyVO vo); + /** + * 단속 정보를 조회한다. + * @param vo 조회 조건을 담은 VO 객체 (crdnYr, crdnNo 필수) + * @return 단속 정보 + */ + LevyRelevyVO selectCrdnInfo(LevyRelevyVO vo); + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/levy/levyRelevy/service/LevyRelevyService.java b/src/main/java/go/kr/project/levy/levyRelevy/service/LevyRelevyService.java index fa3e163..8652d26 100644 --- a/src/main/java/go/kr/project/levy/levyRelevy/service/LevyRelevyService.java +++ b/src/main/java/go/kr/project/levy/levyRelevy/service/LevyRelevyService.java @@ -3,6 +3,7 @@ package go.kr.project.levy.levyRelevy.service; import go.kr.project.levy.levyRelevy.model.LevyRelevyVO; import java.util.List; +import java.util.Map; /** * packageName : go.kr.project.levy.levyRelevy.service @@ -41,4 +42,23 @@ public interface LevyRelevyService { */ List selectRelevyChainList(LevyRelevyVO vo); + /** + * 단속 정보를 조회합니다. + * + * @param vo 조회 조건을 담은 VO 객체 (crdnYr, crdnNo 필수) + * @return 단속 정보 + */ + LevyRelevyVO selectCrdnInfo(LevyRelevyVO vo); + + /** + * 일괄 재부과를 처리합니다. + * 중요로직: 복수 건의 단속 정보를 복사하여 새로운 단속 건으로 등록 + * + * @param crdnList 재부과 대상 목록 (crdnYr, crdnNo 포함) + * @param relevyRsn 재부과 사유 + * @param userId 등록자 ID + * @return 처리 결과 목록 (신규 생성된 단속 번호 등) + */ + List> processBatchRelevy(List> crdnList, String relevyRsn, String userId); + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/levy/levyRelevy/service/impl/LevyRelevyServiceImpl.java b/src/main/java/go/kr/project/levy/levyRelevy/service/impl/LevyRelevyServiceImpl.java index b18fedb..88fceca 100644 --- a/src/main/java/go/kr/project/levy/levyRelevy/service/impl/LevyRelevyServiceImpl.java +++ b/src/main/java/go/kr/project/levy/levyRelevy/service/impl/LevyRelevyServiceImpl.java @@ -1,5 +1,8 @@ package go.kr.project.levy.levyRelevy.service.impl; +import egovframework.exception.MessageException; +import go.kr.project.crdn.crndRegistAndView.main.service.CrdnRelevyService; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRelevyVO; import go.kr.project.levy.levyRelevy.mapper.LevyRelevyMapper; import go.kr.project.levy.levyRelevy.model.LevyRelevyVO; import go.kr.project.levy.levyRelevy.service.LevyRelevyService; @@ -7,7 +10,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -32,6 +38,9 @@ public class LevyRelevyServiceImpl extends EgovAbstractServiceImpl implements Le private final LevyRelevyMapper mapper; + /** 단속 재부과 서비스 (CRDN 패키지의 재부과 로직 재사용) */ + private final CrdnRelevyService crdnRelevyService; + /** * 재부과 대상 목록을 조회합니다. * 중요로직: 재부과 대상이 아닌 경우 빨간색으로 표시 @@ -87,4 +96,116 @@ public class LevyRelevyServiceImpl extends EgovAbstractServiceImpl implements Le return mapper.selectRelevyChainList(vo); } + /** + * 단속 정보를 조회합니다. + * + * @param vo 조회 조건을 담은 VO 객체 (crdnYr, crdnNo 필수) + * @return 단속 정보 + */ + @Override + public LevyRelevyVO selectCrdnInfo(LevyRelevyVO vo) { + return mapper.selectCrdnInfo(vo); + } + + /** + * 일괄 재부과를 처리합니다. + * 중요로직: 전체 건에 대해 먼저 중복 체크 후, 한 건이라도 중복이면 전체 취소 + * + * @param crdnList 재부과 대상 목록 (crdnYr, crdnNo 포함) + * @param relevyRsn 재부과 사유 + * @param userId 등록자 ID + * @return 처리 결과 목록 (신규 생성된 단속 번호 등) + */ + @Override + @Transactional + public List> processBatchRelevy(List> crdnList, String relevyRsn, String userId) { + log.info("일괄 재부과 처리 시작 - 대상 건수: {}", crdnList.size()); + + String currentYear = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy")); + List duplicateList = new ArrayList<>(); + + // 중요로직: 1단계 - 전체 건에 대해 재부과 중복 체크 + log.info("1단계: 전체 재부과 대상 중복 체크 시작"); + for (Map crdnItem : crdnList) { + String srcCrdnYr = crdnItem.get("crdnYr"); + String srcCrdnNo = crdnItem.get("crdnNo"); + + CrdnRelevyVO checkVO = new CrdnRelevyVO(); + checkVO.setCrdnYr(srcCrdnYr); + checkVO.setCrdnNo(srcCrdnNo); + checkVO.setCurrentYear(currentYear); + + CrdnRelevyVO existingRelevy = crdnRelevyService.selectRelevyCheckOne(checkVO); + if (existingRelevy != null) { + String duplicateInfo = String.format("%s-%s (기존 재부과: %s-%s)", + srcCrdnYr, srcCrdnNo, + existingRelevy.getCrdnYr(), existingRelevy.getCrdnNo()); + duplicateList.add(duplicateInfo); + log.warn("재부과 중복 발견: {}", duplicateInfo); + } + } + + // 중요로직: 2단계 - 중복이 발견되면 전체 취소 및 메시지 표출 + if (!duplicateList.isEmpty()) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("선택한 단속 건 중 이미 당해년도 재부과가 존재하는 건이 있습니다.\n\n"); + errorMsg.append("[중복된 재부과 대상 목록]\n"); + for (int i = 0; i < duplicateList.size(); i++) { + errorMsg.append(String.format("%d. %s\n", i + 1, duplicateList.get(i))); + } + errorMsg.append("\n재부과를 진행할 수 없습니다."); + + log.error("재부과 중복 건 발견으로 전체 취소 - 중복 건수: {}", duplicateList.size()); + throw new MessageException(errorMsg.toString()); + } + + log.info("재부과 중복 체크 완료 - 모든 건이 정상입니다."); + + // 중요로직: 3단계 - 각 단속 건에 대해 재부과 처리 수행 + List> resultList = new ArrayList<>(); + int successCount = 0; + + log.info("2단계: 재부과 처리 시작"); + for (Map crdnItem : crdnList) { + String srcCrdnYr = crdnItem.get("crdnYr"); + String srcCrdnNo = crdnItem.get("crdnNo"); + + try { + log.info("재부과 처리 중: srcCrdnYr={}, srcCrdnNo={}", srcCrdnYr, srcCrdnNo); + + // CrdnRelevyService의 processRelevy 메서드를 재사용 + CrdnRelevyVO relevyVO = new CrdnRelevyVO(); + relevyVO.setSrcCrdnYr(srcCrdnYr); + relevyVO.setSrcCrdnNo(srcCrdnNo); + relevyVO.setRelevyRsn(relevyRsn); + relevyVO.setRgtr(userId); + + Map result = crdnRelevyService.processRelevy(relevyVO); + + // 성공 결과 저장 + Map successResult = new HashMap<>(); + successResult.put("srcCrdnYr", srcCrdnYr); + successResult.put("srcCrdnNo", srcCrdnNo); + successResult.put("crdnYr", result.get("newCrdnYr")); + successResult.put("crdnNo", result.get("newCrdnNo")); + successResult.put("success", true); + resultList.add(successResult); + successCount++; + + log.info("재부과 처리 성공: srcCrdnYr={}, srcCrdnNo={}, newCrdnNo={}", + srcCrdnYr, srcCrdnNo, result.get("newCrdnNo")); + + } catch (Exception e) { + log.error("재부과 처리 실패: srcCrdnYr={}, srcCrdnNo={}", srcCrdnYr, srcCrdnNo, e); + + // 트랜잭션 롤백을 위해 예외 재발생 + throw new MessageException("재부과 처리 중 오류 발생: " + srcCrdnYr + "-" + srcCrdnNo + " - " + e.getMessage()); + } + } + + log.info("일괄 재부과 처리 완료 - 성공: {}", successCount); + + return resultList; + } + } \ No newline at end of file diff --git a/src/main/resources/mybatis/mapper/levy/levyRelevy/LevyRelevyMapper_maria.xml b/src/main/resources/mybatis/mapper/levy/levyRelevy/LevyRelevyMapper_maria.xml index d56bf75..fa08c8d 100644 --- a/src/main/resources/mybatis/mapper/levy/levyRelevy/LevyRelevyMapper_maria.xml +++ b/src/main/resources/mybatis/mapper/levy/levyRelevy/LevyRelevyMapper_maria.xml @@ -332,4 +332,36 @@ ORDER BY c.CRDN_YR DESC, c.CRDN_NO ASC + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/levy/levyRelevy/list.jsp b/src/main/webapp/WEB-INF/views/levy/levyRelevy/list.jsp index 024ea5b..ff7919d 100644 --- a/src/main/webapp/WEB-INF/views/levy/levyRelevy/list.jsp +++ b/src/main/webapp/WEB-INF/views/levy/levyRelevy/list.jsp @@ -20,12 +20,20 @@
  • 단속 년도
  • - +
  • 단속 번호
  • +
  • 재부과 대상
  • +
  • + +
  • 지역 구분
  • -
  • 재부과 대상
  • -
  • - -
  • 가중 부과 대상
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    단속 년도단속 번호
    지역구분법정동
    적발방법적발일자
    조사원현재 진행단계
    위치
    * 재부과 사유 + +
    + + + + + + + + + + + + +