package incheon.ags.aip.web;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.egovframe.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import incheon.ags.aip.service.AipExtracService;
import incheon.ags.aip.util.AipAuthUtil;
import incheon.ags.aip.util.AipAuthUtil.AipAuthInfo;
import incheon.ags.aip.util.AipFileUtil;
import incheon.ags.aip.util.ExcelResultHandler;
import incheon.ags.aip.util.ExcelUtil;
import incheon.ags.aip.vo.AipExtracVO;
import incheon.ags.aip.vo.AipSearchVO;
import incheon.ags.ias.comCd.service.ComCdService;
import incheon.ags.ias.comCd.vo.ComCdVO;
import incheon.com.cmm.api.DefaultApiResponse;
import lombok.RequiredArgsConstructor;

@Controller
@RequestMapping("/aip/extrac")
@RequiredArgsConstructor
public class AipExtracController {
	private final Logger log = LoggerFactory.getLogger(AipExtracController.class);

	private final ComCdService comCdService;
	private final AipExtracService aipExtracService;
	private final AipFileUtil aipFileUtils;
	

	private String newErrId(){
		return UUID.randomUUID().toString();
	}

	private ResponseStatusException serverError(String op, String errId, RuntimeException e){
		log.error("AipExtracController {} failed. errId={}", op, errId, e);
		return new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "처리 중 오류가 발생했습니다. errId=" + errId, e);
	}

	private ResponseEntity<DefaultApiResponse<Map<String, Object>>> extracServerError(String op, RuntimeException e){
		String errId = newErrId();
		log.error("AipExtracController {} failed. errId={}", op, errId, e);
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
				.body(DefaultApiResponse.error(500, "처리 중 오류가 발생했습니다.", "errId=" + errId));
	}

	private ResponseEntity<DefaultApiResponse<Map<String, Object>>> extracServerError(String op, IOException e){
		String errId = newErrId();
		log.error("AipExtracController {} failed. errId={}", op, errId, e);
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
				.body(DefaultApiResponse.error(500, "처리 중 오류가 발생했습니다.", "errId=" + errId));
	}


	@PostMapping("/getExtracList.do")
	@ResponseBody
	public Map<String, Object> getExtracList(@RequestParam(defaultValue = "1") int page, @ModelAttribute AipSearchVO vo) throws Exception {
		try{
			PaginationInfo paginationInfo = new PaginationInfo();
			paginationInfo.setCurrentPageNo(page);
			paginationInfo.setRecordCountPerPage(vo.getRecordCountPerPage());
			paginationInfo.setPageSize(vo.getPageSize());

			vo.setFirstIndex(paginationInfo.getFirstRecordIndex());
			vo.setLastIndex(paginationInfo.getLastRecordIndex());
			vo.setRecordCountPerPage(paginationInfo.getRecordCountPerPage());

			List<AipExtracVO> extracList = aipExtracService.getExtracList(vo);
			int totalCount = aipExtracService.getExtracCount(vo);

			Map<String, Object> result = new HashMap<>();
			result.put("extracList", extracList);
			result.put("totalCount", totalCount);
			return result;
		}catch(RuntimeException e){
			String errId = newErrId();
			throw serverError("getExtracList", errId, e);
		}
	}

	@GetMapping("/extracPop.do")
	public String extracPop(@RequestParam("wtnnc_idntfr") String wtnnc_idntfr, Authentication authentication, ModelMap model) throws Exception {
		try{
			AipAuthInfo auth = AipAuthUtil.from(authentication);
			Map<String, Object> commandMap = new HashMap<>();
			boolean isReadOnly = false;

			commandMap.put("wtnnc_idntfr", wtnnc_idntfr);
			commandMap.put("user_id", auth.getLoginUserId());

			Map<String, Object> extracMap = aipExtracService.getExtracMap(commandMap);
			List<Map<String, Object>> extracFileList = aipExtracService.getExtracFileList(commandMap);

			if(extracMap != null){
				if(!auth.isAdmin() && !auth.isTask()){
					isReadOnly = true;
				}
			}

			model.addAttribute("isAdmin", auth.isAdmin());
			model.addAttribute("isTask", auth.isTask());
			model.addAttribute("isReadOnly", isReadOnly);
			model.addAttribute("extracMap", extracMap);
			model.addAttribute("extracFileList", extracFileList);

			return "ags/aip/popup/extracPop";
		}catch(RuntimeException e){
			String errId = newErrId();
			throw serverError("extracPop", errId, e);
		}
	}

	@PostMapping(value = "/updateExtrac.do", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
	@ResponseBody
	public ResponseEntity<DefaultApiResponse<Map<String, Object>>> updateExtrac(@ModelAttribute AipExtracVO vo,
			@RequestParam(value = "files", required = false) List<MultipartFile> files, Authentication authentication,
			@RequestParam(value = "file_idntfr", required = false) List<String> fileIdList) throws Exception {

		Map<String, Object> result = new HashMap<>();

		try{
			AipAuthInfo auth = AipAuthUtil.from(authentication);
			vo.setUser_id(auth.getLoginUserId());

			Long wtnncId = vo.getWtnnc_idntfr();

			aipExtracService.updateExtrac(vo);

			if(fileIdList != null && !fileIdList.isEmpty()){
				List<Map<String, Object>> delTargets = aipExtracService.deleteFiles(fileIdList);

				for(Map<String, Object> map : delTargets){
					String filePath = (String) map.get("flpth");
					if(filePath == null || filePath.isEmpty()) continue;

					try{
						boolean ok = aipFileUtils.deleteByDbPath(filePath);
						if(!ok) log.warn("파일 삭제 실패: {}", filePath);
					}catch(Exception ex){
						log.warn("파일 삭제 실패: {}", filePath);
					}
				}
			}

			if(files != null && !files.isEmpty()){
				int year = LocalDate.now().getYear();

				List<MultipartFile> allowedFiles = new ArrayList<>();
				for(int i = 0; i < files.size(); i++){
					MultipartFile mf = files.get(i);
					if(mf == null || mf.isEmpty()) continue;

					String originalFileNm = Paths.get(mf.getOriginalFilename()).getFileName().toString();
					if(!isAllowedUpload(originalFileNm, mf.getContentType())){
						log.warn("허용되지 않은 업로드 차단: {} ({})", originalFileNm, mf.getContentType());
						continue;
					}
					allowedFiles.add(mf);
				}

				List<AipFileUtil.SavedFile> saved = aipFileUtils.storeExtracFiles(allowedFiles, year);

				for(AipFileUtil.SavedFile sf : saved){
					Map<String, Object> fileRow = new HashMap<>();
					fileRow.put("wtnnc_idntfr", wtnncId);
					fileRow.put("file_uuid", sf.getFileUuid());
					fileRow.put("file_nm", Paths.get(sf.getOriginalFileNm()).getFileName().toString());
					fileRow.put("file_path", sf.getDbFilePath());
					fileRow.put("size", sf.getSize());
					fileRow.put("content_type", sf.getContentType());
					fileRow.put("user_id", auth.getLoginUserId());

					aipExtracService.insertExtracFile(fileRow);
				}
			}

			result.put("wtnnc_idntfr", vo.getWtnnc_idntfr());
			return ResponseEntity.ok(DefaultApiResponse.success(result, "조서 등록 완료"));
		}catch(IOException e){
			return extracServerError("updateExtrac", e);
		}catch(RuntimeException e){
			return extracServerError("updateExtrac", e);
		}
	}

	@PostMapping("/bmkChange.do")
	public ResponseEntity<Map<String, Object>> bmkChange(@RequestParam Map<String, Object> commandMap, Authentication authentication) throws Exception {
		Map<String, Object> result = new HashMap<>();

		try{
			AipAuthInfo auth = AipAuthUtil.from(authentication);
			commandMap.put("user_id", auth.getLoginUserId());

			boolean flag = Boolean.parseBoolean(String.valueOf(commandMap.get("flag")));
			String wtnnc_idntfr = String.valueOf(commandMap.get("wtnnc_idntfr"));

			if(flag){
				String bmkIdx = aipExtracService.insertBmk(commandMap);
				result.put("bmk_idx", bmkIdx);
			}else{
				aipExtracService.deleteBmk(commandMap);
			}

			result.put("wtnnc_idntfr", wtnnc_idntfr);
			result.put("flag", flag);
			result.put("success", true);
			result.put("message", flag ? "북마크 등록 완료" : "북마크 해제 완료");
			return ResponseEntity.ok(result);
		}catch(RuntimeException e){
			String errId = newErrId();
			log.error("AipExtracController bmkChange failed. errId={}", errId, e);

			result.put("success", false);
			result.put("message", "북마크 처리 중 오류 발생");
			result.put("error", "errId=" + errId);
			return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
		}
	}

	@PostMapping("/excelUpload.do")
	@ResponseBody
	public Map<String, Object> recExcelUpload(@RequestParam("excelFile") MultipartFile file,
			@RequestParam(value="year", required=false) String year,
			Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws Exception {

		List<Map<String, Object>> resultList = new ArrayList<>();
		boolean celCheck = true;
		boolean celNullCheck = true;
		boolean extensionCheck = true;

		Map<String, Object> result = new HashMap<>();
		final String yearParam = (year == null) ? "" : year.trim();

		try{
			AipAuthInfo auth = AipAuthUtil.from(authentication);

			if(!ExcelUtil.isXlsxFile(file)){
				result.put("retMsg", "xlsx 확장자만 업로드할 수 있습니다.");
				result.put("extCheck", false);
				result.put("celCk", false);
				result.put("list", Collections.emptyList());
				return result;
			}

			try(InputStream fis = file.getInputStream(); XSSFWorkbook workbook = new XSSFWorkbook(fis)){
				XSSFSheet sheet = workbook.getSheetAt(0);
				int rows = sheet.getPhysicalNumberOfRows();
				final int expectedCols = 23;
				List<ComCdVO> nList = comCdService.getComCdByGroupCd("AIP_EXMN_CD");
				List<ComCdVO> qList = comCdService.getComCdByGroupCd("AIP_PRCS_SE");
				List<ComCdVO> rList = comCdService.getComCdByGroupCd("AIP_STRCT");
				List<ComCdVO> tList = comCdService.getComCdByGroupCd("AIP_VLTN_YN");

				Map<String, String> nCode = buildCanonMap(nList);
				Map<String, String> qCode = buildCanonMap(qList);
				Map<String, String> rCode = buildCanonMap(rList);
				Map<String, String> tCode = buildCanonMap(tList);

				for(int rowindex = 1; rowindex < rows; rowindex++){
					if(!celCheck) break;

					XSSFRow row = sheet.getRow(rowindex);
					if(row == null){
						celNullCheck = false;
						break;
					}

					XSSFCell aCell = row.getCell(0, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
					String aVal = (aCell == null) ? "" : ExcelUtil.getValue(aCell);
					if(aVal == null || aVal.trim().isEmpty()){
						celNullCheck = false;
						break;
					}

					if(!yearParam.isEmpty()){
						XSSFCell bCell = row.getCell(1, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
						String bVal = (bCell == null) ? "" : ExcelUtil.getValue(bCell);
						bVal = (bVal == null) ? "" : bVal.trim();
						if(!yearParam.equals(bVal)){
							continue;
						}
					}

					short lastCellNum = row.getLastCellNum();
					boolean hasExtraValue = false;
					if(lastCellNum > expectedCols){
						for(int c = expectedCols; c < lastCellNum; c++){
							XSSFCell extra = row.getCell(c, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
							if(extra != null){
								String ev = ExcelUtil.getValue(extra);
								if(ev != null && !ev.trim().isEmpty()){
									hasExtraValue = true;
									break;
								}
							}
						}
					}
					if(hasExtraValue){
						celCheck = false;
						break;
					}

					Map<String, Object> celData = new LinkedHashMap<>();
					for(int col = 0; col < expectedCols; col++){
						XSSFCell cell = row.getCell(col, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
						String value = (cell == null) ? "" : ExcelUtil.getValue(cell);
						String celNm = CellReference.convertNumToColString(col);
						celData.put(celNm, value == null ? "" : value);
					}
					celData.put("user_id", auth.getLoginUserId());
					resultList.add(celData);
				}

				if(!resultList.isEmpty() && celCheck){
					ExcelResultHandler handler = new ExcelResultHandler(null, 0, nCode, qCode, rCode, tCode);
					List<ExcelResultHandler.ValidationException> errors = handler.validateColErrors(resultList, 2);

					if(!errors.isEmpty()){
						int limit = 50;
						StringBuilder sb = new StringBuilder();
						sb.append("업로드 중 오류가 발견되었습니다. 총 ").append(errors.size()).append("건\n");
						for(int i = 0; i < Math.min(errors.size(), limit); i++){
							ExcelResultHandler.ValidationException ve = errors.get(i);
							String col = ve.getColumn() == null ? "" : ve.getColumn().toUpperCase();
							int rowNo = ve.getRowIndex();
							sb.append("- ").append(rowNo).append("행 ").append(col).append("열 : ").append(ve.getUserMessage()).append("\n");
						}
						if(errors.size() > limit) sb.append("... 외 ").append(errors.size() - limit).append("건");

						result.put("retMsg", sb.toString().trim());
						result.put("extCheck", extensionCheck);
						result.put("celCk", false);
						result.put("list", Collections.emptyList());
						return result;
					}

					aipExtracService.setExcelData(resultList);
				}
			}
		}catch(ExcelResultHandler.ValidationException ve){
			// 기존 그대로
			String column = ve.getColumn();
			int rowNo = ve.getRowIndex();
			String cell = (column != null ? column.toUpperCase() : "") + rowNo;

			result.put("retMsg", rowNo + "행 " + column + "열(" + cell + "): " + ve.getUserMessage());
			result.put("extCheck", extensionCheck);
			result.put("celCk", false);
			result.put("list", Collections.emptyList());
			return result;

		}catch(IOException e){
			String errId = newErrId();
			log.error("AipExtracController recExcelUpload failed. errId={}", errId, e);

			result.put("retMsg", "시스템 오류가 발생했습니다. errId=" + errId);
			result.put("list", Collections.emptyList());
			return result;

		}catch(RuntimeException e){
			String errId = newErrId();
			log.error("AipExtracController recExcelUpload failed. errId={}", errId, e);

			result.put("retMsg", "시스템 오류가 발생했습니다. errId=" + errId);
			result.put("list", Collections.emptyList());
			return result;
		}

		String retMsg;
		if(!celCheck) retMsg = "엑셀양식이 잘못되었습니다.\n제공한 엑셀양식에 작성해주세요.";
		else if(!celNullCheck) retMsg = "엑셀 데이터가 올바르지 않습니다.";
		else if(resultList.isEmpty()){
			retMsg = yearParam.isEmpty() ? "업로드할 데이터가 없습니다." : ("선택한 년도(" + yearParam + ")에 해당하는 데이터가 없습니다.");
		}else retMsg = "엑셀 업로드가 완료되었습니다.";

		result.put("list", resultList);
		result.put("celCk", celCheck);
		result.put("extCheck", extensionCheck);
		result.put("retMsg", retMsg);
		return result;
	}
	
	private Map<String, String> buildCanonMap(List<ComCdVO> list){
		Map<String, String> m = new LinkedHashMap<>();

		for(ComCdVO vo : list){
			String cd = vo.getCd();
			String cdNm = vo.getCdNm();

			if(cd != null && !cd.isBlank()) m.put(cd.trim(), cd.trim());
			if(cdNm != null && !cdNm.isBlank() && cd != null && !cd.isBlank()) m.put(cdNm.trim(), cd.trim());
		}
		return m;
	}
	
	@GetMapping("/excelDownload.do")
	public void excelDownload(HttpServletResponse response, @ModelAttribute AipSearchVO vo) throws Exception {
		String errId = null;

		try{
			List<LinkedHashMap<String, Object>> rows = aipExtracService.selectExcelDownData(vo);
			if(rows == null) rows = Collections.emptyList();

			final String[] headerList = { "번호", "적출년도", "적출물번호", "도엽번호", "건물번호", "구명", "법정동", "본번", "부번", "추정면적", "발생시기", "발생구분", "발생형태", "조사여부", "조사일자", "신고허가번호", "처리구분", "구조", "용도", "위반여부", "위반사유", "위반면적", "비고/조사자의견" };
			final String[] keyList = {"gid", "extrac_yr", "extrac_no", "mapsht_no", "sn", "sig_kor_nm", "emd_kor_nm", "mno", "sno", "prsmp_area", "ocrn_era", "ocrn_se", "ocrn_shp", "exmn_yn", "exmn_ymd", "dclr_prmsn_no", "prcs_se", "strct", "usg", "vltn_yn", "vltn_rsn", "vltn_area", "rmrk" };

			try(SXSSFWorkbook wb = new SXSSFWorkbook(200)) {
				wb.setCompressTempFiles(true);
				SXSSFSheet sheet = wb.createSheet("판독조서");

				CellStyle headerStyle = createHeaderStyle(wb);

				Row headerRow = sheet.createRow(0);
				for(int i = 0; i < headerList.length; i++){
					Cell c = headerRow.createCell(i);
					c.setCellValue(headerList[i]);
					c.setCellStyle(headerStyle);
				}

				int rownum = 1;
				for(Map<String,Object> row : rows){
					Row excelRow = sheet.createRow(rownum++);
					for(int i = 0; i < keyList.length; i++){
						Cell c = excelRow.createCell(i);
						Object v = row.get(keyList[i]);
						c.setCellValue(v == null ? "" : String.valueOf(v));
					}
				}

				String filename = URLEncoder.encode("판독조서.xlsx", StandardCharsets.UTF_8).replace("+","%20");
				response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
				response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + filename);

				wb.write(response.getOutputStream());
				response.flushBuffer();
			}

		}catch(IOException e){
			String errId1 = newErrId();
			log.error("AipExtracController excelDownload failed. errId={}", errId1, e);

			try{
				if(!response.isCommitted()){
					response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "엑셀 생성 오류");
				}
			}catch(IOException ioe){
				log.warn("엑셀 생성 오류 응답 전송 실패. errId={}", errId1);
			}

		}catch(RuntimeException e){
			String errId2 = newErrId();
			log.error("AipExtracController excelDownload failed. errId={}", errId2, e);

			try{
				if(!response.isCommitted()){
					response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "엑셀 생성 오류");
				}
			}catch(IOException ioe){
				log.warn("엑셀 생성 오류 응답 전송 실패. errId={}", errId2);
			}
		}
	}
	
	
	@PostMapping("/excelDownloadGeo.do")
	public void excelDownloadGeo(HttpServletResponse response, @RequestParam(name="ids", required=false) String ids) throws Exception {
		try{
			List<Long> gidList = new ArrayList<>();

			if(ids != null){
				String[] parts = ids.split(",");
				for(int i=0;i<parts.length;i++){
					String p = parts[i] == null ? "" : parts[i].trim();
					if(p.isEmpty()) continue;

					try{
						gidList.add(Long.valueOf(p));
					}catch(NumberFormatException ignore){}
					if(gidList.size() >= 1000) break;
				}
			}

			List<LinkedHashMap<String, Object>> rows;
			if(gidList.isEmpty()){
				rows = Collections.emptyList();
			}else{
				rows = aipExtracService.selectExcelDownDataByIds(gidList);
				if(rows == null) rows = Collections.emptyList();
			}

			final String[] headerList = { "번호", "적출년도", "적출물번호", "도엽번호", "건물번호", "구명", "법정동", "본번", "부번", "추정면적", "발생시기", "발생구분", "발생형태", "조사여부", "조사일자", "신고허가번호", "처리구분", "구조", "용도", "위반여부", "위반사유", "위반면적", "비고/조사자의견" };
			final String[] keyList = {"gid", "extrac_yr", "extrac_no", "mapsht_no", "sn", "sig_kor_nm", "emd_kor_nm", "mno", "sno", "prsmp_area", "ocrn_era", "ocrn_se", "ocrn_shp", "exmn_yn", "exmn_ymd", "dclr_prmsn_no", "prcs_se", "strct", "usg", "vltn_yn", "vltn_rsn", "vltn_area", "rmrk" };

			try(SXSSFWorkbook wb = new SXSSFWorkbook(200)) {
				wb.setCompressTempFiles(true);
				SXSSFSheet sheet = wb.createSheet("판독조서");

				CellStyle headerStyle = createHeaderStyle(wb);

				Row headerRow = sheet.createRow(0);
				for(int i = 0; i < headerList.length; i++){
					Cell c = headerRow.createCell(i);
					c.setCellValue(headerList[i]);
					c.setCellStyle(headerStyle);
				}

				int rownum = 1;
				for(Map<String,Object> row : rows){
					Row excelRow = sheet.createRow(rownum++);
					for(int i = 0; i < keyList.length; i++){
						Cell c = excelRow.createCell(i);
						Object v = row.get(keyList[i]);
						c.setCellValue(v == null ? "" : String.valueOf(v));
					}
				}

				String filename = URLEncoder.encode("판독조서.xlsx", StandardCharsets.UTF_8).replace("+","%20");
				response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
				response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + filename);

				wb.write(response.getOutputStream());
				response.flushBuffer();
			}

		}catch(IOException e){
			String errId1 = newErrId();
			log.error("AipExtracController excelDownloadGeo failed. errId={}", errId1, e);

			try{
				if(!response.isCommitted()){
					response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "엑셀 생성 오류");
				}
			}catch(IOException ioe){
				log.warn("엑셀 생성 오류 응답 전송 실패. errId={}", errId1);
			}

		}catch(RuntimeException e){
			String errId2 = newErrId();
			log.error("AipExtracController excelDownloadGeo failed. errId={}", errId2, e);

			try{
				if(!response.isCommitted()){
					response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "엑셀 생성 오류");
				}
			}catch(IOException ioe){
				log.warn("엑셀 생성 오류 응답 전송 실패. errId={}", errId2);
			}
		}
	}

	private CellStyle createHeaderStyle(Workbook wb){
		XSSFCellStyle style = (XSSFCellStyle) wb.createCellStyle();

		Font bold = wb.createFont();
		bold.setBold(true);
		bold.setFontHeightInPoints((short)12);
		style.setFont(bold);

		style.setAlignment(HorizontalAlignment.CENTER);
		style.setVerticalAlignment(VerticalAlignment.CENTER);

		XSSFColor headerColor = new XSSFColor(new java.awt.Color(0x88, 0x8e, 0x8c), null);
		style.setFillForegroundColor(headerColor);
		style.setFillPattern(FillPatternType.SOLID_FOREGROUND);

		return style;
	}
	
	private static boolean isAllowedUpload(String filename, String contentType){
		if(filename == null) return false;

		String name = filename.trim();
		if(name.isEmpty()) return false;

		int dot = name.lastIndexOf('.');
		String ext = (dot > -1 ? name.substring(dot + 1) : "").toLowerCase(Locale.ROOT);
		if(ext.isEmpty()) return false;

		boolean extOk = ext.equals("hwp") || ext.equals("hwpx")
				|| ext.equals("doc") || ext.equals("docx")
				|| ext.equals("xls") || ext.equals("xlsx")
				|| ext.equals("ppt") || ext.equals("pptx")
				|| ext.equals("pdf")
				|| ext.equals("bmp") || ext.equals("gif") || ext.equals("jpg") || ext.equals("jpeg") || ext.equals("png")
				|| ext.equals("zip")
				|| ext.equals("tif") || ext.equals("tiff")
				|| ext.equals("dwg") || ext.equals("dxf")
				|| ext.equals("txt") || ext.equals("sam");

		if(!extOk) return false;

		if(contentType == null || contentType.isBlank()) return true;

		String ct = contentType.toLowerCase(Locale.ROOT);

		if(ext.equals("bmp") || ext.equals("gif") || ext.equals("jpg") || ext.equals("jpeg") || ext.equals("png")
				|| ext.equals("tif") || ext.equals("tiff")){
			return ct.startsWith("image/");
		}

		if(ext.equals("pdf")){
			return ct.equals("application/pdf");
		}

		return true;
	}

}
