초기 셋팅
parent
05578efebc
commit
ed04c2ae65
@ -0,0 +1,14 @@
|
||||
-- 자동차 과태료 대상 ID 시퀀스
|
||||
-- 테이블: tb_car_ffnlg_trgt
|
||||
-- 컬럼: CAR_FFNLG_TRGT_ID (varchar(20))
|
||||
|
||||
-- 시퀀스 삭제 (존재하는 경우)
|
||||
DROP SEQUENCE IF EXISTS seq_car_ffnlg_trgt_id;
|
||||
|
||||
-- 시퀀스 생성
|
||||
CREATE SEQUENCE seq_car_ffnlg_trgt_id
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
MINVALUE 1
|
||||
MAXVALUE 99999999999999999999
|
||||
NOCACHE;
|
||||
@ -0,0 +1,33 @@
|
||||
create table tb_car_ffnlg_trgt
|
||||
(
|
||||
CAR_FFNLG_TRGT_ID varchar(20) not null comment '자동차 과태료 대상 ID'
|
||||
primary key,
|
||||
RCPT_YMD varchar(8) null comment '접수 일자',
|
||||
FFNLG_TRGT_SE_CD varchar(1) null comment '과태료 대상 구분 코드',
|
||||
INSPSTN_CD varchar(8) null comment '검사소 코드',
|
||||
INSP_YMD varchar(8) null comment '검사 일자',
|
||||
VHCLNO varchar(30) null comment '차량번호',
|
||||
OWNR_NM varchar(75) null comment '소유자 명',
|
||||
RRNO varchar(100) null comment '주민등록번호',
|
||||
CAR_NM varchar(100) null comment '자동차 명',
|
||||
CAR_KND varchar(100) null comment '자동차 종류',
|
||||
CAR_USG varchar(100) null comment '자동차 용도',
|
||||
INSP_END_YMD varchar(8) null comment '검사 종료 일자',
|
||||
DAYCNT varchar(5) null comment '일수',
|
||||
FFNLG_AMT varchar(10) null comment '과태료 금액',
|
||||
LAST_REG_YMD varchar(8) null comment '최종 등록 일자',
|
||||
ADDR varchar(600) null comment '주소',
|
||||
VLD_PRD_EXPRY_YMD varchar(8) null comment '유효 기간 만료 일자',
|
||||
TRD_GDS varchar(100) null comment '매매 상품',
|
||||
TASK_PRCS_STTS_CD varchar(2) null comment '업무 처리 상태 코드',
|
||||
TASK_PRCS_YMD varchar(8) null comment '업무 처리 일자',
|
||||
RMRK varchar(4000) null comment '비고',
|
||||
CAR_BASS_MATTER_INQIRE_ID varchar(20) null comment '자동차 기본 사항 조회 ID',
|
||||
CAR_LEDGER_FRMBK_ID varchar(20) null comment '자동차 등록 원부 갑 ID',
|
||||
REG_DT datetime null comment '등록 일시',
|
||||
RGTR varchar(11) null comment '등록자',
|
||||
DEL_DT datetime null comment '삭제 일시',
|
||||
DLTR varchar(11) null comment '삭제자'
|
||||
)
|
||||
comment '자동차 과태료 대상';
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
유효기간경과 과태료부과대상 리스트
|
||||
------------------------------------
|
||||
|
||||
|
||||
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
|
||||
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
|
||||
최종등록일 주 소 유효기간만료일 매매상품용
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
H494 2025-11-01 경기11사2222 행주운수(주) 1111110081111 엠뱅크언더리프 특수차구난형소영업용 2021-01-05 1761 30만원
|
||||
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
|
||||
|
||||
H500 2025-11-01 22고2222 주식회사 아일공행산업 111110681111 그랜드 스타렉스 화물차밴형소형자가용 2025-04-28 187 60만원
|
||||
2025-10-01 경기도 용인시 처인구 포곡읍 포곡로 222-2, 202호 2025-03-28
|
||||
|
||||
H692 2025-11-01 33마3333 홍길동 7604092328316 SM6 승용차일반형중자가용 2025-07-14 110 56만원
|
||||
2025-09-22 경기도 용인시 기흥구 관곡로 53, 605동 1802호(구갈동, 가현마을신안아파트) 2025-06-11
|
||||
|
||||
H271 2025-11-01 44구4444 제제제이엔지 주식회사 1111110064044 봉고Ⅲ 1톤 화물차일반형- 자가용 2025-08-25 68 28만원
|
||||
2020-05-20 경기도 용인시 처인구 포곡읍 에버랜드로 444(0-44동(4층)) 2025-07-24
|
||||
|
||||
H420 2025-11-01 55서5555 김철수 5555261080555 아이오닉6 (IONI 승용차일반형중자가용 2025-08-25 68 28만원
|
||||
2024-09-09 경기도 용인시 수지구 성복1로 55, 505동 505호(성오동, 성오역 서피오타치오) 2025-07-25
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 TXT 파일 파싱 설정
|
||||
* application.yml의 car-ffnlg-txt-parse 설정을 바인딩
|
||||
*
|
||||
* hangul-byte-size 값에 따라 적절한 바이트 길이 설정을 자동으로 선택합니다.
|
||||
* - hangul-byte-size: 2 → byte-size-2 설정 사용 (EUC-KR, MS949)
|
||||
* - hangul-byte-size: 3 → byte-size-3 설정 사용 (UTF-8)
|
||||
*
|
||||
* 각 항목별로 지정된 바이트 길이만 설정 (from-to 방식이 아님)
|
||||
* 예: inspstn-cd: 8 (8바이트), insp-ymd: 12 (12바이트)
|
||||
* -1 값은 "나머지 전체"를 의미
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "car-ffnlg-txt-parse")
|
||||
@Data
|
||||
public class CarFfnlgTxtParseConfig {
|
||||
|
||||
/**
|
||||
* 파일 인코딩 (UTF-8, EUC-KR, MS949 등)
|
||||
*/
|
||||
private String encoding;
|
||||
|
||||
/**
|
||||
* 한글 문자 바이트 크기
|
||||
* 2 = EUC-KR/MS949 (2바이트)
|
||||
* 3 = UTF-8 (3바이트)
|
||||
*/
|
||||
private int hangulByteSize;
|
||||
|
||||
/**
|
||||
* 2바이트 환경 설정 (EUC-KR, MS949)
|
||||
*/
|
||||
private ByteSizeConfig byteSize2;
|
||||
|
||||
/**
|
||||
* 3바이트 환경 설정 (UTF-8)
|
||||
*/
|
||||
private ByteSizeConfig byteSize3;
|
||||
|
||||
/**
|
||||
* 바이트 크기별 설정 (first-line, second-line 포함)
|
||||
*/
|
||||
@Data
|
||||
public static class ByteSizeConfig {
|
||||
/**
|
||||
* 첫째줄 필드별 바이트 길이
|
||||
*/
|
||||
private Map<String, Integer> firstLine;
|
||||
|
||||
/**
|
||||
* 둘째줄 필드별 바이트 길이
|
||||
*/
|
||||
private Map<String, Integer> secondLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 hangul-byte-size에 해당하는 설정 가져오기
|
||||
*
|
||||
* @return ByteSizeConfig (2바이트 또는 3바이트 설정)
|
||||
*/
|
||||
private ByteSizeConfig getCurrentConfig() {
|
||||
if (hangulByteSize == 3) {
|
||||
return byteSize3;
|
||||
} else {
|
||||
// 기본값은 2바이트 (EUC-KR, MS949)
|
||||
return byteSize2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 첫째줄 필드의 바이트 길이 가져오기
|
||||
* hangul-byte-size 값에 따라 적절한 설정에서 길이를 반환합니다.
|
||||
*
|
||||
* @param fieldKey 필드 키 (예: "inspstn-cd", "insp-ymd")
|
||||
* @return 바이트 길이 (-1 = 나머지 전체)
|
||||
*/
|
||||
public int getFirstLineLength(String fieldKey) {
|
||||
ByteSizeConfig config = getCurrentConfig();
|
||||
if (config == null || config.getFirstLine() == null) {
|
||||
return 0;
|
||||
}
|
||||
Integer length = config.getFirstLine().get(fieldKey);
|
||||
return length != null ? length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 둘째줄 필드의 바이트 길이 가져오기
|
||||
* hangul-byte-size 값에 따라 적절한 설정에서 길이를 반환합니다.
|
||||
*
|
||||
* @param fieldKey 필드 키 (예: "last-reg-ymd", "addr")
|
||||
* @return 바이트 길이 (-1 = 나머지 전체)
|
||||
*/
|
||||
public int getSecondLineLength(String fieldKey) {
|
||||
ByteSizeConfig config = getCurrentConfig();
|
||||
if (config == null || config.getSecondLine() == null) {
|
||||
return 0;
|
||||
}
|
||||
Integer length = config.getSecondLine().get(fieldKey);
|
||||
return length != null ? length : 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.controller;
|
||||
|
||||
import egovframework.constant.MessageConstants;
|
||||
import egovframework.constant.TilesConstants;
|
||||
import egovframework.util.ApiResponseUtil;
|
||||
import egovframework.util.SessionUtil;
|
||||
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
|
||||
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
|
||||
import go.kr.project.common.model.CmmnCodeSearchVO;
|
||||
import go.kr.project.common.service.CommonCodeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 대상 등록 Controller
|
||||
* 과태료 대상 목록 조회, TXT 파일 업로드 기능 제공
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/carInspectionPenalty/registration")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "자동차 과태료 대상 등록", description = "자동차 과태료 대상 등록 및 목록 조회 API")
|
||||
public class CarFfnlgTrgtController {
|
||||
|
||||
private final CarFfnlgTrgtService service;
|
||||
private final CommonCodeService commonCodeService;
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 화면
|
||||
* @param model 모델
|
||||
* @return 목록 화면 경로
|
||||
*/
|
||||
@GetMapping("/list.do")
|
||||
@Operation(summary = "과태료 대상 목록 화면", description = "과태료 대상 목록 조회 화면을 제공합니다.")
|
||||
public String list(Model model) {
|
||||
log.debug("과태료 대상 목록 화면 요청");
|
||||
|
||||
// 업무 처리 상태 코드 조회 (공통코드)
|
||||
CmmnCodeSearchVO taskPrcsSttsCdSearchVO = CmmnCodeSearchVO.builder()
|
||||
.searchCdGroupId("TASK_PRCS_STTS_CD")
|
||||
.searchUseYn("Y")
|
||||
.sortColumn("SORT_ORDR")
|
||||
.sortAscending(true)
|
||||
.build();
|
||||
model.addAttribute("taskPrcsSttsCdList", commonCodeService.selectCodeDetailList(taskPrcsSttsCdSearchVO));
|
||||
|
||||
return "carInspectionPenalty/registration/list" + TilesConstants.BASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 조회 AJAX
|
||||
* @param paramVO 검색 조건
|
||||
* @return 목록 데이터
|
||||
*/
|
||||
@PostMapping("/list.ajax")
|
||||
@Operation(summary = "과태료 대상 목록 조회", description = "과태료 대상 목록을 조회하고 JSON 형식으로 반환합니다.")
|
||||
public ResponseEntity<?> listAjax(@ModelAttribute CarFfnlgTrgtVO paramVO) {
|
||||
log.debug("과태료 대상 목록 조회 AJAX - 검색조건: {}", paramVO);
|
||||
|
||||
// 1. 총 개수 조회
|
||||
int totalCount = service.selectListTotalCount(paramVO);
|
||||
|
||||
// 2. totalCount 설정
|
||||
paramVO.setTotalCount(totalCount);
|
||||
|
||||
// 3. 페이징 활성화
|
||||
paramVO.setPagingYn("Y");
|
||||
|
||||
// 목록 조회
|
||||
List<CarFfnlgTrgtVO> list = service.selectList(paramVO);
|
||||
|
||||
return ApiResponseUtil.successWithGrid(list, paramVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 업로드 팝업 화면
|
||||
* @return 팝업 화면
|
||||
*/
|
||||
@GetMapping("/uploadPopup.do")
|
||||
@Operation(summary = "파일 업로드 팝업", description = "TXT 파일 업로드 팝업 화면을 제공합니다.")
|
||||
public ModelAndView uploadPopup() {
|
||||
log.debug("파일 업로드 팝업 화면 요청");
|
||||
|
||||
ModelAndView mav = new ModelAndView("carInspectionPenalty/registration/uploadPopup" + TilesConstants.POPUP);
|
||||
|
||||
return mav;
|
||||
}
|
||||
|
||||
/**
|
||||
* TXT 파일 업로드 및 처리
|
||||
*
|
||||
* 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨
|
||||
*
|
||||
* @param file 업로드된 TXT 파일
|
||||
* @return 처리 결과
|
||||
*/
|
||||
@PostMapping("/upload.ajax")
|
||||
@Operation(summary = "TXT 파일 업로드", description = "TXT 파일을 업로드하고 파싱하여 DB에 저장합니다. 한 건이라도 실패 시 전체 롤백됩니다.")
|
||||
public ResponseEntity<?> upload(
|
||||
@Parameter(description = "TXT 파일") @RequestParam("file") MultipartFile file) {
|
||||
|
||||
log.info("TXT 파일 업로드 요청 - 파일명: {}", file != null ? file.getOriginalFilename() : "null");
|
||||
|
||||
try {
|
||||
// 세션에서 사용자 ID 가져오기
|
||||
String rgtr = SessionUtil.getUserId();
|
||||
if (rgtr == null || rgtr.isEmpty()) {
|
||||
return ApiResponseUtil.error("로그인 정보가 없습니다.");
|
||||
}
|
||||
|
||||
// 파일 업로드 및 파싱 (한 건이라도 실패 시 전체 롤백)
|
||||
Map<String, Object> result = service.uploadAndParseTxtFile(file, rgtr);
|
||||
|
||||
boolean success = (boolean) result.get("success");
|
||||
int successCount = (int) result.get("successCount");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> errorMessages = (List<String>) result.get("errorMessages");
|
||||
|
||||
if (success) {
|
||||
// 모든 데이터가 성공적으로 저장됨
|
||||
String message = String.format("파일 업로드가 완료되었습니다.\n\n성공: %d건", successCount);
|
||||
return ApiResponseUtil.success(result, message);
|
||||
} else {
|
||||
// 파일 검증 오류 (트랜잭션 시작 전 오류)
|
||||
StringBuilder message = new StringBuilder();
|
||||
if (!errorMessages.isEmpty()) {
|
||||
for (String errorMsg : errorMessages) {
|
||||
message.append(errorMsg).append("\n");
|
||||
}
|
||||
} else {
|
||||
message.append("파일 업로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
return ApiResponseUtil.error(message.toString());
|
||||
}
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
// 데이터 처리 중 오류 발생 - 전체 롤백됨
|
||||
log.error("TXT 파일 업로드 중 오류 발생 - 전체 롤백", e);
|
||||
return ApiResponseUtil.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
// 예상치 못한 오류
|
||||
log.error("TXT 파일 업로드 중 예상치 못한 오류 발생", e);
|
||||
return ApiResponseUtil.error("파일 업로드 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 과태료 대상 상세 조회
|
||||
* @param carFfnlgTrgtId 과태료 대상 ID
|
||||
* @return 상세 정보
|
||||
*/
|
||||
@GetMapping("/selectOne.ajax")
|
||||
@Operation(summary = "과태료 대상 상세 조회", description = "과태료 대상 상세 정보를 조회합니다.")
|
||||
public ResponseEntity<?> selectOne(
|
||||
@Parameter(description = "과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
|
||||
|
||||
log.debug("과태료 대상 상세 조회 - ID: {}", carFfnlgTrgtId);
|
||||
|
||||
try {
|
||||
CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
|
||||
vo.setCarFfnlgTrgtId(carFfnlgTrgtId);
|
||||
|
||||
CarFfnlgTrgtVO result = service.selectOne(vo);
|
||||
|
||||
if (result != null) {
|
||||
return ApiResponseUtil.success(result, "조회 성공");
|
||||
} else {
|
||||
return ApiResponseUtil.error("조회된 데이터가 없습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("과태료 대상 상세 조회 중 오류 발생", e);
|
||||
return ApiResponseUtil.error("조회 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 과태료 대상 삭제 (논리삭제)
|
||||
* @param carFfnlgTrgtId 과태료 대상 ID
|
||||
* @return 삭제 결과
|
||||
*/
|
||||
@PostMapping("/delete.ajax")
|
||||
@Operation(summary = "과태료 대상 삭제", description = "과태료 대상을 삭제(논리삭제)합니다.")
|
||||
public ResponseEntity<?> delete(
|
||||
@Parameter(description = "과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
|
||||
|
||||
log.info("과태료 대상 삭제 요청 - ID: {}", carFfnlgTrgtId);
|
||||
|
||||
try {
|
||||
String dltr = SessionUtil.getUserId();
|
||||
if (dltr == null || dltr.isEmpty()) {
|
||||
return ApiResponseUtil.error("로그인 정보가 없습니다.");
|
||||
}
|
||||
|
||||
CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
|
||||
vo.setCarFfnlgTrgtId(carFfnlgTrgtId);
|
||||
vo.setDltr(dltr);
|
||||
|
||||
int result = service.delete(vo);
|
||||
|
||||
if (result > 0) {
|
||||
return ApiResponseUtil.success(MessageConstants.Common.DELETE_SUCCESS);
|
||||
} else {
|
||||
return ApiResponseUtil.error(MessageConstants.Common.DELETE_ERROR);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("과태료 대상 삭제 중 오류 발생", e);
|
||||
return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.mapper;
|
||||
|
||||
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 대상 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CarFfnlgTrgtMapper {
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 총 개수 조회
|
||||
* @param vo 검색 조건
|
||||
* @return 총 개수
|
||||
*/
|
||||
int selectListTotalCount(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 조회
|
||||
* @param vo 검색 조건
|
||||
* @return 목록
|
||||
*/
|
||||
List<CarFfnlgTrgtVO> selectList(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 상세 조회
|
||||
* @param vo 조회 조건 (carFfnlgTrgtId)
|
||||
* @return 상세 정보
|
||||
*/
|
||||
CarFfnlgTrgtVO selectOne(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 등록
|
||||
* @param vo 등록할 데이터
|
||||
* @return 등록 건수
|
||||
*/
|
||||
int insert(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 수정
|
||||
* @param vo 수정할 데이터
|
||||
* @return 수정 건수
|
||||
*/
|
||||
int update(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 삭제 (논리삭제)
|
||||
* @param vo 삭제할 데이터 (carFfnlgTrgtId, dltr)
|
||||
* @return 삭제 건수
|
||||
*/
|
||||
int delete(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 차량번호로 중복 체크 (삭제되지 않은 데이터 중)
|
||||
* @param vo 조회 조건 (vhclno, inspYmd)
|
||||
* @return 존재 개수
|
||||
*/
|
||||
int checkDuplicateVhclno(CarFfnlgTrgtVO vo);
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.model;
|
||||
|
||||
import go.kr.project.common.model.PagingVO;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 대상 VO
|
||||
* 테이블: tb_car_ffnlg_trgt
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CarFfnlgTrgtVO extends PagingVO {
|
||||
|
||||
// 기본키
|
||||
private String carFfnlgTrgtId; // 자동차 과태료 대상 ID
|
||||
|
||||
// 업무 필드
|
||||
private String rcptYmd; // 접수 일자
|
||||
private String ffnlgTrgtSeCd; // 과태료 대상 구분 코드
|
||||
private String inspstnCd; // 검사소 코드
|
||||
private String inspYmd; // 검사 일자
|
||||
private String vhclno; // 차량번호
|
||||
private String ownrNm; // 소유자 명
|
||||
private String rrno; // 주민등록번호
|
||||
private String carNm; // 자동차 명
|
||||
private String carKnd; // 자동차 종류
|
||||
private String carUsg; // 자동차 용도
|
||||
private String inspEndYmd; // 검사 종료 일자
|
||||
private String daycnt; // 일수
|
||||
private String ffnlgAmt; // 과태료 금액
|
||||
private String lastRegYmd; // 최종 등록 일자
|
||||
private String addr; // 주소
|
||||
private String vldPrdExpryYmd; // 유효 기간 만료 일자
|
||||
private String trdGds; // 매매 상품
|
||||
private String taskPrcsSttsCd; // 업무 처리 상태 코드 (01=접수, 02=처리중, 03=완료)
|
||||
private String taskPrcsYmd; // 업무 처리 일자
|
||||
private String rmrk; // 비고
|
||||
private String carBassMatterInqireId; // 자동차 기본 사항 조회 ID
|
||||
private String carLedgerFrmbkId; // 자동차 등록 원부 갑 ID
|
||||
|
||||
// 감사 필드
|
||||
private LocalDateTime regDt; // 등록 일시
|
||||
private String rgtr; // 등록자
|
||||
private LocalDateTime delDt; // 삭제 일시
|
||||
private String dltr; // 삭제자
|
||||
|
||||
// 조회용 필드
|
||||
private String taskPrcsSttsCdNm; // 업무 처리 상태 코드명
|
||||
private String rgtrNm; // 등록자명
|
||||
|
||||
// 검색 조건 필드
|
||||
private String schRcptYmdStart; // 검색 시작 접수 일자
|
||||
private String schRcptYmdEnd; // 검색 종료 접수 일자
|
||||
private String schVhclno; // 검색 차량번호
|
||||
private String schOwnrNm; // 검색 소유자명
|
||||
private String schTaskPrcsSttsCd; // 검색 업무 처리 상태 코드
|
||||
private String schInspYmdStart; // 검색 시작 검사 일자
|
||||
private String schInspYmdEnd; // 검색 종료 검사 일자
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.service;
|
||||
|
||||
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 대상 Service
|
||||
*/
|
||||
public interface CarFfnlgTrgtService {
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 총 개수 조회
|
||||
* @param vo 검색 조건
|
||||
* @return 총 개수
|
||||
*/
|
||||
int selectListTotalCount(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 조회
|
||||
* @param vo 검색 조건
|
||||
* @return 목록
|
||||
*/
|
||||
List<CarFfnlgTrgtVO> selectList(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 상세 조회
|
||||
* @param vo 조회 조건 (carFfnlgTrgtId)
|
||||
* @return 상세 정보
|
||||
*/
|
||||
CarFfnlgTrgtVO selectOne(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 등록
|
||||
* @param vo 등록할 데이터
|
||||
* @return 등록 건수
|
||||
*/
|
||||
int insert(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 수정
|
||||
* @param vo 수정할 데이터
|
||||
* @return 수정 건수
|
||||
*/
|
||||
int update(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* 과태료 대상 삭제 (논리삭제)
|
||||
* @param vo 삭제할 데이터 (carFfnlgTrgtId, dltr)
|
||||
* @return 삭제 건수
|
||||
*/
|
||||
int delete(CarFfnlgTrgtVO vo);
|
||||
|
||||
/**
|
||||
* TXT 파일 업로드 및 파싱하여 DB 저장
|
||||
* @param file 업로드된 TXT 파일
|
||||
* @param rgtr 등록자 ID
|
||||
* @return 처리 결과 (성공 건수, 실패 건수, 오류 메시지 목록)
|
||||
*/
|
||||
Map<String, Object> uploadAndParseTxtFile(MultipartFile file, String rgtr);
|
||||
}
|
||||
@ -0,0 +1,737 @@
|
||||
package go.kr.project.carInspectionPenalty.registration.service.impl;
|
||||
|
||||
import egovframework.exception.MessageException;
|
||||
import go.kr.project.carInspectionPenalty.registration.config.CarFfnlgTxtParseConfig;
|
||||
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
|
||||
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
|
||||
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 자동차 과태료 대상 Service 구현체
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CarFfnlgTrgtServiceImpl implements CarFfnlgTrgtService {
|
||||
|
||||
private final CarFfnlgTrgtMapper mapper;
|
||||
private final CarFfnlgTxtParseConfig parseConfig;
|
||||
|
||||
// 날짜 형식 (YYYYMMDD)
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
@Override
|
||||
public int selectListTotalCount(CarFfnlgTrgtVO vo) {
|
||||
return mapper.selectListTotalCount(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CarFfnlgTrgtVO> selectList(CarFfnlgTrgtVO vo) {
|
||||
return mapper.selectList(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CarFfnlgTrgtVO selectOne(CarFfnlgTrgtVO vo) {
|
||||
return mapper.selectOne(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int insert(CarFfnlgTrgtVO vo) {
|
||||
return mapper.insert(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int update(CarFfnlgTrgtVO vo) {
|
||||
return mapper.update(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int delete(CarFfnlgTrgtVO vo) {
|
||||
return mapper.delete(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* TXT 파일 업로드 및 파싱하여 DB 저장
|
||||
*
|
||||
* 파일 형식: 고정폭 텍스트 파일 (2줄 1세트)
|
||||
* - 헤더: 8, 9번째 라인
|
||||
* - 데이터: 11번째 라인부터 시작
|
||||
* - 각 데이터는 2줄로 구성
|
||||
* 1) 첫 번째 줄: 검사소/검사일자/차량번호/소유자명/주민번호/차명/차종/용도/종료일/일수/과태료
|
||||
* 2) 두 번째 줄: 최종등록일/주소/유효기간만료일/매매상품
|
||||
*
|
||||
* 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> uploadAndParseTxtFile(MultipartFile file, String rgtr) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<String> errorMessages = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int dataLineNumber = 0; // 실제 데이터 라인 번호
|
||||
|
||||
try {
|
||||
// 파일 검증
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("파일이 선택되지 않았습니다.");
|
||||
}
|
||||
|
||||
// 파일 확장자 검증
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".txt")) {
|
||||
throw new IllegalArgumentException("TXT 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename);
|
||||
}
|
||||
|
||||
// 파일 크기 검증 (50MB 제한)
|
||||
if (file.getSize() > 50 * 1024 * 1024) {
|
||||
throw new IllegalArgumentException("파일 크기는 50MB를 초과할 수 없습니다. 파일 크기: " + (file.getSize() / 1024 / 1024) + "MB");
|
||||
}
|
||||
|
||||
log.info("TXT 파일 업로드 시작 - 파일명: {}, 크기: {} bytes", originalFilename, file.getSize());
|
||||
|
||||
// 설정된 인코딩으로 파일 읽기
|
||||
String encoding = parseConfig.getEncoding();
|
||||
log.info("파일 인코딩: {}, 한글 바이트 크기: {}", encoding, parseConfig.getHangulByteSize());
|
||||
|
||||
List<String> allLines = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(file.getInputStream(), encoding))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
allLines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 최소 라인 검증 (헤더 10라인 + 데이터 최소 2라인)
|
||||
if (allLines.size() < 12) {
|
||||
throw new IllegalArgumentException("파일 형식이 올바르지 않습니다. 최소 12라인 이상이어야 합니다. 현재 라인 수: " + allLines.size());
|
||||
}
|
||||
|
||||
log.info("파일 읽기 완료 - 총 라인 수: {}", allLines.size());
|
||||
|
||||
// 11번째 라인부터 데이터 처리 (인덱스 10부터 시작)
|
||||
for (int i = 10; i < allLines.size(); i++) {
|
||||
String firstLine = allLines.get(i);
|
||||
|
||||
// 빈 줄 스킵
|
||||
if (firstLine.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 구분선 스킵
|
||||
if (firstLine.trim().startsWith("---")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 다음 줄 확인 (2줄 1세트이므로)
|
||||
if (i + 1 >= allLines.size()) {
|
||||
String errorMsg = String.format("[라인 %d] 데이터가 불완전합니다. 2줄 1세트 형식이 필요합니다.", i + 1);
|
||||
errorMessages.add(errorMsg);
|
||||
// 한 건이라도 실패하면 전체 롤백
|
||||
throw new RuntimeException(buildErrorMessage(errorMessages));
|
||||
}
|
||||
|
||||
String secondLine = allLines.get(i + 1);
|
||||
dataLineNumber++;
|
||||
|
||||
// 고정폭 파싱
|
||||
CarFfnlgTrgtVO vo = parseFixedWidthData(firstLine, secondLine, dataLineNumber, errorMessages);
|
||||
|
||||
if (vo == null) {
|
||||
// 파싱 실패 시 전체 롤백
|
||||
throw new RuntimeException(buildErrorMessage(errorMessages));
|
||||
}
|
||||
|
||||
// 필수 필드 검증
|
||||
List<String> validationErrors = validateParsedData(dataLineNumber, vo);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
errorMessages.addAll(validationErrors);
|
||||
// 검증 실패 시 전체 롤백
|
||||
throw new RuntimeException(buildErrorMessage(errorMessages));
|
||||
}
|
||||
|
||||
// 차량번호 중복 체크
|
||||
CarFfnlgTrgtVO checkVO = new CarFfnlgTrgtVO();
|
||||
checkVO.setVhclno(vo.getVhclno());
|
||||
checkVO.setInspYmd(vo.getInspYmd());
|
||||
int duplicateCount = mapper.checkDuplicateVhclno(checkVO);
|
||||
if (duplicateCount > 0) {
|
||||
String errorMsg = String.format("[데이터 %d] 중복된 차량번호입니다. 차량번호: %s, 검사일자: %s",
|
||||
dataLineNumber, vo.getVhclno(), vo.getInspYmd());
|
||||
errorMessages.add(errorMsg);
|
||||
// 중복 체크 실패 시 전체 롤백
|
||||
throw new RuntimeException(buildErrorMessage(errorMessages));
|
||||
}
|
||||
|
||||
// 업무 처리 상태 및 등록자 설정
|
||||
vo.setTaskPrcsSttsCd("01"); // 01=접수
|
||||
vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
|
||||
vo.setRcptYmd(LocalDate.now().format(DATE_FORMATTER)); // 접수일자는 현재 날짜
|
||||
vo.setRgtr(rgtr);
|
||||
|
||||
// DB 저장
|
||||
int insertResult = mapper.insert(vo);
|
||||
|
||||
if (insertResult > 0) {
|
||||
successCount++;
|
||||
log.debug("데이터 저장 성공 [데이터 {}] - 차량번호: {}", dataLineNumber, vo.getVhclno());
|
||||
} else {
|
||||
String errorMsg = String.format("[데이터 %d] 데이터 저장 실패 - 차량번호: %s", dataLineNumber, vo.getVhclno());
|
||||
errorMessages.add(errorMsg);
|
||||
// DB 저장 실패 시 전체 롤백
|
||||
throw new RuntimeException(buildErrorMessage(errorMessages));
|
||||
}
|
||||
|
||||
// 2줄 1세트이므로 다음 줄 건너뛰기
|
||||
i++;
|
||||
}
|
||||
|
||||
log.info("TXT 파일 처리 완료 - 성공: {}건", successCount);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", 0);
|
||||
result.put("errorMessages", errorMessages);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 파일 검증 오류는 롤백 대상이 아님 (트랜잭션 시작 전 오류)
|
||||
log.error("파일 검증 중 오류 발생: {}", e.getMessage());
|
||||
errorMessages.add(e.getMessage());
|
||||
result.put("success", false);
|
||||
result.put("successCount", 0);
|
||||
result.put("failCount", 0);
|
||||
result.put("errorMessages", errorMessages);
|
||||
} catch (RuntimeException e) {
|
||||
// 데이터 처리 중 오류 발생 시 전체 롤백
|
||||
log.error("TXT 파일 업로드 중 오류 발생 - 전체 롤백 처리", e);
|
||||
throw e; // 트랜잭션 롤백을 위해 예외 재발생
|
||||
} catch (Exception e) {
|
||||
log.error("TXT 파일 업로드 중 예상치 못한 오류 발생", e);
|
||||
errorMessages.add("파일 업로드 중 오류가 발생했습니다: " + e.getMessage());
|
||||
throw new MessageException(buildErrorMessage(errorMessages), e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 메시지 목록을 하나의 문자열로 조합
|
||||
*/
|
||||
private String buildErrorMessage(List<String> errorMessages) {
|
||||
if (errorMessages.isEmpty()) {
|
||||
return "파일 업로드 중 오류가 발생했습니다.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("파일 업로드 실패 - 전체 롤백 처리되었습니다.\n\n");
|
||||
sb.append("[오류 상세 내역]\n");
|
||||
|
||||
// 최대 10개의 오류 메시지만 표시
|
||||
int displayCount = Math.min(errorMessages.size(), 10);
|
||||
for (int i = 0; i < displayCount; i++) {
|
||||
sb.append(errorMessages.get(i)).append("\n");
|
||||
}
|
||||
|
||||
if (errorMessages.size() > 10) {
|
||||
sb.append("... 외 ").append(errorMessages.size() - 10).append("건\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 기반 바이트 단위 고정폭 데이터 파싱 (2줄 1세트)
|
||||
*
|
||||
* application.yml의 car-ffnlg-txt-parse 설정을 기반으로 동적 파싱
|
||||
* - 인코딩: UTF-8, EUC-KR, MS949 등 설정 가능
|
||||
* - 한글 바이트 크기: UTF-8=3바이트, EUC-KR/MS949=2바이트
|
||||
* - 각 필드별 바이트 길이: yml 설정에서 변경 가능 (길이만 지정, -1=끝까지)
|
||||
*/
|
||||
private CarFfnlgTrgtVO parseFixedWidthData(String firstLine, String secondLine,
|
||||
int dataLineNumber, List<String> errorMessages) {
|
||||
try {
|
||||
CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
|
||||
|
||||
// 설정된 인코딩 가져오기
|
||||
String encoding = parseConfig.getEncoding();
|
||||
log.info("[DEBUG_LOG] ====== 데이터 {} 파싱 시작 (인코딩: {}, 한글바이트: {}) ======",
|
||||
dataLineNumber, encoding, parseConfig.getHangulByteSize());
|
||||
log.info("[DEBUG_LOG] 첫째줄 원본: [{}]", firstLine);
|
||||
log.info("[DEBUG_LOG] 둘째줄 원본: [{}]", secondLine);
|
||||
|
||||
// 첫 번째 줄 바이트 단위 파싱 (길이 기반)
|
||||
byte[] firstBytes = firstLine.getBytes(encoding);
|
||||
log.info("[DEBUG_LOG] 첫째줄 바이트 길이: {}", firstBytes.length);
|
||||
|
||||
int pos = 0; // 현재 바이트 위치
|
||||
|
||||
// 검사소 (8바이트)
|
||||
int len = parseConfig.getFirstLineLength("inspstn-cd");
|
||||
String inspstnCd = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 검사소(inspstnCd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspstnCd);
|
||||
pos += len;
|
||||
|
||||
// 검사일자 (12바이트)
|
||||
len = parseConfig.getFirstLineLength("insp-ymd");
|
||||
String inspYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 검사일자(inspYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspYmd);
|
||||
pos += len;
|
||||
|
||||
// 차량번호 (13바이트)
|
||||
len = parseConfig.getFirstLineLength("vhclno");
|
||||
String vhclno = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 차량번호(vhclno) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vhclno);
|
||||
pos += len;
|
||||
|
||||
// 소유자명 (31바이트)
|
||||
len = parseConfig.getFirstLineLength("ownr-nm");
|
||||
String ownrNm = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 소유자명(ownrNm) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, ownrNm);
|
||||
pos += len;
|
||||
|
||||
// 주민번호 (15바이트)
|
||||
len = parseConfig.getFirstLineLength("rrno");
|
||||
String rrno = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 주민번호(rrno) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, rrno);
|
||||
pos += len;
|
||||
|
||||
// 차명 (16바이트)
|
||||
len = parseConfig.getFirstLineLength("car-nm");
|
||||
String carNm = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 차명(carNm) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carNm);
|
||||
pos += len;
|
||||
|
||||
// 차종 (25바이트)
|
||||
len = parseConfig.getFirstLineLength("car-knd");
|
||||
String carKnd = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 차종(carKnd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carKnd);
|
||||
pos += len;
|
||||
|
||||
// 용도 (12바이트)
|
||||
len = parseConfig.getFirstLineLength("car-usg");
|
||||
String carUsg = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 용도(carUsg) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carUsg);
|
||||
pos += len;
|
||||
|
||||
// 종료일 (12바이트)
|
||||
len = parseConfig.getFirstLineLength("insp-end-ymd");
|
||||
String inspEndYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 종료일(inspEndYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspEndYmd);
|
||||
pos += len;
|
||||
|
||||
// 일수 (8바이트)
|
||||
len = parseConfig.getFirstLineLength("daycnt");
|
||||
String daycnt = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 일수(daycnt) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, daycnt);
|
||||
pos += len;
|
||||
|
||||
// 과태료 (-1=끝까지)
|
||||
len = parseConfig.getFirstLineLength("ffnlg-amt");
|
||||
String ffnlgAmt = extractByteLength(firstBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 과태료(ffnlgAmt) [{}바이트, 위치 {}~끝] = [{}]", len, pos, ffnlgAmt);
|
||||
|
||||
// 두 번째 줄 바이트 단위 파싱 (길이 기반)
|
||||
byte[] secondBytes = secondLine.getBytes(encoding);
|
||||
log.info("[DEBUG_LOG] 둘째줄 바이트 길이: {}", secondBytes.length);
|
||||
|
||||
pos = 0; // 위치 초기화
|
||||
|
||||
// 공백 스킵 (8바이트)
|
||||
len = parseConfig.getSecondLineLength("skip");
|
||||
log.info("[DEBUG_LOG] 공백 스킵 [{}바이트]", len);
|
||||
pos += len;
|
||||
|
||||
// 최종등록일 (12바이트)
|
||||
len = parseConfig.getSecondLineLength("last-reg-ymd");
|
||||
String lastRegYmd = extractByteLength(secondBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 최종등록일(lastRegYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, lastRegYmd);
|
||||
pos += len;
|
||||
|
||||
// 주소 (88바이트)
|
||||
len = parseConfig.getSecondLineLength("addr");
|
||||
String addr = extractByteLength(secondBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 주소(addr) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, addr);
|
||||
pos += len;
|
||||
|
||||
// 유효기간만료일 (12바이트)
|
||||
len = parseConfig.getSecondLineLength("vld-prd-expry-ymd");
|
||||
String vldPrdExpryYmd = extractByteLength(secondBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 유효기간만료일(vldPrdExpryYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vldPrdExpryYmd);
|
||||
pos += len;
|
||||
|
||||
// 매매상품 (-1=끝까지)
|
||||
len = parseConfig.getSecondLineLength("trd-gds");
|
||||
String trdGds = extractByteLength(secondBytes, pos, len, encoding).trim();
|
||||
log.info("[DEBUG_LOG] 매매상품(trdGds) [{}바이트, 위치 {}~끝] = [{}]", len, pos, trdGds);
|
||||
|
||||
// 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
|
||||
inspYmd = convertDateFormat(inspYmd);
|
||||
inspEndYmd = convertDateFormat(inspEndYmd);
|
||||
lastRegYmd = convertDateFormat(lastRegYmd);
|
||||
vldPrdExpryYmd = convertDateFormat(vldPrdExpryYmd);
|
||||
|
||||
log.info("[DEBUG_LOG] 날짜 변환 후 - inspYmd: [{}], inspEndYmd: [{}], lastRegYmd: [{}], vldPrdExpryYmd: [{}]",
|
||||
inspYmd, inspEndYmd, lastRegYmd, vldPrdExpryYmd);
|
||||
|
||||
// 과태료 금액 숫자만 추출 (예: "30만원" -> "300000")
|
||||
ffnlgAmt = extractNumericAmount(ffnlgAmt);
|
||||
log.info("[DEBUG_LOG] 과태료 변환 후: [{}]", ffnlgAmt);
|
||||
|
||||
// VO 설정
|
||||
vo.setInspstnCd(inspstnCd);
|
||||
vo.setInspYmd(inspYmd);
|
||||
vo.setVhclno(vhclno);
|
||||
vo.setOwnrNm(ownrNm);
|
||||
vo.setRrno(rrno);
|
||||
vo.setCarNm(carNm);
|
||||
vo.setCarKnd(carKnd);
|
||||
vo.setCarUsg(carUsg);
|
||||
vo.setInspEndYmd(inspEndYmd);
|
||||
vo.setDaycnt(daycnt);
|
||||
vo.setFfnlgAmt(ffnlgAmt);
|
||||
vo.setLastRegYmd(lastRegYmd);
|
||||
vo.setAddr(addr);
|
||||
vo.setVldPrdExpryYmd(vldPrdExpryYmd);
|
||||
vo.setTrdGds(trdGds);
|
||||
|
||||
log.info("[DEBUG_LOG] ====== 데이터 {} 파싱 완료 ======", dataLineNumber);
|
||||
|
||||
return vo;
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMsg = String.format("[데이터 %d] 파싱 중 오류 발생 - %s", dataLineNumber, e.getMessage());
|
||||
errorMessages.add(errorMsg);
|
||||
log.error("데이터 {} 파싱 중 오류 - 첫째줄: {}, 둘째줄: {}", dataLineNumber, firstLine, secondLine, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 바이트 배열에서 지정된 위치부터 지정된 길이만큼 추출하여 문자열로 반환
|
||||
*
|
||||
* @param bytes 원본 바이트 배열
|
||||
* @param pos 시작 바이트 위치 (포함)
|
||||
* @param length 추출할 바이트 길이 (-1=끝까지)
|
||||
* @param encoding 인코딩 (UTF-8, EUC-KR, MS949 등)
|
||||
* @return 추출된 문자열
|
||||
*/
|
||||
private String extractByteLength(byte[] bytes, int pos, int length, String encoding) {
|
||||
try {
|
||||
// 위치 검증
|
||||
if (pos < 0) pos = 0;
|
||||
if (pos >= bytes.length) return "";
|
||||
|
||||
// 길이 계산 (-1이면 끝까지)
|
||||
int actualLength;
|
||||
if (length < 0) {
|
||||
actualLength = bytes.length - pos;
|
||||
} else {
|
||||
actualLength = Math.min(length, bytes.length - pos);
|
||||
}
|
||||
|
||||
if (actualLength <= 0) return "";
|
||||
|
||||
// 바이트 추출
|
||||
byte[] extracted = new byte[actualLength];
|
||||
System.arraycopy(bytes, pos, extracted, 0, actualLength);
|
||||
|
||||
// 지정된 인코딩으로 문자열 변환
|
||||
return new String(extracted, encoding);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("바이트 추출 중 오류 발생 - pos: {}, length: {}, encoding: {}", pos, length, encoding, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
|
||||
*/
|
||||
private String convertDateFormat(String dateStr) {
|
||||
if (dateStr == null || dateStr.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
// 하이픈 제거
|
||||
return dateStr.replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 과태료 금액에서 숫자만 추출
|
||||
* 예: "30만원" -> "300000", "56만원" -> "560000"
|
||||
*/
|
||||
private String extractNumericAmount(String amountStr) {
|
||||
if (amountStr == null || amountStr.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// "만원" 형식 처리
|
||||
if (amountStr.contains("만원")) {
|
||||
String numStr = amountStr.replace("만원", "").trim();
|
||||
try {
|
||||
int num = Integer.parseInt(numStr);
|
||||
return String.valueOf(num * 10000);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("과태료 금액 파싱 실패: {}", amountStr);
|
||||
return amountStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 숫자만 추출
|
||||
return amountStr.replaceAll("[^0-9]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 파싱된 데이터 유효성 검증
|
||||
*
|
||||
* 필수 필드: 검사소, 검사일자, 차량번호, 소유자명, 주민번호, 과태료금액
|
||||
* 선택 필드: 차명, 차종, 용도, 검사종료일자, 일수, 최종등록일, 주소, 유효기간만료일, 매매상품
|
||||
*/
|
||||
private List<String> validateParsedData(int dataLineNumber, CarFfnlgTrgtVO vo) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
String vhclno = vo.getVhclno() != null ? vo.getVhclno() : "알 수 없음";
|
||||
|
||||
// 1. 검사소 코드 검증
|
||||
if (vo.getInspstnCd() == null || vo.getInspstnCd().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 검사소 코드가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
|
||||
} else if (vo.getInspstnCd().length() > 8) {
|
||||
errors.add(String.format("[데이터 %d] 검사소 코드가 너무 깁니다. 검사소: %s (최대 8자), 차량번호: %s",
|
||||
dataLineNumber, vo.getInspstnCd(), vhclno));
|
||||
}
|
||||
|
||||
// 2. 검사일자 검증
|
||||
if (vo.getInspYmd() == null || vo.getInspYmd().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 검사일자가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
|
||||
} else if (!isValidDate(vo.getInspYmd())) {
|
||||
errors.add(String.format("[데이터 %d] 검사일자 형식이 올바르지 않습니다. 검사일자: %s (YYYYMMDD 형식이어야 함), 차량번호: %s",
|
||||
dataLineNumber, vo.getInspYmd(), vhclno));
|
||||
}
|
||||
|
||||
// 3. 차량번호 검증
|
||||
if (vo.getVhclno() == null || vo.getVhclno().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 차량번호가 누락되었습니다.", dataLineNumber));
|
||||
} else if (vo.getVhclno().length() > 30) {
|
||||
errors.add(String.format("[데이터 %d] 차량번호가 너무 깁니다. 차량번호: %s (최대 30자)",
|
||||
dataLineNumber, vo.getVhclno()));
|
||||
} else if (!isValidVehicleNumber(vo.getVhclno())) {
|
||||
errors.add(String.format("[데이터 %d] 차량번호 형식이 올바르지 않습니다. 차량번호: %s (예: 12가3456, 123가4567)",
|
||||
dataLineNumber, vo.getVhclno()));
|
||||
}
|
||||
|
||||
// 4. 소유자명 검증
|
||||
if (vo.getOwnrNm() == null || vo.getOwnrNm().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 소유자명이 누락되었습니다. 차량번호: %s",
|
||||
dataLineNumber, vhclno));
|
||||
} else if (vo.getOwnrNm().length() > 75) {
|
||||
errors.add(String.format("[데이터 %d] 소유자명이 너무 깁니다. 소유자명: %s (최대 75자), 차량번호: %s",
|
||||
dataLineNumber, vo.getOwnrNm(), vhclno));
|
||||
}
|
||||
|
||||
// 5. 주민등록번호 검증
|
||||
if (vo.getRrno() == null || vo.getRrno().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 주민등록번호가 누락되었습니다. 차량번호: %s, 소유자: %s",
|
||||
dataLineNumber, vhclno, vo.getOwnrNm()));
|
||||
} else if (vo.getRrno().length() > 100) {
|
||||
errors.add(String.format("[데이터 %d] 주민등록번호가 너무 깁니다. 주민번호 길이: %d (최대 100자), 차량번호: %s",
|
||||
dataLineNumber, vo.getRrno().length(), vhclno));
|
||||
} else if (!isValidRrno(vo.getRrno())) {
|
||||
errors.add(String.format("[데이터 %d] 주민등록번호 형식이 올바르지 않습니다. 주민번호: %s (13자리 숫자), 차량번호: %s",
|
||||
dataLineNumber, vo.getRrno(), vhclno));
|
||||
}
|
||||
|
||||
// 6. 과태료금액 검증
|
||||
if (vo.getFfnlgAmt() == null || vo.getFfnlgAmt().isEmpty()) {
|
||||
errors.add(String.format("[데이터 %d] 과태료금액이 누락되었습니다. 차량번호: %s, 소유자: %s",
|
||||
dataLineNumber, vhclno, vo.getOwnrNm()));
|
||||
} else if (!isNumeric(vo.getFfnlgAmt())) {
|
||||
errors.add(String.format("[데이터 %d] 과태료금액은 숫자여야 합니다. 과태료금액: %s, 차량번호: %s",
|
||||
dataLineNumber, vo.getFfnlgAmt(), vhclno));
|
||||
} else if (vo.getFfnlgAmt().length() > 10) {
|
||||
errors.add(String.format("[데이터 %d] 과태료금액이 너무 큽니다. 과태료금액: %s (최대 10자리), 차량번호: %s",
|
||||
dataLineNumber, vo.getFfnlgAmt(), vhclno));
|
||||
}
|
||||
|
||||
// 7. 선택 필드 길이 검증
|
||||
if (vo.getCarNm() != null && vo.getCarNm().length() > 100) {
|
||||
errors.add(String.format("[데이터 %d] 자동차명이 너무 깁니다. 자동차명: %s (최대 100자), 차량번호: %s",
|
||||
dataLineNumber, vo.getCarNm(), vhclno));
|
||||
}
|
||||
|
||||
if (vo.getCarKnd() != null && vo.getCarKnd().length() > 100) {
|
||||
errors.add(String.format("[데이터 %d] 자동차종류가 너무 깁니다. 자동차종류: %s (최대 100자), 차량번호: %s",
|
||||
dataLineNumber, vo.getCarKnd(), vhclno));
|
||||
}
|
||||
|
||||
if (vo.getCarUsg() != null && vo.getCarUsg().length() > 100) {
|
||||
errors.add(String.format("[데이터 %d] 자동차용도가 너무 깁니다. 자동차용도: %s (최대 100자), 차량번호: %s",
|
||||
dataLineNumber, vo.getCarUsg(), vhclno));
|
||||
}
|
||||
|
||||
if (vo.getAddr() != null && vo.getAddr().length() > 600) {
|
||||
errors.add(String.format("[데이터 %d] 주소가 너무 깁니다. 주소 길이: %d (최대 600자), 차량번호: %s",
|
||||
dataLineNumber, vo.getAddr().length(), vhclno));
|
||||
}
|
||||
|
||||
// 8. 선택 필드 날짜 검증 (값이 있는 경우에만)
|
||||
if (vo.getInspEndYmd() != null && !vo.getInspEndYmd().isEmpty() && !isValidDate(vo.getInspEndYmd())) {
|
||||
errors.add(String.format("[데이터 %d] 검사종료일자 형식이 올바르지 않습니다. 검사종료일자: %s (YYYYMMDD 형식), 차량번호: %s",
|
||||
dataLineNumber, vo.getInspEndYmd(), vhclno));
|
||||
}
|
||||
|
||||
if (vo.getLastRegYmd() != null && !vo.getLastRegYmd().isEmpty() && !isValidDate(vo.getLastRegYmd())) {
|
||||
errors.add(String.format("[데이터 %d] 최종등록일자 형식이 올바르지 않습니다. 최종등록일자: %s (YYYYMMDD 형식), 차량번호: %s",
|
||||
dataLineNumber, vo.getLastRegYmd(), vhclno));
|
||||
}
|
||||
|
||||
if (vo.getVldPrdExpryYmd() != null && !vo.getVldPrdExpryYmd().isEmpty() && !isValidDate(vo.getVldPrdExpryYmd())) {
|
||||
errors.add(String.format("[데이터 %d] 유효기간만료일자 형식이 올바르지 않습니다. 유효기간만료일자: %s (YYYYMMDD 형식), 차량번호: %s",
|
||||
dataLineNumber, vo.getVldPrdExpryYmd(), vhclno));
|
||||
}
|
||||
|
||||
// 9. 일수 검증 (값이 있는 경우에만)
|
||||
if (vo.getDaycnt() != null && !vo.getDaycnt().isEmpty() && !isNumeric(vo.getDaycnt())) {
|
||||
errors.add(String.format("[데이터 %d] 일수는 숫자여야 합니다. 일수: %s, 차량번호: %s",
|
||||
dataLineNumber, vo.getDaycnt(), vhclno));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 날짜 유효성 검증 (YYYYMMDD 형식)
|
||||
* @param dateStr 날짜 문자열
|
||||
* @return 유효성 여부
|
||||
*/
|
||||
private boolean isValidDate(String dateStr) {
|
||||
if (dateStr == null || dateStr.length() != 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LocalDate.parse(dateStr, DATE_FORMATTER);
|
||||
return true;
|
||||
} catch (DateTimeParseException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 여부 검증
|
||||
* @param str 문자열
|
||||
* @return 숫자 여부
|
||||
*/
|
||||
private boolean isNumeric(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Long.parseLong(str);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량번호 형식 검증
|
||||
*
|
||||
* 유효한 형식:
|
||||
* - 일반: 12가3456, 123가4567 (2~3자리 숫자 + 한글 + 4자리 숫자)
|
||||
* - 특수: 경기11사2222 (지역명 + 숫자 + 한글 + 숫자)
|
||||
*
|
||||
* @param vhclno 차량번호
|
||||
* @return 유효성 여부
|
||||
*/
|
||||
private boolean isValidVehicleNumber(String vhclno) {
|
||||
if (vhclno == null || vhclno.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 공백 제거
|
||||
vhclno = vhclno.trim();
|
||||
|
||||
// 최소 길이 체크 (예: 12가3456 = 7자)
|
||||
if (vhclno.length() < 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 패턴1: 일반 차량번호 (2~3자리 숫자 + 한글 1자 + 4자리 숫자)
|
||||
// 예: 12가3456, 123가4567
|
||||
if (vhclno.matches("\\d{2,3}[가-힣]\\d{4}")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 패턴2: 지역명이 포함된 차량번호 (한글 + 숫자 + 한글 + 숫자)
|
||||
// 예: 경기11사2222, 서울12가3456
|
||||
if (vhclno.matches("[가-힣]+\\d+[가-힣]\\d+")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 패턴3: 특수 차량번호 (숫자 + 한글 + 숫자)
|
||||
// 예: 22고2222, 33마3333
|
||||
if (vhclno.matches("\\d+[가-힣]+\\d+")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 그 외의 경우는 일단 허용 (실제 차량번호 형식이 다양할 수 있음)
|
||||
// 최소한 한글과 숫자가 모두 포함되어 있는지 확인
|
||||
return vhclno.matches(".*[가-힣].*") && vhclno.matches(".*\\d.*");
|
||||
}
|
||||
|
||||
/**
|
||||
* 주민등록번호 형식 검증
|
||||
*
|
||||
* 유효한 형식:
|
||||
* - 13자리 숫자 (예: 1234567890123)
|
||||
* - 6자리-7자리 형식도 허용 (예: 123456-1234567)
|
||||
*
|
||||
* @param rrno 주민등록번호
|
||||
* @return 유효성 여부
|
||||
*/
|
||||
private boolean isValidRrno(String rrno) {
|
||||
if (rrno == null || rrno.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 공백 제거
|
||||
rrno = rrno.trim();
|
||||
|
||||
// 하이픈 제거
|
||||
String rrnoDigits = rrno.replace("-", "");
|
||||
|
||||
// 13자리 숫자인지 확인
|
||||
if (rrnoDigits.length() == 13 && rrnoDigits.matches("\\d{13}")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 10자리 이상 숫자로 구성된 경우도 허용 (사업자등록번호 등)
|
||||
if (rrnoDigits.length() >= 10 && rrnoDigits.matches("\\d+")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper">
|
||||
|
||||
<!-- 과태료 대상 목록 총 개수 조회 -->
|
||||
<select id="selectListTotalCount" parameterType="CarFfnlgTrgtVO" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM tb_car_ffnlg_trgt t
|
||||
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
|
||||
LEFT JOIN (
|
||||
SELECT CD_ID, CD_NM
|
||||
FROM tb_cd_detail
|
||||
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
|
||||
AND USE_YN = 'Y'
|
||||
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
|
||||
WHERE t.DEL_DT IS NULL
|
||||
<if test='schRcptYmdStart != null and schRcptYmdStart != ""'>
|
||||
AND t.RCPT_YMD >= #{schRcptYmdStart}
|
||||
</if>
|
||||
<if test='schRcptYmdEnd != null and schRcptYmdEnd != ""'>
|
||||
AND t.RCPT_YMD <= #{schRcptYmdEnd}
|
||||
</if>
|
||||
<if test='schVhclno != null and schVhclno != ""'>
|
||||
AND t.VHCLNO LIKE CONCAT('%', #{schVhclno}, '%')
|
||||
</if>
|
||||
<if test='schOwnrNm != null and schOwnrNm != ""'>
|
||||
AND t.OWNR_NM LIKE CONCAT('%', #{schOwnrNm}, '%')
|
||||
</if>
|
||||
<if test='schTaskPrcsSttsCd != null and schTaskPrcsSttsCd != ""'>
|
||||
AND t.TASK_PRCS_STTS_CD = #{schTaskPrcsSttsCd}
|
||||
</if>
|
||||
<if test='schInspYmdStart != null and schInspYmdStart != ""'>
|
||||
AND t.INSP_YMD >= #{schInspYmdStart}
|
||||
</if>
|
||||
<if test='schInspYmdEnd != null and schInspYmdEnd != ""'>
|
||||
AND t.INSP_YMD <= #{schInspYmdEnd}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 과태료 대상 목록 조회 -->
|
||||
<select id="selectList" parameterType="CarFfnlgTrgtVO" resultType="CarFfnlgTrgtVO">
|
||||
SELECT
|
||||
t.CAR_FFNLG_TRGT_ID AS carFfnlgTrgtId,
|
||||
t.RCPT_YMD AS rcptYmd,
|
||||
t.FFNLG_TRGT_SE_CD AS ffnlgTrgtSeCd,
|
||||
t.INSPSTN_CD AS inspstnCd,
|
||||
t.INSP_YMD AS inspYmd,
|
||||
t.VHCLNO AS vhclno,
|
||||
t.OWNR_NM AS ownrNm,
|
||||
t.RRNO AS rrno,
|
||||
t.CAR_NM AS carNm,
|
||||
t.CAR_KND AS carKnd,
|
||||
t.CAR_USG AS carUsg,
|
||||
t.INSP_END_YMD AS inspEndYmd,
|
||||
t.DAYCNT AS daycnt,
|
||||
t.FFNLG_AMT AS ffnlgAmt,
|
||||
t.LAST_REG_YMD AS lastRegYmd,
|
||||
t.ADDR AS addr,
|
||||
t.VLD_PRD_EXPRY_YMD AS vldPrdExpryYmd,
|
||||
t.TRD_GDS AS trdGds,
|
||||
t.TASK_PRCS_STTS_CD AS taskPrcsSttsCd,
|
||||
t.TASK_PRCS_YMD AS taskPrcsYmd,
|
||||
t.RMRK AS rmrk,
|
||||
t.CAR_BASS_MATTER_INQIRE_ID AS carBassMatterInqireId,
|
||||
t.CAR_LEDGER_FRMBK_ID AS carLedgerFrmbkId,
|
||||
t.REG_DT AS regDt,
|
||||
t.RGTR AS rgtr,
|
||||
t.DEL_DT AS delDt,
|
||||
t.DLTR AS dltr,
|
||||
cd.CD_NM AS taskPrcsSttsCdNm,
|
||||
u.USER_NM AS rgtrNm
|
||||
FROM tb_car_ffnlg_trgt t
|
||||
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
|
||||
LEFT JOIN (
|
||||
SELECT CD_ID, CD_NM
|
||||
FROM tb_cd_detail
|
||||
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
|
||||
AND USE_YN = 'Y'
|
||||
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
|
||||
WHERE t.DEL_DT IS NULL
|
||||
<if test='schRcptYmdStart != null and schRcptYmdStart != ""'>
|
||||
AND t.RCPT_YMD >= #{schRcptYmdStart}
|
||||
</if>
|
||||
<if test='schRcptYmdEnd != null and schRcptYmdEnd != ""'>
|
||||
AND t.RCPT_YMD <= #{schRcptYmdEnd}
|
||||
</if>
|
||||
<if test='schVhclno != null and schVhclno != ""'>
|
||||
AND t.VHCLNO LIKE CONCAT('%', #{schVhclno}, '%')
|
||||
</if>
|
||||
<if test='schOwnrNm != null and schOwnrNm != ""'>
|
||||
AND t.OWNR_NM LIKE CONCAT('%', #{schOwnrNm}, '%')
|
||||
</if>
|
||||
<if test='schTaskPrcsSttsCd != null and schTaskPrcsSttsCd != ""'>
|
||||
AND t.TASK_PRCS_STTS_CD = #{schTaskPrcsSttsCd}
|
||||
</if>
|
||||
<if test='schInspYmdStart != null and schInspYmdStart != ""'>
|
||||
AND t.INSP_YMD >= #{schInspYmdStart}
|
||||
</if>
|
||||
<if test='schInspYmdEnd != null and schInspYmdEnd != ""'>
|
||||
AND t.INSP_YMD <= #{schInspYmdEnd}
|
||||
</if>
|
||||
ORDER BY t.REG_DT DESC
|
||||
<if test='pagingYn == "Y"'>
|
||||
limit #{startIndex}, #{perPage} /* 서버사이드 페이징 처리 */
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 과태료 대상 상세 조회 -->
|
||||
<select id="selectOne" parameterType="CarFfnlgTrgtVO" resultType="CarFfnlgTrgtVO">
|
||||
SELECT
|
||||
t.CAR_FFNLG_TRGT_ID AS carFfnlgTrgtId,
|
||||
t.RCPT_YMD AS rcptYmd,
|
||||
t.FFNLG_TRGT_SE_CD AS ffnlgTrgtSeCd,
|
||||
t.INSPSTN_CD AS inspstnCd,
|
||||
t.INSP_YMD AS inspYmd,
|
||||
t.VHCLNO AS vhclno,
|
||||
t.OWNR_NM AS ownrNm,
|
||||
t.RRNO AS rrno,
|
||||
t.CAR_NM AS carNm,
|
||||
t.CAR_KND AS carKnd,
|
||||
t.CAR_USG AS carUsg,
|
||||
t.INSP_END_YMD AS inspEndYmd,
|
||||
t.DAYCNT AS daycnt,
|
||||
t.FFNLG_AMT AS ffnlgAmt,
|
||||
t.LAST_REG_YMD AS lastRegYmd,
|
||||
t.ADDR AS addr,
|
||||
t.VLD_PRD_EXPRY_YMD AS vldPrdExpryYmd,
|
||||
t.TRD_GDS AS trdGds,
|
||||
t.TASK_PRCS_STTS_CD AS taskPrcsSttsCd,
|
||||
t.TASK_PRCS_YMD AS taskPrcsYmd,
|
||||
t.RMRK AS rmrk,
|
||||
t.CAR_BASS_MATTER_INQIRE_ID AS carBassMatterInqireId,
|
||||
t.CAR_LEDGER_FRMBK_ID AS carLedgerFrmbkId,
|
||||
t.REG_DT AS regDt,
|
||||
t.RGTR AS rgtr,
|
||||
t.DEL_DT AS delDt,
|
||||
t.DLTR AS dltr,
|
||||
cd.CD_NM AS taskPrcsSttsCdNm,
|
||||
u.USER_NM AS rgtrNm
|
||||
FROM tb_car_ffnlg_trgt t
|
||||
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
|
||||
LEFT JOIN (
|
||||
SELECT CD_ID, CD_NM
|
||||
FROM tb_cd_detail
|
||||
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
|
||||
AND USE_YN = 'Y'
|
||||
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
|
||||
WHERE t.CAR_FFNLG_TRGT_ID = #{carFfnlgTrgtId}
|
||||
AND t.DEL_DT IS NULL
|
||||
</select>
|
||||
|
||||
<!-- 과태료 대상 등록 -->
|
||||
<insert id="insert" parameterType="CarFfnlgTrgtVO">
|
||||
INSERT INTO tb_car_ffnlg_trgt (
|
||||
CAR_FFNLG_TRGT_ID,
|
||||
RCPT_YMD,
|
||||
FFNLG_TRGT_SE_CD,
|
||||
INSPSTN_CD,
|
||||
INSP_YMD,
|
||||
VHCLNO,
|
||||
OWNR_NM,
|
||||
RRNO,
|
||||
CAR_NM,
|
||||
CAR_KND,
|
||||
CAR_USG,
|
||||
INSP_END_YMD,
|
||||
DAYCNT,
|
||||
FFNLG_AMT,
|
||||
LAST_REG_YMD,
|
||||
ADDR,
|
||||
VLD_PRD_EXPRY_YMD,
|
||||
TRD_GDS,
|
||||
TASK_PRCS_STTS_CD,
|
||||
TASK_PRCS_YMD,
|
||||
RMRK,
|
||||
CAR_BASS_MATTER_INQIRE_ID,
|
||||
CAR_LEDGER_FRMBK_ID,
|
||||
REG_DT,
|
||||
RGTR
|
||||
) VALUES (
|
||||
CONCAT('CFT', LPAD(NEXTVAL(seq_car_ffnlg_trgt_id), 17, '0')),
|
||||
#{rcptYmd},
|
||||
#{ffnlgTrgtSeCd},
|
||||
#{inspstnCd},
|
||||
#{inspYmd},
|
||||
#{vhclno},
|
||||
#{ownrNm},
|
||||
#{rrno},
|
||||
#{carNm},
|
||||
#{carKnd},
|
||||
#{carUsg},
|
||||
#{inspEndYmd},
|
||||
#{daycnt},
|
||||
#{ffnlgAmt},
|
||||
#{lastRegYmd},
|
||||
#{addr},
|
||||
#{vldPrdExpryYmd},
|
||||
#{trdGds},
|
||||
#{taskPrcsSttsCd},
|
||||
#{taskPrcsYmd},
|
||||
#{rmrk},
|
||||
#{carBassMatterInqireId},
|
||||
#{carLedgerFrmbkId},
|
||||
NOW(),
|
||||
#{rgtr}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 과태료 대상 수정 -->
|
||||
<update id="update" parameterType="CarFfnlgTrgtVO">
|
||||
UPDATE tb_car_ffnlg_trgt
|
||||
SET TASK_PRCS_STTS_CD = #{taskPrcsSttsCd},
|
||||
TASK_PRCS_YMD = #{taskPrcsYmd},
|
||||
RMRK = #{rmrk},
|
||||
CAR_BASS_MATTER_INQIRE_ID = #{carBassMatterInqireId},
|
||||
CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId}
|
||||
WHERE CAR_FFNLG_TRGT_ID = #{carFfnlgTrgtId}
|
||||
AND DEL_DT IS NULL
|
||||
</update>
|
||||
|
||||
<!-- 과태료 대상 삭제 (논리삭제) -->
|
||||
<update id="delete" parameterType="CarFfnlgTrgtVO">
|
||||
UPDATE tb_car_ffnlg_trgt
|
||||
SET DEL_DT = NOW(),
|
||||
DLTR = #{dltr}
|
||||
WHERE CAR_FFNLG_TRGT_ID = #{carFfnlgTrgtId}
|
||||
AND DEL_DT IS NULL
|
||||
</update>
|
||||
|
||||
<!-- 차량번호 중복 체크 -->
|
||||
<select id="checkDuplicateVhclno" parameterType="CarFfnlgTrgtVO" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM tb_car_ffnlg_trgt
|
||||
WHERE VHCLNO = #{vhclno}
|
||||
AND INSP_YMD = #{inspYmd}
|
||||
AND DEL_DT IS NULL
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,437 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
|
||||
|
||||
<!-- Main body -->
|
||||
<div class="main_body">
|
||||
<section id="section8" class="main_bars">
|
||||
<div class="bgs-main">
|
||||
<section id="section5">
|
||||
<div class="sub_title">과태료 대상 목록</div>
|
||||
<button type="button" id="registerBtn" class="newbtn bg1">등록</button>
|
||||
<button type="button" id="deleteBtn" class="newbtn bg3">삭제</button>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<div class="contants_body">
|
||||
<div class="gs_b_top">
|
||||
<ul class="lef">
|
||||
<li class="th">접수일자</li>
|
||||
<li>
|
||||
<input type="text" id="schRcptYmdStart" name="schRcptYmdStart" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/> ~
|
||||
<input type="text" id="schRcptYmdEnd" name="schRcptYmdEnd" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/>
|
||||
</li>
|
||||
<li class="th">검사일자</li>
|
||||
<li>
|
||||
<input type="text" id="schInspYmdStart" name="schInspYmdStart" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/> ~
|
||||
<input type="text" id="schInspYmdEnd" name="schInspYmdEnd" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/>
|
||||
</li>
|
||||
<li class="th">차량번호</li>
|
||||
<li>
|
||||
<input type="text" id="schVhclno" name="schVhclno" class="input" style="width: 150px;" maxlength="30" autocomplete="off" placeholder="예: 12가3456"/>
|
||||
</li>
|
||||
<li class="th">소유자명</li>
|
||||
<li>
|
||||
<input type="text" id="schOwnrNm" name="schOwnrNm" class="input" style="width: 150px;" maxlength="75" autocomplete="off"/>
|
||||
</li>
|
||||
<li class="th">처리상태</li>
|
||||
<li>
|
||||
<select id="schTaskPrcsSttsCd" name="schTaskPrcsSttsCd" class="input" style="width: 120px;">
|
||||
<option value="">전체</option>
|
||||
<c:forEach var="code" items="${taskPrcsSttsCdList}">
|
||||
<option value="${code.cdId}">${code.cdNm}</option>
|
||||
</c:forEach>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="rig2">
|
||||
<li><button type="button" id="search_btn" class="newbtnss bg1">검색</button></li>
|
||||
<li><button type="button" id="reset_btn" class="newbtnss bg5" style="margin-left: 5px;">초기화</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="gs_booking">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="box_column">
|
||||
<ul class="box_title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<li class="tit">과태료 대상 목록</li>
|
||||
<li class="rig">
|
||||
<span id="totalCount" class="total-count" style="padding-left: 25px;padding-right: 25px;">총 0건</span>
|
||||
<select id="perPageSelect" class="input" style="width: 112px;">
|
||||
<option value="15">페이지당 15</option>
|
||||
<option value="50">페이지당 50</option>
|
||||
<option value="100">페이지당 100</option>
|
||||
</select>
|
||||
<span class="page_number"><span id="currentPage"></span><span class="bar">/</span><span id="totalPages"></span> Pages</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="containers">
|
||||
<div id="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Main body -->
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 관리 모듈
|
||||
*/
|
||||
(function(window, $) {
|
||||
'use strict';
|
||||
|
||||
var SEARCH_COND = {};
|
||||
|
||||
// 페이징 정보를 저장할 전역 변수
|
||||
var GRID_PAGINATION_INFO = {
|
||||
totalCount: 0,
|
||||
page: 0,
|
||||
perPage: 0
|
||||
};
|
||||
|
||||
// 검색정보 설정
|
||||
var setSearchCond = function() {
|
||||
var schRcptYmdStart = $.trim(nvl($("#schRcptYmdStart").val(), ""));
|
||||
var schRcptYmdEnd = $.trim(nvl($("#schRcptYmdEnd").val(), ""));
|
||||
var schInspYmdStart = $.trim(nvl($("#schInspYmdStart").val(), ""));
|
||||
var schInspYmdEnd = $.trim(nvl($("#schInspYmdEnd").val(), ""));
|
||||
var schVhclno = $.trim(nvl($("#schVhclno").val(), ""));
|
||||
var schOwnrNm = $.trim(nvl($("#schOwnrNm").val(), ""));
|
||||
var schTaskPrcsSttsCd = $.trim(nvl($("#schTaskPrcsSttsCd").val(), ""));
|
||||
|
||||
SEARCH_COND.schRcptYmdStart = schRcptYmdStart.replace(/-/g, '');
|
||||
SEARCH_COND.schRcptYmdEnd = schRcptYmdEnd.replace(/-/g, '');
|
||||
SEARCH_COND.schInspYmdStart = schInspYmdStart.replace(/-/g, '');
|
||||
SEARCH_COND.schInspYmdEnd = schInspYmdEnd.replace(/-/g, '');
|
||||
SEARCH_COND.schVhclno = schVhclno;
|
||||
SEARCH_COND.schOwnrNm = schOwnrNm;
|
||||
SEARCH_COND.schTaskPrcsSttsCd = schTaskPrcsSttsCd;
|
||||
};
|
||||
|
||||
/**
|
||||
* 과태료 대상 목록 관리 네임스페이스
|
||||
*/
|
||||
var CarFfnlgTrgtList = {
|
||||
/**
|
||||
* 선택된 행 정보
|
||||
*/
|
||||
selectedRow: null,
|
||||
|
||||
/**
|
||||
* 그리드 관련 객체
|
||||
*/
|
||||
grid: {
|
||||
/**
|
||||
* 그리드 인스턴스
|
||||
*/
|
||||
instance: null,
|
||||
|
||||
/**
|
||||
* 그리드 설정 초기화
|
||||
* @returns {Object} 그리드 설정 객체
|
||||
*/
|
||||
initConfig: function() {
|
||||
// 데이터 소스 설정
|
||||
var dataSource = this.createDataSource();
|
||||
|
||||
// 현재 선택된 perPage 값 가져오기
|
||||
var perPage = parseInt($('#perPageSelect').val() || 15, 10);
|
||||
|
||||
// 그리드 설정 객체 생성
|
||||
var gridConfig = new XitTuiGridConfig();
|
||||
|
||||
// 기본 설정
|
||||
gridConfig.setOptDataSource(dataSource);
|
||||
gridConfig.setOptGridId('grid');
|
||||
gridConfig.setOptGridHeight(470);
|
||||
gridConfig.setOptRowHeight(30);
|
||||
gridConfig.setOptRowHeaderType('checkbox');
|
||||
gridConfig.setOptUseClientSort(false);
|
||||
|
||||
// 페이징 옵션 설정
|
||||
gridConfig.setOptPageOptions({
|
||||
useClient: false,
|
||||
perPage: perPage
|
||||
});
|
||||
|
||||
gridConfig.setOptColumns(this.getGridColumns());
|
||||
|
||||
return gridConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* 그리드 컬럼 정의
|
||||
* @returns {Array} 그리드 컬럼 배열
|
||||
*/
|
||||
getGridColumns: function() {
|
||||
return [
|
||||
{
|
||||
header: '번호',
|
||||
name: '_rowNum',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
sortable: false,
|
||||
formatter: function(e) {
|
||||
var totalCount = GRID_PAGINATION_INFO.totalCount;
|
||||
var page = GRID_PAGINATION_INFO.page;
|
||||
var perPage = GRID_PAGINATION_INFO.perPage;
|
||||
var rowIndex = e.row.rowKey;
|
||||
return totalCount - (page - 1) * perPage - rowIndex;
|
||||
}
|
||||
},
|
||||
{ header: '접수일자', name: 'rcptYmd', align: 'center', width: 100,
|
||||
formatter: function(e) {
|
||||
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{ header: '검사일자', name: 'inspYmd', align: 'center', width: 100,
|
||||
formatter: function(e) {
|
||||
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{ header: '차량번호', name: 'vhclno', align: 'center', width: 100 },
|
||||
{ header: '소유자명', name: 'ownrNm', align: 'center', width: 100 },
|
||||
{ header: '자동차명', name: 'carNm', align: 'left', width: 150 },
|
||||
{ header: '자동차종류', name: 'carKnd', align: 'center', width: 100 },
|
||||
{ header: '자동차용도', name: 'carUsg', align: 'center', width: 100 },
|
||||
{ header: '검사종료일자', name: 'inspEndYmd', align: 'center', width: 100,
|
||||
formatter: function(e) {
|
||||
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{ header: '일수', name: 'daycnt', align: 'right', width: 60 },
|
||||
{ header: '과태료금액', name: 'ffnlgAmt', align: 'right', width: 100,
|
||||
formatter: function(e) {
|
||||
return e.value ? parseInt(e.value).toLocaleString() + '원' : '';
|
||||
}
|
||||
},
|
||||
{ header: '주소', name: 'addr', align: 'left', width: 250 },
|
||||
{ header: '처리상태', name: 'taskPrcsSttsCdNm', align: 'center', width: 100 },
|
||||
{ header: '처리일자', name: 'taskPrcsYmd', align: 'center', width: 100,
|
||||
formatter: function(e) {
|
||||
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{ header: '등록일시', name: 'regDt', align: 'center', width: 150 },
|
||||
{ header: '등록자', name: 'rgtrNm', align: 'center', width: 100 }
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* 데이터 소스 생성
|
||||
* @returns {Object} 데이터 소스 설정 객체
|
||||
*/
|
||||
createDataSource: function() {
|
||||
return {
|
||||
api: {
|
||||
readData: {
|
||||
url: '<c:url value="/carInspectionPenalty/registration/list.ajax"/>',
|
||||
method: 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
processData: true
|
||||
}
|
||||
},
|
||||
initialRequest: false,
|
||||
serializer: function(params) {
|
||||
setSearchCond();
|
||||
SEARCH_COND.perPage = params.perPage;
|
||||
SEARCH_COND.page = params.page;
|
||||
return $.param(SEARCH_COND);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 그리드 인스턴스 생성
|
||||
*/
|
||||
create: function() {
|
||||
var gridConfig = this.initConfig();
|
||||
var Grid = tui.Grid;
|
||||
this.instance = gridConfig.instance(Grid);
|
||||
|
||||
// 그리드 테마 설정
|
||||
Grid.applyTheme('striped');
|
||||
|
||||
this.gridBindEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* 그리드 이벤트 바인딩
|
||||
*/
|
||||
gridBindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// 데이터 로딩 완료 이벤트
|
||||
this.instance.on('successResponse', function(ev) {
|
||||
var responseObj = JSON.parse(ev.xhr.response);
|
||||
if(responseObj){
|
||||
$("#currentPage").text(responseObj.data.pagination.page);
|
||||
$("#totalPages").text(responseObj.data.pagination.totalPages);
|
||||
var totalCount = responseObj.data.pagination.totalCount;
|
||||
$("#totalCount").text('총 ' + totalCount.toLocaleString() + '건');
|
||||
|
||||
// 페이징 정보를 전역 변수에 저장
|
||||
GRID_PAGINATION_INFO.totalCount = responseObj.data.pagination.totalCount;
|
||||
GRID_PAGINATION_INFO.page = responseObj.data.pagination.page;
|
||||
GRID_PAGINATION_INFO.perPage = responseObj.data.pagination.perPage;
|
||||
}
|
||||
|
||||
// 선택된 행 초기화
|
||||
CarFfnlgTrgtList.selectedRow = null;
|
||||
});
|
||||
|
||||
// 행 선택 이벤트
|
||||
this.instance.on('check', function(ev) {
|
||||
var rowKey = ev.rowKey;
|
||||
CarFfnlgTrgtList.selectedRow = self.instance.getRow(rowKey);
|
||||
});
|
||||
|
||||
// 행 선택 해제 이벤트
|
||||
this.instance.on('uncheck', function(ev) {
|
||||
CarFfnlgTrgtList.selectedRow = null;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 그리드 새로고침
|
||||
*/
|
||||
reload: function() {
|
||||
this.instance.readData(1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
init: function() {
|
||||
this.grid.create();
|
||||
this.bindEvents();
|
||||
|
||||
// 초기 데이터 로드
|
||||
this.grid.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* 이벤트 바인딩
|
||||
*/
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// 검색 버튼 클릭
|
||||
$("#search_btn").on('click', function() {
|
||||
self.grid.reload();
|
||||
});
|
||||
|
||||
// 초기화 버튼 클릭
|
||||
$("#reset_btn").on('click', function() {
|
||||
$("#schRcptYmdStart").val("");
|
||||
$("#schRcptYmdEnd").val("");
|
||||
$("#schInspYmdStart").val("");
|
||||
$("#schInspYmdEnd").val("");
|
||||
$("#schVhclno").val("");
|
||||
$("#schOwnrNm").val("");
|
||||
$("#schTaskPrcsSttsCd").val("");
|
||||
self.grid.reload();
|
||||
});
|
||||
|
||||
// 등록 버튼 클릭 - 파일 업로드 팝업 열기
|
||||
$("#registerBtn").on('click', function() {
|
||||
self.openUploadPopup();
|
||||
});
|
||||
|
||||
// 삭제 버튼 클릭
|
||||
$("#deleteBtn").on('click', function() {
|
||||
self.deleteData();
|
||||
});
|
||||
|
||||
// 페이지당 건수 변경
|
||||
$("#perPageSelect").on('change', function() {
|
||||
var perPage = parseInt($(this).val(), 10);
|
||||
self.grid.instance.setPerPage(perPage);
|
||||
self.grid.reload();
|
||||
});
|
||||
|
||||
// 검색 조건 입력 필드에서 Enter 키 처리
|
||||
$(".gs_b_top input").on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
self.grid.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 파일 업로드 팝업 열기
|
||||
*/
|
||||
openUploadPopup: function() {
|
||||
var popupUrl = '<c:url value="/carInspectionPenalty/registration/uploadPopup.do"/>';
|
||||
var popupName = 'uploadPopup';
|
||||
var popupWidth = 600;
|
||||
var popupHeight = 400;
|
||||
|
||||
var left = (window.screen.width - popupWidth) / 2;
|
||||
var top = (window.screen.height - popupHeight) / 2;
|
||||
|
||||
var popup = window.open(
|
||||
popupUrl,
|
||||
popupName,
|
||||
'width=' + popupWidth + ',height=' + popupHeight + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes'
|
||||
);
|
||||
|
||||
// 팝업이 닫힐 때 그리드 새로고침
|
||||
var checkPopupClosed = setInterval(function() {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkPopupClosed);
|
||||
CarFfnlgTrgtList.grid.reload();
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* 데이터 삭제
|
||||
*/
|
||||
deleteData: function() {
|
||||
var checkedRows = this.grid.instance.getCheckedRows();
|
||||
|
||||
if (checkedRows.length === 0) {
|
||||
alert("삭제할 데이터를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(checkedRows.length + "건의 데이터를 삭제하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var deletePromises = [];
|
||||
|
||||
checkedRows.forEach(function(row) {
|
||||
var promise = $.ajax({
|
||||
url: '<c:url value="/carInspectionPenalty/registration/delete.ajax"/>',
|
||||
type: 'POST',
|
||||
data: {
|
||||
carFfnlgTrgtId: row.carFfnlgTrgtId
|
||||
}
|
||||
});
|
||||
deletePromises.push(promise);
|
||||
});
|
||||
|
||||
$.when.apply($, deletePromises).done(function() {
|
||||
alert("삭제가 완료되었습니다.");
|
||||
CarFfnlgTrgtList.grid.reload();
|
||||
}).fail(function() {
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
$(document).ready(function() {
|
||||
CarFfnlgTrgtList.init();
|
||||
});
|
||||
|
||||
})(window, jQuery);
|
||||
|
||||
</script>
|
||||
@ -0,0 +1,295 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TXT 파일 업로드</title>
|
||||
<link rel="stylesheet" href="<c:url value='/resources/css/common.css'/>">
|
||||
<link rel="stylesheet" href="<c:url value='/resources/xit/xit-popup.css'/>">
|
||||
<script src="<c:url value='/resources/js/jquery-3.6.0.min.js'/>"></script>
|
||||
<script src="<c:url value='/resources/js/common.js'/>"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: 'Malgun Gothic', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.popup-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.popup-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
color: #333;
|
||||
}
|
||||
.upload-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.upload-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.file-input-wrapper input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.file-input-wrapper input[type="file"]:hover {
|
||||
border-color: #4CAF50;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.file-info {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.file-format-info {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.file-format-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #856404;
|
||||
font-size: 14px;
|
||||
}
|
||||
.file-format-info ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #856404;
|
||||
}
|
||||
.file-format-info li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.button-area {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 30px;
|
||||
margin: 0 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-upload {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
.btn-upload:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.btn-upload:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-close {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
.btn-close:hover {
|
||||
background-color: #da190b;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup-container">
|
||||
<div class="popup-title">과태료 대상 TXT 파일 업로드</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<label class="upload-label">TXT 파일 선택</label>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="txtFile" name="txtFile" accept=".txt" />
|
||||
</div>
|
||||
<div class="file-info">
|
||||
※ TXT 파일만 업로드 가능합니다. (최대 50MB)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-format-info">
|
||||
<h4>📋 파일 형식 안내</h4>
|
||||
<ul>
|
||||
<li>첫 번째 줄은 헤더로 간주되어 스킵됩니다.</li>
|
||||
<li>데이터는 탭(Tab)으로 구분되어야 합니다.</li>
|
||||
<li>컬럼 순서: 접수일자, 구분, 검사소코드, 검사일자, 차량번호, 소유자명, 주민등록번호, 자동차명, 자동차종류, 자동차용도, 검사종료일자, 일수, 과태료금액, 최종등록일자, 주소, 유효기간만료일자, 매매상품</li>
|
||||
<li>필수 항목: 접수일자, 검사일자, 차량번호, 소유자명, 과태료금액</li>
|
||||
<li>날짜 형식: YYYYMMDD (예: 20231113)</li>
|
||||
<li>차량번호 형식: 12가3456 또는 123가4567</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<div>파일 업로드 중입니다. 잠시만 기다려주세요...</div>
|
||||
</div>
|
||||
|
||||
<div class="button-area">
|
||||
<button type="button" class="btn btn-upload" id="uploadBtn">파일 업로드</button>
|
||||
<button type="button" class="btn btn-close" id="closeBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
/**
|
||||
* 파일 업로드 버튼 클릭
|
||||
*/
|
||||
$("#uploadBtn").on('click', function() {
|
||||
var fileInput = document.getElementById('txtFile');
|
||||
var file = fileInput.files[0];
|
||||
|
||||
// 파일 선택 검증
|
||||
if (!file) {
|
||||
alert("파일을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일 확장자 검증
|
||||
var fileName = file.name;
|
||||
var fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
if (fileExt !== 'txt') {
|
||||
alert("TXT 파일만 업로드 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일 크기 검증 (50MB)
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
alert("파일 크기는 50MB를 초과할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 확인 메시지
|
||||
if (!confirm("선택한 파일을 업로드하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FormData 생성
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 로딩 표시
|
||||
$("#loading").addClass('active');
|
||||
$("#uploadBtn").prop('disabled', true);
|
||||
|
||||
// AJAX 업로드
|
||||
$.ajax({
|
||||
url: '<c:url value="/carInspectionPenalty/registration/upload.ajax"/>',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
// 로딩 숨김
|
||||
$("#loading").removeClass('active');
|
||||
$("#uploadBtn").prop('disabled', false);
|
||||
|
||||
if (response.success) {
|
||||
alert(response.message);
|
||||
|
||||
// 성공 시 파일 입력 초기화
|
||||
fileInput.value = '';
|
||||
|
||||
// 부모 창 새로고침을 위해 잠시 대기 후 닫기
|
||||
setTimeout(function() {
|
||||
window.close();
|
||||
}, 500);
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// 로딩 숨김
|
||||
$("#loading").removeClass('active');
|
||||
$("#uploadBtn").prop('disabled', false);
|
||||
|
||||
console.error("업로드 오류:", error);
|
||||
|
||||
var errorMessage = "파일 업로드 중 오류가 발생했습니다.";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage += "\n\n" + xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 닫기 버튼 클릭
|
||||
*/
|
||||
$("#closeBtn").on('click', function() {
|
||||
window.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* 파일 선택 시 파일 정보 표시
|
||||
*/
|
||||
$("#txtFile").on('change', function() {
|
||||
var file = this.files[0];
|
||||
if (file) {
|
||||
var fileSize = (file.size / 1024).toFixed(2); // KB 단위
|
||||
var fileSizeText = fileSize < 1024
|
||||
? fileSize + ' KB'
|
||||
: (fileSize / 1024).toFixed(2) + ' MB';
|
||||
|
||||
$('.file-info').html(
|
||||
'선택된 파일: <strong>' + file.name + '</strong><br>' +
|
||||
'파일 크기: ' + fileSizeText
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue