diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registration/controller/CarFfnlgTrgtController.java b/src/main/java/go/kr/project/carInspectionPenalty/registration/controller/CarFfnlgTrgtController.java index aa0265f..88ef5fd 100644 --- a/src/main/java/go/kr/project/carInspectionPenalty/registration/controller/CarFfnlgTrgtController.java +++ b/src/main/java/go/kr/project/carInspectionPenalty/registration/controller/CarFfnlgTrgtController.java @@ -22,6 +22,8 @@ import org.springframework.web.servlet.ModelAndView; import java.util.List; import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import java.net.URLEncoder; /** * 자동차 과태료 대상 등록 Controller @@ -83,6 +85,47 @@ public class CarFfnlgTrgtController { return ApiResponseUtil.successWithGrid(list, paramVO); } + + /** + * 과태료 대상 목록 다운로드 (EUC-KR 텍스트) + * 샘플 파일과 동일한 고정폭 포맷으로 생성하여 다운로드 제공합니다. + * - 인코딩: EUC-KR (한글 2바이트) + * - 헤더/구분선/컬럼 구성은 docs/샘플용-EUC-KR.txt와 동일 + */ + @GetMapping("/download.do") + @Operation(summary = "과태료 대상 목록 다운로드", description = "EUC-KR 인코딩의 고정폭 텍스트로 목록을 샘플과 동일한 포맷으로 다운로드합니다.") + public void download( + @ModelAttribute CarFfnlgTrgtVO paramVO, + HttpServletResponse response + ) { + try { + + // 페이징 없이 전체 조회를 위해 페이징 비활성화 + paramVO.setPagingYn("N"); + + // 서비스에서 EUC-KR 텍스트 콘텐츠 생성 + byte[] fileBytes = service.generateEucKrDownloadBytes(paramVO); + + String fileName = URLEncoder.encode("유효기간경과_과태료부과대상_리스트.txt", "UTF-8"); + + // 응답 헤더 설정 (텍스트 파일, EUC-KR 인코딩) + response.setContentType("text/plain; charset=EUC-KR"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentLength(fileBytes.length); + + // 바이트 스트림으로 전송 (본문은 EUC-KR 바이트) + response.getOutputStream().write(fileBytes); + response.getOutputStream().flush(); + } catch (Exception e) { + log.error("목록 다운로드 중 오류", e); + try { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("다운로드 처리 중 오류가 발생했습니다: " + e.getMessage()); + } catch (Exception ignored) { + + } + } + } /** * 파일 업로드 팝업 화면 diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/CarFfnlgTrgtService.java b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/CarFfnlgTrgtService.java index 5aa0edd..1801672 100644 --- a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/CarFfnlgTrgtService.java +++ b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/CarFfnlgTrgtService.java @@ -60,4 +60,13 @@ public interface CarFfnlgTrgtService { * @return 처리 결과 (성공 건수, 실패 건수, 오류 메시지 목록) */ Map uploadAndParseTxtFile(MultipartFile file, String rgtr); + + /** + * 과태료 대상 목록을 EUC-KR 인코딩의 고정폭 텍스트 바이트로 생성 + * 샘플 텍스트(docs/샘플용-EUC-KR.txt)와 동일한 포맷으로 출력합니다. + * + * @param vo 검색 조건 + * @return EUC-KR 인코딩 바이트 배열 + */ + byte[] generateEucKrDownloadBytes(CarFfnlgTrgtVO vo); } diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java index b853795..1dc9c46 100644 --- a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java +++ b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java @@ -233,6 +233,161 @@ public class CarFfnlgTrgtServiceImpl implements CarFfnlgTrgtService { return result; } + + /** + * 목록을 EUC-KR 텍스트로 생성하여 다운로드용 바이트 배열을 반환 + * 중요: 샘플 파일과 완전히 동일한 고정폭 포맷을 맞추기 위해 각 필드를 바이트 기준으로 패딩/절단 처리함 (한글 2바이트) + */ + @Override + public byte[] generateEucKrDownloadBytes(CarFfnlgTrgtVO vo) { + try { + // 인코딩 및 바이트 규칙 설정 (기본 EUC-KR, 한글 2바이트) + final String encoding = parseConfig.getEncoding() == null || parseConfig.getEncoding().trim().isEmpty() + ? "EUC-KR" : parseConfig.getEncoding().trim(); + + // 1) 데이터 조회 (페이징 비활성화) + List list = mapper.selectList(vo); + + StringBuilder sb = new StringBuilder(); + + // 2) 헤더 구성 (샘플 텍스트와 동일) + sb.append(padLeftBytes("유효기간경과 과태료부과대상 리스트", 46, encoding)).append("\r\n"); + sb.append(padLeftBytes("------------------------------------", 48, encoding)).append("\r\n"); + sb.append("\r\n "); + sb.append("\r\n "); + sb.append(" * 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)\r\n"); + sb.append(" * 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.\r\n"); + sb.append("-------------------------------------------------------------------------------------------------------------------------------------------------\r\n"); + sb.append("검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료\r\n"); + sb.append(" 최종등록일 주 소 유효기간만료일 매매상품용\r\n"); + sb.append("-------------------------------------------------------------------------------------------------------------------------------------------------\r\n"); + + // 3) 데이터 라인 생성 (각 항목 2줄) + for (CarFfnlgTrgtVO row : list) { + // 첫째줄: 고정폭 필드들 연결 + String firstLine = + padRightBytes(nvl(row.getInspstnCd()), parseConfig.getFirstLineLength("inspstn-cd"), encoding) + + padRightBytes(formatYmd(row.getInspYmd(), true), parseConfig.getFirstLineLength("insp-ymd"), encoding) + + padRightBytes(nvl(row.getVhclno()), parseConfig.getFirstLineLength("vhclno"), encoding) + + padRightBytes(nvl(row.getOwnrNm()), parseConfig.getFirstLineLength("ownr-nm"), encoding) + + padRightBytes(nvl(row.getRrno()), parseConfig.getFirstLineLength("rrno"), encoding) + + padRightBytes(nvl(row.getCarNm()), parseConfig.getFirstLineLength("car-nm"), encoding) + + padRightBytes(nvl(row.getCarKnd()), parseConfig.getFirstLineLength("car-knd"), encoding) + + padRightBytes(nvl(row.getCarUsg()), parseConfig.getFirstLineLength("car-usg"), encoding) + + padRightBytes(formatYmd(row.getInspEndYmd(), true), parseConfig.getFirstLineLength("insp-end-ymd"), encoding) + + padLeftBytes(nvl(row.getDaycnt()), parseConfig.getFirstLineLength("daycnt"), encoding) + + padLeftBytes(formatAmtToManWon(row.getFfnlgAmt()), parseConfig.getFirstLineLength("ffnlg-amt"), encoding); + + sb.append(firstLine).append("\r\n"); + + // 둘째줄: skip + 나머지 필드 + String secondLine = + padRightBytes("", parseConfig.getSecondLineLength("skip"), encoding) + + padRightBytes(formatYmd(row.getLastRegYmd(), true), parseConfig.getSecondLineLength("last-reg-ymd"), encoding) + + padRightBytes(nvl(row.getAddr()), parseConfig.getSecondLineLength("addr"), encoding) + + padRightBytes(formatYmd(row.getVldPrdExpryYmd(), true), parseConfig.getSecondLineLength("vld-prd-expry-ymd"), encoding) + + padRightBytes(nvl(row.getTrdGds()), parseConfig.getSecondLineLength("trd-gds"), encoding); + + sb.append(secondLine).append("\r\n"); + sb.append("\r\n"); + } + + return sb.toString().getBytes(encoding); + } catch (Exception e) { + throw new RuntimeException("다운로드 파일 생성 중 오류: " + e.getMessage(), e); + } + } + + // ================== 내부 유틸 메서드 ================== + + /** null 안전 치환 */ + private static String nvl(String s) { return s == null ? "" : s; } + + /** + * 날짜 포맷 변환: YYYYMMDD → YYYY-MM-DD (입력에 '-'가 이미 있으면 그대로 사용) + * - 샘플 출력과 동일한 포맷을 위함 + */ + private static String formatYmd(String ymd, boolean withHyphen) { + if (ymd == null || ymd.trim().isEmpty()) return ""; + String v = ymd.trim(); + if (!withHyphen) return v; + if (v.contains("-")) return v; // 이미 하이픈 포함 + if (v.length() == 8) { + return v.substring(0,4) + "-" + v.substring(4,6) + "-" + v.substring(6,8); + } + return v; + } + + /** + * 과태료 금액을 "만원" 단위로 표현 + * - 입력이 숫자형(원)일 경우: 300000 → 30만원 + * - 이미 "만원" 문자열 포함 시 그대로 사용 + */ + private static String formatAmtToManWon(String amt) { + String v = nvl(amt).trim(); + if (v.isEmpty()) return ""; + if (v.endsWith("만원")) return v; + try { + long won = Long.parseLong(v.replaceAll("[^0-9]", "")); + long man = Math.round(won / 10000.0); + return String.valueOf(man) + "만원"; + } catch (NumberFormatException e) { + return v; // 숫자 변환 실패 시 원문 유지 + } + } + + /** 주어진 문자열을 지정 바이트 길이로 오른쪽 공백 패딩 (EUC-KR 기준 바이트) */ + private static String padRightBytes(String s, int byteLen, String encoding) throws Exception { + if (byteLen <= 0) return nvl(s); + String v = nvl(s); + byte[] b = v.getBytes(encoding); + if (b.length == byteLen) return v; + if (b.length > byteLen) { + return truncateToBytes(v, byteLen, encoding); + } + // 패딩 + StringBuilder sb = new StringBuilder(v); + while (sb.toString().getBytes(encoding).length < byteLen) { + sb.append(' '); + } + return sb.toString(); + } + + /** 주어진 문자열을 지정 바이트 길이로 왼쪽 공백 패딩 (주로 숫자용 정렬) */ + private static String padLeftBytes(String s, int byteLen, String encoding) throws Exception { + if (byteLen <= 0) return nvl(s); + String v = nvl(s); + byte[] b = v.getBytes(encoding); + if (b.length == byteLen) return v; + if (b.length > byteLen) { + return truncateToBytes(v, byteLen, encoding); + } + StringBuilder sb = new StringBuilder(v); + while (sb.toString().getBytes(encoding).length < byteLen) { + sb.insert(0, ' '); + } + return sb.toString(); + } + + /** 지정 바이트 길이에 맞게 문자열을 잘라냄 (문자 중간 분리 방지) */ + private static String truncateToBytes(String s, int byteLen, String encoding) throws Exception { + if (s == null) return ""; + byte[] b = s.getBytes(encoding); + if (b.length <= byteLen) return s; + // 바이트 배열을 직접 잘라 안전한 문자열 생성 + byte[] cut = new byte[byteLen]; + System.arraycopy(b, 0, cut, 0, byteLen); + // 잘린 바이트가 멀티바이트 문자를 깨뜨렸을 수 있으므로 + // 디코딩 실패 시 한 바이트씩 줄이며 복구 + for (int len = byteLen; len > 0; len--) { + try { + return new String(cut, 0, len, encoding); + } catch (Exception ignore) { + // len 감소 + } + } + return ""; + } /** * 오류 메시지 목록을 하나의 문자열로 조합 diff --git a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp index 2d4384f..19d86df 100644 --- a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp +++ b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp @@ -10,6 +10,7 @@
과태료 대상 목록
+ @@ -112,6 +113,21 @@ SEARCH_COND.schTaskPrcsSttsCd = schTaskPrcsSttsCd; }; + // 다운로드 URL 생성 (현재 검색조건을 쿼리스트링으로 부여) + var buildDownloadUrl = function() { + setSearchCond(); + var baseUrl = ''; + var params = []; + if (SEARCH_COND.schRcptYmdStart) params.push('schRcptYmdStart=' + encodeURIComponent(SEARCH_COND.schRcptYmdStart)); + if (SEARCH_COND.schRcptYmdEnd) params.push('schRcptYmdEnd=' + encodeURIComponent(SEARCH_COND.schRcptYmdEnd)); + if (SEARCH_COND.schInspYmdStart) params.push('schInspYmdStart=' + encodeURIComponent(SEARCH_COND.schInspYmdStart)); + if (SEARCH_COND.schInspYmdEnd) params.push('schInspYmdEnd=' + encodeURIComponent(SEARCH_COND.schInspYmdEnd)); + if (SEARCH_COND.schVhclno) params.push('schVhclno=' + encodeURIComponent(SEARCH_COND.schVhclno)); + if (SEARCH_COND.schOwnrNm) params.push('schOwnrNm=' + encodeURIComponent(SEARCH_COND.schOwnrNm)); + if (SEARCH_COND.schTaskPrcsSttsCd) params.push('schTaskPrcsSttsCd=' + encodeURIComponent(SEARCH_COND.schTaskPrcsSttsCd)); + return baseUrl + (params.length ? ('?' + params.join('&')) : ''); + }; + /** * 과태료 대상 목록 관리 네임스페이스 */ @@ -370,6 +386,13 @@ self.deleteData(); }); + // 목록 다운로드 버튼 클릭 + $("#downloadBtn").on('click', function() { + var url = buildDownloadUrl(); + // 파일 다운로드는 단순 이동으로 처리 + window.location.href = url; + }); + // 페이지당 건수 변경 $("#perPageSelect").on('change', function() { var perPage = parseInt($(this).val(), 10);