초기 셋팅

internalApi
박성영 1 month ago
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;
}
}
/**
*
*
* :
* - : 123456, 1234567 (2~3 + + 4 )
* - : 112222 ( + + + )
*
* @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;
}
}

@ -147,12 +147,10 @@ file:
max-size: 10 # 단일 파일 최대 크기 (MB)
max-total-size: 100 # 총 파일 최대 크기 (MB)
max-files: 20 # 최대 파일 개수
allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip
real-file-delete: true # 실제 파일 삭제 여부
allowed-extensions: txt,hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip
real-file-delete: false # 실제 파일 삭제 여부
sub-dirs:
bbs-notice: bbs/notice # 공지사항 sample 파일 저장 경로
bbs-post: bbs/post # 게시판 파일 저장 경로
html-editor: common/html_editor # HTML 에디터 파일 저장 경로
api-target-list-txt-file: api/target # api 조회 대상 파일
# Juso API configuration
juso:

@ -96,3 +96,53 @@ interceptor:
# - application-local.yml: 로컬 개발 환경
# - application-dev.yml: 개발 환경
# - application-prd.yml: 운영 환경
# ===== 자동차 과태료 TXT 파일 파싱 설정 =====
# hangul-byte-size에 따라 자동으로 적절한 바이트 길이 설정을 선택합니다.
# - hangul-byte-size: 2 → byte-size-2 설정 사용 (EUC-KR, MS949)
# - hangul-byte-size: 3 → byte-size-3 설정 사용 (UTF-8)
car-ffnlg-txt-parse:
encoding: EUC-KR # 파일 인코딩 (UTF-8, EUC-KR, MS949 등)
hangul-byte-size: 2 # 한글 문자 바이트 크기 (2=EUC-KR/MS949, 3=UTF-8)
# ===== 2바이트 환경 설정 (EUC-KR, MS949) =====
byte-size-2:
first-line: # 첫째줄 필드별 바이트 길이 (2바이트 기준)
inspstn-cd: 8 # 검사소 코드
insp-ymd: 12 # 검사일자
vhclno: 13 # 차량번호
ownr-nm: 31 # 소유자명
rrno: 15 # 주민등록번호
car-nm: 16 # 자동차명
car-knd: 6 # 자동차종류
car-usg: 19 # 자동차용도
insp-end-ymd: 12 # 검사종료일자
daycnt: 8 # 일수
ffnlg-amt: 6 # 과태료금액
second-line: # 둘째줄 필드별 바이트 길이 (2바이트 기준)
skip: 8 # 공백 (스킵)
last-reg-ymd: 12 # 최종등록일자
addr: 86 # 주소
vld-prd-expry-ymd: 12 # 유효기간만료일자
trd-gds: 11 # 매매상품
# ===== 3바이트 환경 설정 (UTF-8) =====
byte-size-3:
first-line: # 첫째줄 필드별 바이트 길이 (3바이트 기준)
inspstn-cd: 8 # 검사소 코드
insp-ymd: 12 # 검사일자
vhclno: 13 # 차량번호
ownr-nm: 31 # 소유자명
rrno: 15 # 주민등록번호
car-nm: 16 # 자동차명
car-knd: 6 # 자동차종류
car-usg: 19 # 자동차용도
insp-end-ymd: 12 # 검사종료일자
daycnt: 8 # 일수
ffnlg-amt: 6 # 과태료금액
second-line: # 둘째줄 필드별 바이트 길이 (3바이트 기준)
skip: 8 # 공백 (스킵)
last-reg-ymd: 12 # 최종등록일자
addr: 86 # 주소
vld-prd-expry-ymd: 12 # 유효기간만료일자
trd-gds: 11 # 매매상품

@ -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 &gt;= #{schRcptYmdStart}
</if>
<if test='schRcptYmdEnd != null and schRcptYmdEnd != ""'>
AND t.RCPT_YMD &lt;= #{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 &gt;= #{schInspYmdStart}
</if>
<if test='schInspYmdEnd != null and schInspYmdEnd != ""'>
AND t.INSP_YMD &lt;= #{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 &gt;= #{schRcptYmdStart}
</if>
<if test='schRcptYmdEnd != null and schRcptYmdEnd != ""'>
AND t.RCPT_YMD &lt;= #{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 &gt;= #{schInspYmdStart}
</if>
<if test='schInspYmdEnd != null and schInspYmdEnd != ""'>
AND t.INSP_YMD &lt;= #{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…
Cancel
Save