[final project] day_4

김예은·2023년 8월 18일

엑셀파일 업로드

  1. 클라이언트가 엑셀 파일을 선택하여 업로드 버튼을 누릅니다.
  2. 업로드 버튼을 클릭하면 클라이언트에서 선택한 엑셀 파일이 서버로 업로드됩니다.
  3. 서버에서는 업로드된 파일을 파싱하여 데이터를 추출합니다.
  4. 추출한 데이터를 사용하여 엑셀 데이터를 처리합니다.

라이브러리 비교

sheetJs vs poi

sheetJs클라이언트 사이드에서 가벼운 엑셀 파일 파싱과 처리에 적합하며, 간단한 데이터 구조를 다룰 때 사용.

poi서버 사이드에서 복잡한 엑셀 파일을 처리하고 다루는 데에 특화되어 있으며, 대용량 데이터 및 복잡한 구조를 다루는 데 사용.

본 프로젝트에서는 하나의 파일에 대량 정보가 입력되어 전달되는 형태이나
엑셀 입력 및 다운로드의 복잡한 기능이 필요하여 서버단에서 처리하는 것이 적합하다고 판단하였다.

sheetJs

	 @PostMapping("/excel")
    public String uploadExcel(@RequestParam("file") MultipartFile file,
    						  BoardFile boardFile) {
        try {
            List<Map<String, Object>> jsonDataList = parseExcel(file);
            // 업로드 후
            empService.excelUpload(boardFile);
            // json 데이터로 처리
            empService.excelProcess(jsonDataList);
            return "엑셀 파일 업로드 및 처리 성공";
        } catch (Exception e) {
            return "엑셀 파일 업로드 및 처리 실패: " + e.getMessage();
        }
    }

POI

import
java.io = inputStream을 사용하여 데이터를 읽어옴
WorkbookFactory는 Apache POI를 다루기 위함. 엑셀 워크북을 생성( 'WorkbookFactory.create(inputStream)' )하고 열기위해 사용되며, 엑셀 파일을 InputStream으로부터 읽어와서 워크북 객체로 변환하는 역할을 수행.

registEmp.jsp (controller와 연결되는 부분)

<script>
	    $(document).ready(function() {
	     	// 업로드 버튼 클릭 시
	        $('#uploadBtn').click(function(event) {
	            const fileInput = $('#fileInput');

	            if (fileInput.get(0).files.length === 0) {
	                event.preventDefault(); // 기본 동작 중단
	                alert('파일을 선택해주세요.');
	                return false;
	            }

	            const file = fileInput.get(0).files[0]; // 선택된 파일 가져오기
	            const fileName = file.name;
	            const fileExtension = fileName.split('.').pop().toLowerCase();

	            // 엑셀 파일이 아닌 경우 업로드 막기
	            if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
	                event.preventDefault(); // 기본 동작 중단
	                alert('엑셀 파일(xlsx 또는 xls)만 선택해주세요.');
	                return false;
	            }

	            const formData = new FormData(); // FormData 객체 생성
	            formData.append('file', file); // FormData에 엑셀 파일 추가

	            const reader = new FileReader(); // FileReader 객체 생성
	            reader.onload = function(event) {
	                const data = new Uint8Array(event.target.result); // 파일 데이터를 Uint8Array 형태로 읽어들임

	                const workbook = XLSX.read(data, { type: 'array' }); // 엑셀 파일을 배열 형태로 읽어들임
	                const sheetName = workbook.SheetNames[0]; // 첫 번째 시트 이름 가져오기

	                const sheet = workbook.Sheets[sheetName]; // 첫 번째 시트 가져오기

	                // 시트 데이터를 JSON 형태로 변환하여 jsonData 변수에 저장
	                const jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 });

	                formData.append('jsonData', JSON.stringify(jsonData)); // 파싱된 데이터를 FormData에 추가

	                // 파싱된 데이터를 서버로 전송
	                $.ajax({
	                    url: '/excel', // 엔드포인트 URL 설정
	                    type: 'POST',
	                    data: formData, // FormData 전송
	                    processData: false, // 데이터 처리 방지 (FormData를 직접 보내므로 false 설정)
	                    contentType: false, // 컨텐츠 타입 자동 설정 방지
	                    success: function(response) {
	                        console.log(response);
	                        // 업로드 및 저장이 성공했을 때의 동작을 정의할 수 있습니다.
	                    },
	                    error: function(xhr, status, error) {
	                        console.error(error);
	                        // 업로드 및 저장이 실패했을 때의 동작을 정의할 수 있습니다.
	                    }
	                });
	            };
	            reader.readAsArrayBuffer(file); // 파일 데이터를 ArrayBuffer 형태로 읽어들임
	        });
	    });
	</script>
    <body>
      <h3>사원 등록</h3>

      <!-- 엑셀 공통 양식 다운로드 버튼 추가 -->
      <a href="/upload/excelForm.xlsx" download="excelForm.xlsx">엑셀 공통 양식 다운로드</a>

      <!-- 파일 업로드 -->
      <form id="uploadForm" action="/excelUpload" method="post" enctype="multipart/form-data">
          <input type="file" name="file" id="fileInput">
          <button type="submit" id="uploadBtn">저장</button>
          <span id="msg"></span>
      </form>
    </body>

files.length에서 선택된 파일의 개수를 가져오는 JavaScript의 기본기능을 사용.

RestController

json 데이터를 취급한다.

version_1

// 엑셀 파일을 파싱하여 JSON 데이터로 변환하는 메서드
private List<Map<String, Object>> parseExcel(MultipartFile file) throws IOException {
    List<Map<String, Object>> jsonDataList = new ArrayList<>();
	// InputStream은 Java의 기본 클래스, 데이터를 읽어오는 데 사용되는 입력 스트림을 표현하는 인터페이스. 파일, 네트워크 연결, 메모리 등 다양한 소스로부터 데이터를 읽을 때 사용

    try (InputStream inputStream = file.getInputStream()) {
        Workbook workbook = WorkbookFactory.create(inputStream); // 엑셀 파일을 읽어 Workbook 객체 생성
        Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트 가져오기

        // 헤더 행 추출
        Row headerRow = sheet.getRow(0); // 첫 번째 행을 헤더 행으로 추출
        List<String> headers = new ArrayList<>();
        for (Cell cell : headerRow) {
            headers.add(cell.getStringCellValue()); // 헤더 행의 각 셀의 값을 가져와서 헤더 리스트에 추가
        }

        // 데이터 행 추출 및 변환
        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
            Row dataRow = sheet.getRow(rowIndex); // 데이터 행을 추출
            if (dataRow != null) {
                Map<String, Object> rowData = new HashMap<>(); // 한 행의 데이터를 저장할 Map 객체 생성
                for (int cellIndex = 0; cellIndex < headers.size(); cellIndex++) {
                    Cell cell = dataRow.getCell(cellIndex); // 해당 셀 추출
                    if (cell != null) {
                        String header = headers.get(cellIndex); // 현재 셀의 헤더 이름 가져오기
                        rowData.put(header, getCellValue(cell)); // 헤더 이름을 키로, 셀의 값을 변환하여 Map에 추가
                    }
                }
                jsonDataList.add(rowData); // 변환된 데이터를 리스트에 추가
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    return jsonDataList; // 변환된 JSON 데이터 리스트 반환
}

// 셀의 값을 변환하여 반환하는 메서드
private Object getCellValue(Cell cell) {
    switch (cell.getCellType()) {
        case STRING:
            return cell.getStringCellValue(); // 문자열 값 반환
        case NUMERIC:
            if (DateUtil.isCellDateFormatted(cell)) {
                return cell.getDateCellValue(); // 날짜 형식인 경우 날짜 값 반환
            } else {
                return cell.getNumericCellValue(); // 숫자 값 반환
            }
        case BOOLEAN:
            return cell.getBooleanCellValue(); // 불리언 값 반환
        default:
            return null; // 다른 타입의 값은 null로 반환
    }
}

문제점
1. 업로드 버튼을 눌렀는데 값이 전달되었는지 json 데이터로 받아져 열렸다.

하지만 입사일은 YYYY.MM.DD로 받아졌고, 사원번호는 1111000.0, 권한은 0.0 으로 들어온다는 문제가 발생했다.
이는 DB 필드의 데이터 형식인 YYYY-MM-DD(날짜), 정수(사원번호, 권한)을 만족하지 못하므로, 데이터를 저장하는 부분에 분기하여 put 처리를 해주었다.
2. service 단에 DB와 연결해주는 부분이 작성되어있지 않았다.
controller에서 처리한 값을 service단과 연결해주는 uploadExcel 메서드를 만들었다. 성공시 success를 반환, 실패시 error를 반환해준다.

version_2

@PostMapping("/excel")// 엑셀 파일을 파싱하여 JSON 데이터로 변환하는 메서드
    private List<Map<String, Object>> parseExcel(MultipartFile file) throws IOException {
        List<Map<String, Object>> jsonDataList = new ArrayList<>();
        
        try (InputStream inputStream = file.getInputStream()) {
        	// InputStream은 Java의 기본 클래스.
        	// 데이터를 읽어오는 데 사용되는 '입력 스트림'을 표현하는 인터페이스.
        	// 파일, 네트워크 연결, 메모리 등 다양한 소스로부터 데이터를 읽을 때 사용한다.
            Workbook workbook = WorkbookFactory.create(inputStream); // 엑셀 파일을 읽어 Workbook 객체 생성
            Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트 가져오기

            // 헤더 행 추출
            Row headerRow = sheet.getRow(0); // 첫 번째 행을 헤더 행으로 추출
            List<String> headers = new ArrayList<>();
            for (Cell cell : headerRow) {
                headers.add(cell.getStringCellValue()); // 헤더 행의 각 셀의 값을 가져와서 헤더 리스트에 추가
            }

            SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD"); // import java.util.Date; 자바의 기본 클래스 활용. SimpleDateFormat 객체를 생성 -> 날짜를 "YYYY-MM-DD" 형식으로 포맷팅할 준비.

            // 데이터 행 추출 및 변환
            for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
                Row dataRow = sheet.getRow(rowIndex); // 데이터 행을 추출
                if (dataRow != null) {
                    Map<String, Object> rowData = new HashMap<>(); // 한 행의 데이터를 저장할 Map 객체 생성
                    for (int cellIndex = 0; cellIndex < headers.size(); cellIndex++) {
                        Cell cell = dataRow.getCell(cellIndex); // 해당 셀 추출
                        if (cell != null) {
                            String header = headers.get(cellIndex); // 현재 셀의 헤더 이름 가져오기
                            if ("입사일".equals(header)) {
                            	Date dateCellValue = cell.getDateCellValue(); // 셀 값을 날짜로 가져와줌.
                                String formattedDate = dateFormat.format(dateCellValue); // 포맷된 날짜 변수를 생성. dateFormat 객체를 이용해 날짜를 "yyyy-MM-dd" 형식으로 변환
                                rowData.put(header, formattedDate);
                            } else if ("사원번호".equals(header) || "권한".equals(header)) {
                                int intValue = (int) cell.getNumericCellValue(); // 실수를 정수로 변환
                                rowData.put(header, intValue);
                            } else {
                                rowData.put(header, getCellValue(cell)); // 해당 셀 추출
                            }
                        }
                    }
                    jsonDataList.add(rowData); // 변환된 데이터를 리스트에 추가
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return jsonDataList; // 변환된 JSON 데이터 리스트 반환
    }
    // 셀의 값을 변환하여 반환하는 메서드
    private Object getCellValue(Cell cell) {
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue(); // 문자열 값 반환
            case NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    return cell.getDateCellValue(); // 날짜 형식인 경우 날짜 값 반환
                } else {
                    return cell.getNumericCellValue(); // 숫자 값 반환
                }
            case BOOLEAN:
                return cell.getBooleanCellValue(); // 불리언 값 반환
            default:
                return null; // 다른 타입의 값은 null로 반환
        }
    }
    
    public String uploadExcel(@RequestParam("file") MultipartFile file) {
        try {
            List<Map<String, Object>> jsonDataList = parseExcel(file);
            empService.excelProcess(jsonDataList);
            return "success"; // 성공 페이지로 리턴하거나 메시지를 반환
        } catch (IOException e) {
            e.printStackTrace();
            return "error"; // 오류 페이지로 리턴하거나 메시지를 반환
        }
    }

simpleDateFormat객체를 생성하고 YYYY.MM.DD -> YYYY-MM-DD로 바꿀 수 있게 했다.

이와 관련해서 멀티 쓰레드 개념이 있다. 멀티 쓰레드를 사용하는, 부하가 많은 시스템에서는 SimpleDateFormat을 사용할 때 java.lang.ArrayIndexOutOfBoundsException 가 발생한다.
이를 보완해서 Joda-Time 라이브러리 또는 apache의 common에 FastDateTime 이라는 라이브러리를 사용하는데, Thread-Safe 하며 속도도 빠르다.

*스레드란? 컴퓨터에서 실행되는 작은 작업 단위

*멀티 스레드의 예시

  • 웹 서버
    여러 클라이언트의 요청을 동시에 처리해야 하는 경우.
    각 클라이언트 요청은 하나의 스레드로 처리되어야 하며, 이를 통해 여러 클라이언트의 동시 접속에 대응할 수 있음.

  • 다중 사용자 어플리케이션
    여러 사용자가 동시에 어플리케이션을 사용하는 경우.
    각 사용자는 별도의 스레드에서 작업을 수행.
    ex. 채팅 어플리케이션에서 여러 사용자가 메시지를 주고받을 때, 각 사용자의 메시지 처리는 별도의 스레드에서 이루어짐.

  • 멀티미디어 애플리케이션
    동영상 플레이어나 음악 플레이어와 같은 멀티미디어 애플리케이션은 화면 표시와 미디어 재생을 병행 처리해야 합니다.
    화면 갱신은 한 스레드에서, 미디어 재생은 다른 스레드에서 처리하여 부드러운 화면 표시와 재생을 구현할 수 있습니다.

  • 병렬 처리
    대용량 데이터를 처리하는 작업에서 병렬 스레드를 사용하여 처리 속도를 높일 수 있음.
    ex. 이미지 처리나 데이터베이스 쿼리와 같은 작업에서 병렬 처리를 통해 더 빠른 결과를 얻을 수 있습니다.

  • 게임
    게임에서는 화면 표시, 게임 로직, 사용자 입력 처리 등 다양한 작업을 병행. 각각의 작업을 별도의 스레드에서 처리하여 게임의 반응성과 성능을 향상시킬 수 있음.

Date dateCellValue = cell.getDateCellValue(); 를 사용하여 셀 값을 날짜로 가져와주었다.

문제점

java.lang.IllegalStateException: Cannot get a NUMERIC value from a STRING cell
엑셀 셀의 타입이 숫자(NUMERIC)인데 문자열(STRING)로 값을 가져오려고 할 때 발생
에헤이.
지금 로직은 getCellValue를 통해 형식을 추출해서 parseExcel 메서드 - Map<String, Object> rowData에 데이터를 넣어주고 있다.
아까는 rowData에서 날짜형식과 정수 형식을 맞추어줬는데, 결국 getCellValue에서 형식을 전달해주는 것이니 그 처리가 옮겨져야한다.

링크텍스트 이 블로그에서는 여러가지 해결방법을 제시하고 있다. 이중 '다' 방법을 사용했다. cell type을 변경해서 put 해주는 방식.

혹시몰라 엑셀도 다시 만졌다.

version_3

package com.fit.restapi;

import java.io.IOException;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.fit.service.EmpService;

import java.text.SimpleDateFormat;
import java.util.Date;

@RestController
public class ExcelUploadRest {
    
    @Autowired
    private EmpService empService;

    @PostMapping("/excelUpload")// 엑셀 파일을 파싱하여 JSON 데이터로 변환하는 메서드
    private List<Map<String, Object>> parseExcel(MultipartFile file) throws IOException {
        List<Map<String, Object>> jsonDataList = new ArrayList<>();
        
        try (InputStream inputStream = file.getInputStream()) {
        	// InputStream은 Java의 기본 클래스.
        	// 데이터를 읽어오는 데 사용되는 '입력 스트림'을 표현하는 인터페이스.
        	// 파일, 네트워크 연결, 메모리 등 다양한 소스로부터 데이터를 읽을 때 사용한다.
            Workbook workbook = WorkbookFactory.create(inputStream); // 엑셀 파일을 읽어 Workbook 객체 생성
            Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트 가져오기

            // 헤더 행 추출
            Row headerRow = sheet.getRow(0); // 첫 번째 행을 헤더 행으로 추출
            List<String> headers = new ArrayList<>();
            for (Cell cell : headerRow) {
                headers.add(cell.getStringCellValue()); // 헤더 행의 각 셀의 값을 가져와서 헤더 리스트에 추가
            }
            
         // 데이터 행 추출 및 변환
            for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
                Row dataRow = sheet.getRow(rowIndex); // 데이터 행을 추출
                if (dataRow != null) {
                    Map<String, Object> rowData = new HashMap<>(); // 한 행의 데이터를 저장할 Map 객체 생성
                    for (int cellIndex = 0; cellIndex < headers.size(); cellIndex++) {
                        Cell cell = dataRow.getCell(cellIndex); // 해당 셀 추출
                        if (cell != null) {
                            String header = headers.get(cellIndex); // 현재 셀의 헤더 이름 가져오기
                            rowData.put(header, getCellValue(cell, header)); // 셀 값을 추출하여 rowData에 저장
                        }
                    }
                    jsonDataList.add(rowData); // 변환된 데이터를 리스트에 추가
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return jsonDataList; // 변환된 JSON 데이터 리스트 반환
    }
    // 셀의 값을 변환하여 반환하는 메서드
    private Object getCellValue(Cell cell, String header) {
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue(); // 문자열 값 반환
            case NUMERIC:
                if ("입사일".equals(header)) {
                    Date dateCellValue = cell.getDateCellValue(); // 셀 값을 날짜로 가져옴.
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // '월'인 MM은 '분' mm과 겹칠 수 있어 대문자로 표기한다고 함.
                    String formattedDate = dateFormat.format(dateCellValue); // 포맷된 날짜 변수를 생성
                    return formattedDate; // 포맷된 날짜를 반환
                } else if ("사원번호".equals(header) || "권한".equals(header)) {
                    return (int) cell.getNumericCellValue(); // 실수를 정수로 변환하여 반환
                } else {
                    return cell.getNumericCellValue(); // 숫자 값 반환
                }
            case BOOLEAN:
                return cell.getBooleanCellValue(); // 불리언 값 반환
            default:
                return null; // 다른 타입의 값은 null로 반환
        }
    }

    public String uploadExcel(@RequestParam("file") MultipartFile file) {
        try {
            List<Map<String, Object>> jsonDataList = parseExcel(file);
            empService.excelProcess(jsonDataList); //서비스에 DB insert를 실행
            return "success"; // 성공 페이지로 리턴하거나 메시지를 반환
        } catch (IOException e) {
            e.printStackTrace();
            return "error"; // 오류 페이지로 리턴하거나 메시지를 반환
        }
    }
    
}

문제점
또 오류...
HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=7m46s838ms927µs700ns).
이제는 스레드의 문제다. 한번에 너무 많은 처리가 있어서 스레드 풀의 맥시멈 값을 늘려 설정해줘야한다.

application.properties에서 추가 설정을 해주었다.

# HikariCP: 자바에서 사용되는 데이터베이스 커넥션 풀 라이브러리
# Thread starvation or clock leap detected 오류 보완: 스레드 풀 내에서 스레드가 부족
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

version_4

<%@ 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">
<title>registEmp</title>
	<!-- JQuery -->
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
	<!-- xls 형식을 파싱하기 위해 SheetJS 라이브러리의 xls 모듈을 사용 -->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js"></script>
	<script>
		$(document).ready(function() { // 웹 페이지가 모든 html 요소를 로드한 후에 내부(JQuery)의 코드를 실행하도록 보장
			
			// 취소 버튼 클릭 시
			$('#cancelBtn').click(function() {
				let result = confirm('HOME으로 이동할까요?'); // 사용자 선택 값에 따라 true or false 반환
				if (result) {
					window.location.href = '/home'; // Home으로 이동
				}
			});
		
			// 업로드 버튼 클릭 시
	        $('#uploadBtn').click(function(event) {
	            const fileInput = $('#fileInput');

	            if (fileInput.get(0).files.length === 0) {
	                event.preventDefault(); // 기본 동작 중단
	                alert('파일을 선택해주세요.');
	                return false;
	            }

	            const file = fileInput.get(0).files[0]; // 선택된 파일 가져오기
	            const fileName = file.name;
	            const fileExtension = fileName.split('.').pop().toLowerCase();

	            // 엑셀 파일이 아닌 경우 업로드 막기
	            if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
	                event.preventDefault(); // 기본 동작 중단
	                alert('엑셀 파일(xlsx 또는 xls)만 선택해주세요.');
	                return false;
	            }

	            const formData = new FormData(); // FormData 객체 생성
	            formData.append('file', file); // FormData에 엑셀 파일 추가

	            const reader = new FileReader(); // FileReader 객체 생성
	            reader.onload = function(event) {
	                const data = new Uint8Array(event.target.result); // 파일 데이터를 Uint8Array 형태로 읽어들임

	                const workbook = XLSX.read(data, { type: 'array' }); // 엑셀 파일을 배열 형태로 읽어들임
	                const sheetName = workbook.SheetNames[0]; // 첫 번째 시트 이름 가져오기

	                const sheet = workbook.Sheets[sheetName]; // 첫 번째 시트 가져오기

	                // 시트 데이터를 JSON 형태로 변환하여 jsonData 변수에 저장
	                const jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 });

	                formData.append('jsonData', JSON.stringify(jsonData)); // 파싱된 데이터를 FormData에 추가

	                // 파싱된 데이터를 서버로 전송
	                $.ajax({
	                    url: '/excelUpload', // 엔드포인트 URL 설정
	                    type: 'POST',
	                    data: formData, // FormData 전송
	                    processData: false, // 데이터 처리 방지 (FormData를 직접 보내므로 false 설정)
	                    contentType: false, // 컨텐츠 타입 자동 설정 방지
	                    success: function(response) {
	                        console.log(response); // 업로드 및 저장에 성공했을 시
	                        alert("사원 정보가 업로드 되었습니다."); // 알림 메시지 표시
	                        window.location.href = '/emp/empList'; // 사원 목록 페이지로 리다이렉트
	                    },
	                    error: function(xhr, status, error) {
	                        console.error(error);
	                        $('#msg').text('파일 업로드에 실패했습니다. 파일을 다시 선택해주세요.'); // span 태그에 메시지 표시
	                        $('#fileInput').val(''); // 파일 선택 입력란 비우기
	                    }
	                });
	            };
	            reader.readAsArrayBuffer(file); // 파일 데이터를 ArrayBuffer 형태로 읽어들임
	        });
		});
	</script>
<style>
	hr {
	    border: solid 3px black;
	    width: 20%;
	    margin: 0; /* auto 가운데 정렬 */
	}

	table {
		text-align: center;
	}
</style>
</head>
<body>
	<h3>사원 등록</h3>
	
	<!-- 엑셀 공통 양식 다운로드 버튼 추가 -->
	<a href="/upload/excelForm.xlsx" download="excelForm.xlsx">엑셀 공통 양식 다운로드</a>
	
	<!-- 파일 업로드 -->
	<form id="uploadForm" action="/excelUpload" method="post" enctype="multipart/form-data">
		<input type="file" name="file" id="fileInput">
		<button type="submit" id="uploadBtn">저장</button>
		<span id="msg"></span>
	</form>
	
	<!-- 사원 정보 등록 -->
	<form action="/emp/registEmp" method="post">
	<input type="hidden" name="empState" value="재직">
	<input type="hidden" name="remainDays" value="0.0">
		<table border="1">
			<tr>
				<td>사원번호</td>
				<td><input type="number" name="empNo" value="${empNo}"></td>
				<td><button type="button">사원번호 생성</button></td>
			</tr>
			<tr>
				<td>사원명</td>
				<td colspan="2"><input type="text" name="empName"></td>
			</tr>
			<tr>
				<td>부서명</td>
				<td colspan="2">
					<select name="deptName">
						<option value="" <c:if test="${emp.deptName.equals('')}">selected</c:if>>없음</option>
						<c:forEach var="d" items="${deptList}">
							<option value="${d.deptName}" <c:if test="${emp.deptName.equals(d.deptName)}">selected</c:if>>${d.deptName}</option>
						</c:forEach>
					</select>
				</td>
			</tr>
			<tr>
				<td>팀명</td>
				<td colspan="2">
					<select name="teamName">
						<option value="" <c:if test="${emp.teamName.equals('')}">selected</c:if>>없음</option>
						<c:forEach var="t" items="${teamList}">
							<option value="${t.teamName}" <c:if test="${emp.teamName.equals(t.teamName)}">selected</c:if>>${t.teamName}</option>
						</c:forEach>
					</select>
				</td>
			</tr>
			<tr>
			    <td>직급</td>
			    <td colspan="2">
			        <select name="empPosition">
						<option value="CEO" <c:if test="${emp.empPosition.equals('CEO')}">selected</c:if>>CEO</option>
						<option value="부서장" <c:if test="${emp.empPosition.equals('부서장')}">selected</c:if>>부서장</option>
						<option value="팀장" <c:if test="${emp.empPosition.equals('팀장')}">selected</c:if>>팀장</option>
						<option value="부팀장" <c:if test="${emp.empPosition.equals('부팀장')}">selected</c:if>>부팀장</option>
						<option value="사원" <c:if test="${emp.empPosition.equals('사원')}">selected</c:if>>사원</option>
					</select>
			    </td>
			</tr>
			<tr>
			    <td>권한</td>
			    <td colspan="2">
			        <select name="accessLevel">
						<option value="0" <c:if test="${emp.accessLevel.equals('0')}">selected</c:if>>0레벨</option>
						<option value="1" <c:if test="${emp.accessLevel.equals('1')}">selected</c:if>>1레벨</option>
						<option value="2" <c:if test="${emp.accessLevel.equals('2')}">selected</c:if>>2레벨</option>
						<option value="3" <c:if test="${emp.accessLevel.equals('3')}">selected</c:if>>3레벨</option>
					</select>
			    </td>
			</tr>
			<tr>
				<td>입사일</td>
				<td colspan="2">
					<input type="date" name="employDate">
				</td>
			</tr>
		</table>
		<br>
		<hr><!-- 구분선 -->
		<br>
		<button type="button" id="cancelBtn">취소</button><!-- 좌정렬 -->
		<button type="submit" id="saveBtn">등록</button><!-- 우정렬 -->
		<!-- 목록으로 보내기 -->
	</form>
</body>
</html>

아... 파싱해서 보내는 부분이 지금 두개가 있는 거였다.
view에서도 파싱해서 restController에 보내고, restController에서도 파싱해서 db에 보내고 있었다.

일단 분리했다.

version_5

restController

 			String stringValue = cell.getStringCellValue();
            if (isValidDateFormat(stringValue, "yyyy-MM-dd")) {
                return stringValue;
            } else {
                // 유효한 날짜 형식이 아닐 경우 오류 메시지 반환
                return "Invalid Date Format: " + stringValue;
            }
            
 ...
 
 			else if ("권한".equals(header)) {
                    log.error(CC.YE + "ExcelUpload.getCellValue() 권한 정제" + CC.RESET);
                    // 권한 값 처리 (소수점도 고려)
                    double permissionValue = cell.getNumericCellValue();
                    log.error(CC.YE + "ExcelUpload.getCellValue() permissionValue double값 확인: " + permissionValue + CC.RESET);
                    int permissionInt = (int) permissionValue;
                    log.error(CC.YE + "ExcelUpload.getCellValue() permissionInt int로 변환: " + permissionInt + CC.RESET);
                    String permissionStr = String.valueOf(permissionInt);
                    log.error(CC.YE + "ExcelUpload.getCellValue() permissionStr string으로 변환: " + permissionStr + CC.RESET);
                    return permissionStr;

이제는 여기가 문제가 있는 것 같다. 날짜를 파싱해오는 과정에서 시간의 충돌? 같은 게 있는 듯 하다.
또, 권한이 자꾸 0.0으로 받아지니까, 정수로 변환시켰다가 문자열로 변환했어야 했는데 이 과정에서 ERROR가 발생했다.

그런데

와 세상에...

계속 ERROR라고 뜨던 콘솔창... 그 이유는 바로 디버깅 코드를 잘못 적었기 때문이었다. ^0^

gpt의 답변
콘솔 창에 표시되는 로그 메시지가 "ERROR"로 표시되는 이유는 코드 내에 로그 메시지 출력 시 로그 레벨을 "ERROR"로 설정해 놓았기 때문입니다. 코드에서 log.error를 사용하여 로그 메시지를 출력하고 있습니다.

일반적으로 로그 레벨은 다음과 같이 분류됩니다.

ERROR: 심각한 오류를 나타내는 경우
WARN: 경고 메시지
INFO: 정보성 메시지
DEBUG: 디버그 정보
TRACE: 상세한 디버그 정보

내가 설정해놓은 디버깅 값이었는데ㅋㅋㅋㅋㅋㅋㅋㅋ
무슨 심각한 오류가 있는 것으로 인지하고 멀쩡한 코드를 자꾸 들여다보았다...
코드 많이 보는 건 좋은거니까(?) 손해는 없는거야... 응... 맞어...

version_6 최최최최최종!

properties에서도 추가 설정한 shread의 max값 설정 등을 지웠다.

그리고 또 한가지 깨달은 것은..!!

package com.fit.restapi;

import java.io.IOException;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.fit.CC;
import com.fit.service.EmpService;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@RestController
public class ExcelUploadRest {
    
    @Autowired
    private EmpService empService;

    @PostMapping("/excelUpload")
    public String uploadExcel(@RequestParam("file") MultipartFile file) {
        try {
            List<Map<String, Object>> jsonDataList = parseExcel(file);
            log.debug(CC.YE + "ExcelUpload.uploadExcel() jsonDataList: " + jsonDataList + CC.RESET);
            empService.excelProcess(jsonDataList); //서비스에 DB insert를 실행
            String msg = "=?success";
            return "/emp/empList"+msg; // 성공 페이지로 리턴하거나 메시지를 반환
        } catch (IOException e) {
        	log.error(CC.YE + "ExcelUpload.uploadExcel() error: " + e.getMessage() + CC.RESET);
            e.printStackTrace();
            String msg = "=?fail";
            return "/emp/registEmp"+msg; // 오류 페이지로 리턴하거나 메시지를 반환
        }
    }
    
    // 엑셀 파일을 파싱하여 JSON 데이터로 변환하는 메서드
    private List<Map<String, Object>> parseExcel(MultipartFile file) throws IOException {
        List<Map<String, Object>> jsonDataList = new ArrayList<>();
        
        try (InputStream inputStream = file.getInputStream()) {
        	// InputStream은 Java의 기본 클래스.
        	// 데이터를 읽어오는 데 사용되는 '입력 스트림'을 표현하는 인터페이스.
        	// 파일, 네트워크 연결, 메모리 등 다양한 소스로부터 데이터를 읽을 때 사용한다.
            Workbook workbook = WorkbookFactory.create(inputStream); // 엑셀 파일을 읽어 Workbook 객체 생성
            Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트 가져오기

            // 헤더 행 추출
            Row headerRow = sheet.getRow(0); // 첫 번째 행을 헤더 행으로 추출
            List<String> headers = new ArrayList<>();
            for (Cell cell : headerRow) {
                headers.add(cell.getStringCellValue()); // 헤더 행의 각 셀의 값을 가져와서 헤더 리스트에 추가
            }
            
            log.debug(CC.YE + "ExcelUpload.parseExcel() Headers: " + headers + CC.RESET);
            
         // 데이터 행 추출 및 변환
            for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
                Row dataRow = sheet.getRow(rowIndex); // 데이터 행을 추출
                if (dataRow != null) {
                    Map<String, Object> rowData = new HashMap<>(); // 한 행의 데이터를 저장할 Map 객체 생성
                    for (int cellIndex = 0; cellIndex < headers.size(); cellIndex++) {
                        Cell cell = dataRow.getCell(cellIndex); // 해당 셀 추출
                        if (cell != null) {
                            String header = headers.get(cellIndex); // 현재 셀의 헤더 이름 가져오기
                            log.debug(CC.YE + "ExcelUpload.parseExcel() header: " + header + CC.RESET);
                            rowData.put(header, getCellValue(cell, header)); // 셀 값을 추출하여 rowData에 저장
                            log.debug(CC.YE + "ExcelUpload.parseExcel() rowData: " + rowData + CC.RESET);
                        }
                    }
                    jsonDataList.add(rowData); // 변환된 데이터를 리스트에 추가
                }
            }
        } catch (Exception e) {
        	log.debug(CC.YE + "error" + CC.RESET);
            e.printStackTrace();
        }

        return jsonDataList; // 변환된 JSON 데이터 리스트 반환
    }
    // 셀의 값을 변환하여 반환하는 메서드
    private Object getCellValue(Cell cell, String header) {
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue(); // 문자열 값 반환
            case NUMERIC:
                if ("입사일".equals(header)) {
                    Date dateCellValue = cell.getDateCellValue(); // 셀 값을 날짜로 가져옴.
                    log.debug(CC.YE + "ExcelUpload.getCellValue() dateCellValue: " + dateCellValue + CC.RESET);
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // '월'인 MM은 '분' mm과 겹칠 수 있어 대문자로 표기한다고 함.
                    String formattedDate = dateFormat.format(dateCellValue); // 포맷된 날짜 변수를 생성
                    log.debug(CC.YE + "ExcelUpload.getCellValue() formattedDate: " + formattedDate + CC.RESET);
                    return formattedDate; // 포맷된 날짜를 반환
                } else if ("사원번호".equals(header)) {
                    log.debug(CC.YE + "ExcelUpload.getCellValue() 사원번호 정제" + CC.RESET);
                    return (int) cell.getNumericCellValue(); // 실수를 정수로 변환하여 반환
                } else if ("권한".equals(header)) {
                	double permissionValue = cell.getNumericCellValue();
                	log.debug(CC.YE + "ExcelUpload.getCellValue() 권한 값을 double 값 확인: " + permissionValue + CC.RESET);
                    String permissionStr = String.valueOf(permissionValue);
                    log.debug(CC.YE + "ExcelUpload.getCellValue() 권한 값을 문자열로 변환: " + permissionStr + CC.RESET);
                    if (permissionStr.endsWith(".0")) {
                        permissionStr = permissionStr.substring(0, permissionStr.length() - 2); // ".0" 제거
                        log.debug(CC.YE + "ExcelUpload.getCellValue() 문자열 권한 값의 소숫점 제거: " + permissionStr + CC.RESET);
                    }
                    return permissionStr;
                } else {
                    log.debug(CC.YE + "ExcelUpload.getCellValue() : 이외 나머지 값 정수로 변환" + CC.RESET);
                    return cell.getNumericCellValue(); // 숫자 값 반환
                }
            case BOOLEAN:
                log.debug(CC.YE + "ExcelUpload.getCellValue() boolean값 정제" + CC.RESET);
                return cell.getBooleanCellValue(); // 불리언 값 반환
            default:
                log.error(CC.YE + "ExcelUpload.getCellValue() 다른 타입의 값을 null 처리 " + CC.RESET);
                return null; // 다른 타입의 값은 null로 반환
        }
    }
}

이 restController의 postMapping 설정의 오류이다.

먼저 실행되어야하는 메소드를 잘못 지정해서, service 단과 연결해주는 최종 메소드가 실행되지 않았던 것이다.

그래서 service단으로 안가졌고, 쿼리가 실행되지 않았던 것이다.
순서는 다음과 같았다.
1. uploadExcel 메소드 실행.
2. parseExcel로부터 file을 json으로 바꾼 값을 받음.
3. 그 과정에서 getCellValue 메소드로 형식값을 처리하여 사용.
4. parseExcel의 결과값인 jsonDataList를 empService단 excelProcess 메소드에 전달.

이런 과정이었다!!!! ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ 허어어어어어어 너무 감격스럽다ㅠㅠㅠㅠㅠ

최최최최최최종!!

다 된줄 알았는데 문제 발생. redirect가 안된다.

registEmp.jsp

<%@ 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">
<title>registEmp</title>
	<!-- JQuery -->
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
	<!-- xls 형식을 파싱하기 위해 SheetJS 라이브러리의 xls 모듈을 사용 -->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js"></script>
	<script>
		$(document).ready(function() { // 웹 페이지가 모든 html 요소를 로드한 후에 내부(JQuery)의 코드를 실행하도록 보장
			
			// 취소 버튼 클릭 시
			$('#cancelBtn').click(function() {
				let result = confirm('HOME으로 이동할까요?'); // 사용자 선택 값에 따라 true or false 반환
				if (result) {
					window.location.href = '/home'; // Home으로 이동
				}
			});
		
			// 업로드 버튼 클릭 시
	        $('#uploadBtn').click(function(event) {
	            const fileInput = $('#fileInput');

	            if (fileInput.get(0).files.length === 0) {
	                event.preventDefault(); // 기본 동작 중단
	                alert('파일을 선택해주세요.');
	                return false;
	            }

	            const file = fileInput.get(0).files[0]; // 선택된 파일 가져오기
	            const fileName = file.name;
	            const fileExtension = fileName.split('.').pop().toLowerCase();

	            // 엑셀 파일이 아닌 경우 업로드 막기
	            if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
	                event.preventDefault(); // 기본 동작 중단
	                alert('엑셀 파일(xlsx 또는 xls)만 선택해주세요.');
	                return false;
	            }
	        });
			
	     	// 페이지 로딩 시 주소창 파라미터 확인 후 알림 표시
	        const urlParams = new URLSearchParams(window.location.search);
	        const failParam = urlParams.get('fail'); // '?' 제외한 파라미터 이름만 사용

	        if (failParam !== null) { // 파라미터 값이 있을 경우에만 알림 표시
	            alert('엑셀 파일 업로드에 실패했습니다. 양식에 맞는 엑셀파일을 제출해주세요.');
	        }
			
		});
	</script>
<style>
	/* 구분선 */
	hr {
	    border: solid 3px black;
	    width: 20%;
	    margin: 0; /* auto 가운데 정렬 */
	}
	/* 테이블 중앙 정렬 */
	table {
		text-align: center;
	}
</style>
</head>
<body>
	<h3>사원 등록</h3>
	
	<!-- 엑셀 공통 양식 다운로드 버튼 추가 -->
	<a href="/file/defaultTemplate.xlsx" download="defaultTemplate.xlsx">엑셀 공통 양식 다운로드</a>
	
	<!-- 파일 업로드 -->
	<form id="uploadForm" action="/excelUpload" method="post" enctype="multipart/form-data">
		<input type="file" name="file" id="fileInput">
		<button type="submit" id="uploadBtn">저장</button>
		<span id="msg"></span>
	</form>
	
	<!-- 사원 정보 등록 -->
	<form action="/emp/registEmp" method="post"><!-- 성공 시 사원목록 페이지로 -->
		<!-- 재직과, 남은휴가일수는 고정되어있으므로 hidden 타입으로 제출 -->
		<input type="hidden" name="empState" value="재직">
		<input type="hidden" name="remainDays" value="0.0">
		
		<table border="1">
			<tr>
				<td>사원번호</td>
				<td><input type="number" name="empNo" value="${empNo}"></td><!-- 직접 입력 / 사원번호 랜덤 생성 후 자동 입력 -->
				<td><button type="button">사원번호 생성</button></td>
			</tr>
			<tr>
				<td>사원명</td>
				<td colspan="2"><input type="text" name="empName"></td>
			</tr>
			<tr>
				<td>부서명</td>
				<td colspan="2">
					<select name="deptName">
						<option value="" <c:if test="${emp.deptName.equals('')}">selected</c:if>>없음</option>
						<c:forEach var="d" items="${deptList}">
							<option value="${d.deptName}" <c:if test="${emp.deptName.equals(d.deptName)}">selected</c:if>>${d.deptName}</option>
						</c:forEach>
					</select>
				</td>
			</tr>
			<tr>
				<td>팀명</td>
				<td colspan="2">
					<select name="teamName">
						<option value="" <c:if test="${emp.teamName.equals('')}">selected</c:if>>없음</option>
						<c:forEach var="t" items="${teamList}">
							<option value="${t.teamName}" <c:if test="${emp.teamName.equals(t.teamName)}">selected</c:if>>${t.teamName}</option>
						</c:forEach>
					</select>
				</td>
			</tr>
			<tr>
			    <td>직급</td>
			    <td colspan="2">
			        <select name="empPosition">
						<option value="CEO" <c:if test="${emp.empPosition.equals('CEO')}">selected</c:if>>CEO</option>
						<option value="부서장" <c:if test="${emp.empPosition.equals('부서장')}">selected</c:if>>부서장</option>
						<option value="팀장" <c:if test="${emp.empPosition.equals('팀장')}">selected</c:if>>팀장</option>
						<option value="부팀장" <c:if test="${emp.empPosition.equals('부팀장')}">selected</c:if>>부팀장</option>
						<option value="사원" <c:if test="${emp.empPosition.equals('사원')}">selected</c:if>>사원</option>
					</select>
			    </td>
			</tr>
			<tr>
			    <td>권한</td>
			    <td colspan="2">
			        <select name="accessLevel">
						<option value="0" <c:if test="${emp.accessLevel.equals('0')}">selected</c:if>>0레벨</option>
						<option value="1" <c:if test="${emp.accessLevel.equals('1')}">selected</c:if>>1레벨</option>
						<option value="2" <c:if test="${emp.accessLevel.equals('2')}">selected</c:if>>2레벨</option>
						<option value="3" <c:if test="${emp.accessLevel.equals('3')}">selected</c:if>>3레벨</option>
					</select>
			    </td>
			</tr>
			<tr>
				<td>입사일</td>
				<td colspan="2">
					<input type="date" name="employDate">
				</td>
			</tr>
		</table>
		
		<br>
		<hr><!-- 구분선 -->
		<br>
		
		<button type="button" id="cancelBtn">취소</button><!-- 좌정렬 예정 -->
		<button type="submit" id="saveBtn">등록</button><!-- 우정렬 예정 -->
	</form>
</body>
</html>

ExcelUploadRest - restController

package com.fit.restapi;

import java.io.IOException;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.fit.CC;
import com.fit.service.EmpService;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@RestController
public class ExcelUploadRest {
    
    @Autowired
    private EmpService empService;

    @PostMapping("/excelUpload")
    public String excelUpload( @RequestParam("file") MultipartFile file ) { // MultiparFile 형식의 file을 요청
    	String result = "";
    	try {
            List<Map<String, Object>> jsonDataList = parseExcel(file); // 업로드된 엑셀 파일을 파싱(분석, 해부)하여 JSON 데이터로 변환
            log.debug(CC.YE + "ExcelUpload Post excelUpload() jsonDataList: " + jsonDataList + CC.RESET);
            
            empService.excelProcess(jsonDataList); // 변환된 데이터를 서비스를 통해 DB에 삽입
            
            result = "success";
            
        } catch (IOException e) {
        	log.error(CC.YE + "ExcelUpload Post excelUpload() error: " + e.getMessage() + CC.RESET);
            e.printStackTrace();
            
            result = "fail";
        }
    	if (result.equals("success")) {
            return "/emp/empList?result=" + result;
        } else {
            return "/emp/registEmp?result=" + result; // 실패 시 사원 등록 페이지로 리다이렉트 + 오류 메시지 전달
        }
    }
    
    // 엑셀 파일을 파싱하여 JSON 데이터로 변환하는 메서드
    private List<Map<String, Object>> parseExcel( MultipartFile file ) throws IOException { // excelUPload에서 호출하여 사용
        List<Map<String, Object>> jsonDataList = new ArrayList<>();
        
        try ( InputStream inputStream = file.getInputStream() ) { // import InputStream
        	/* InputStream = Java의 기본 클래스이자, 데이터를 '읽어오는 데 사용'되는 입력 스트림을 표현하는 인터페이스.
        	   파일, 네트워크 연결, 메모리 등 다양한 소스로부터 데이터를 읽을 때 사용. */
            Workbook workbook = WorkbookFactory.create(inputStream); // import poi Workbook/WorkbookFactory
            /* Workbook : 엑셀 파일의 논리적 구조를 나타내는 클래스
               WorkbookFactory : 엑셀 파일을 읽어 Workbook 객체 생성(.xlsx와 .xls 파일을 형식에 관계없이 다룸)
            */
            Sheet sheet = workbook.getSheetAt(0); // import Sheet, 첫 번째 시트 가져오기(인덱스는 0부터 시작하므로)

            // 헤더 행 추출
            Row headerRow = sheet.getRow(0); // 첫 번째 행을 헤더 행으로 추출
            List<String> headers = new ArrayList<>();
            for ( Cell cell : headerRow ) {
                headers.add(cell.getStringCellValue()); // 헤더 행의 각 셀의 값을 가져와서 헤더 ArrayList에 추가
            }
            log.debug(CC.YE + "ExcelUpload.parseExcel() Headers: " + headers + CC.RESET);
            
            // 데이터 행 추출 및 변환
            for ( int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++ ) { // 시트의 마지막 행까지 반복
                Row dataRow = sheet.getRow(rowIndex); // 데이터 행을 추출
                
                if ( dataRow != null ) { // 행이 있으면
                	
                	// 한 행의 키와 값을 함께 저장할 Map 객체 생성
                    Map<String, Object> rowData = new HashMap<>();
                    
                    // 헤더 셀의 수만큼 반복
                    for ( int cellIndex = 0; cellIndex < headers.size(); cellIndex++ ) {
                        Cell cell = dataRow.getCell(cellIndex); // 인덱스에 해당하는 셀 값을 추출
                        
                        if ( cell != null ) {
                            String header = headers.get(cellIndex); // 현재 셀의 헤더 이름 가져오기
                            log.debug(CC.YE + "ExcelUpload.parseExcel() header: " + header + CC.RESET);
                            
                            rowData.put( header, getCellValue( cell, header ) ); // 헤더 데이터 형식에 맞추어 값을 정제 -> rowData에 넣음
                            log.debug(CC.YE + "ExcelUpload.parseExcel() rowData: " + rowData + CC.RESET);
                        }
                    }
                    jsonDataList.add( rowData ); // 변환된 데이터를 리스트에 추가
                }
            }
        } catch ( Exception e ) {
        	log.debug(CC.YE + "ExcelUpload.parseExcel() error" + CC.RESET);
            e.printStackTrace();
        }

        return jsonDataList; // 변환된 JSON 데이터 리스트 반환
    }
    
    // 셀 데이터 형식을 변환하여 반환하는 메서드
    private Object getCellValue( Cell cell, String header ) { // parseExcel에서 호출하여 사용
    	// 셀의 타입에 따라
        switch ( cell.getCellType() ) {
        	
            case STRING: // 문자열 셀 데이터일 경우
                return cell.getStringCellValue(); // 문자열 값 반환
                
            case NUMERIC: // 숫자 셀 데이터일 경우
                if ( "입사일".equals(header) ) { // 숫자 셀의 헤더 값이 "입사일"일때
                	
                    Date dateCellValue = cell.getDateCellValue(); // 해당하는 셀의 값을 날짜로 가져와서 Java의 Date 객체로 변환
                    log.debug(CC.YE + "ExcelUpload.getCellValue() dateCellValue: " + dateCellValue + CC.RESET);
                    
                    /* 그런데 Date로 가져오면 ex) Thu Aug 17 00:00:00 KST 2023 이러한 형태로 들어오게 됨.
                       => SimpleDateFormat 객체를 만들어 날짜 형식 "yyyy-MM-dd"로 바꾸어줌. */
                    
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // '월' MM은 '분' mm과 겹칠 수 있어 대문자로 표기. 
                    
                    /* simpleDateFormat 처리를 거친 후 날짜 포맷 */
                    
                    String formattedDate = dateFormat.format(dateCellValue);
                    log.debug(CC.YE + "ExcelUpload.getCellValue() formattedDate: " + formattedDate + CC.RESET);
                    
                    return formattedDate; // 포맷된 날짜를 반환
                    
                } else if ( "사원번호".equals(header) ) { // 숫자 셀의 헤더 값이 "사원번호"일때
                    log.debug(CC.YE + "ExcelUpload.getCellValue() 사원번호 정제" + CC.RESET);
                    
                    return (int) cell.getNumericCellValue(); // 실수를 정수로 변환하여 반환
                    
                } else if ( "권한".equals(header) ) { // 숫자 셀의 헤더 값이 "권한"일때(원래는 ENUM 값)
                	double permissionValue = cell.getNumericCellValue(); // 권한 값을 double로 받아오기(엑셀에서 텍스트 형식으로 지정해도 더블로 받아옴)
                	log.debug(CC.YE + "ExcelUpload.getCellValue() 권한 값을 double 값 확인: " + permissionValue + CC.RESET);

                    String permissionStr = String.valueOf(permissionValue); // 권한 값을 문자열로 변환
                    log.debug(CC.YE + "ExcelUpload.getCellValue() 권한 값을 문자열로 변환: " + permissionStr + CC.RESET);
                    
                    if ( permissionStr.endsWith(".0") ) { // 문자열 상태의 권한 값이 .0으로 끝난다면
                        permissionStr = permissionStr.substring( 0, permissionStr.length() - 2 ); // ".0" 소숫점 제거
                        log.debug(CC.YE + "ExcelUpload.getCellValue() 문자열 권한 값의 소숫점 제거: " + permissionStr + CC.RESET);
                    }
                    
                    return permissionStr;
                } else { // 숫자 셀의 헤더 값이 예외적일때
                    log.debug(CC.YE + "ExcelUpload.getCellValue() : 이외 나머지 값 정수로 변환" + CC.RESET);
                    
                    return cell.getNumericCellValue(); // 모두 숫자 값을 반환
                }
                
            case BOOLEAN: // 불리언 셀 데이터일 경우
                log.debug(CC.YE + "ExcelUpload.getCellValue() boolean값 정제" + CC.RESET);
                
                return cell.getBooleanCellValue(); // 불리언 값 반환
                
            default: // 셀 데이터 타입을 모를 경우
                log.error(CC.YE + "ExcelUpload.getCellValue() 다른 타입의 값을 null 처리 " + CC.RESET);
                
                return null; // null로 반환
        }
    }
}

깨달은 것은. restController는 JSON 형태로 데이터를 반환하는 controller이고, url을 문자열로 반환하려고 했기 때문에 문제가 되는 것이었다.

-chat gpt의 답변-
PostMapping의 Response: excelUpload 메서드의 POST 매핑은 RestController의 역할을 수행하고 있습니다. 이 경우에는 REST API 엔드포인트를 정의하고 있기 때문에, 해당 메서드의 리턴값은 실제로는 JSON 형태의 데이터를 반환해야 합니다. 하지만 여기서는 리다이렉션 URL을 문자열로 반환하려고 시도하고 있습니다. 이것은 일반적인 방식이 아닙니다.

원래 있던 empController로 메소드를 옮기니 너무너무 redirect가 잘 된다!
정말 기능구현을 완료했다.

이제 후처리(사원번호 중복검사)가 남았다. 그건 내일 해야지...

0개의 댓글