기존에는 파일을 하나만 넣고 꺼내서 보여줄 수가 있었다.
하지만 내가 구현해야 하는 페이지에는 파일을 여러개 넘겨야 했다.
그리고 그것들을 모두 꺼내서 보여주는 페이지와 그 중에 하나의 이미지만 보여줘야 하는 페이지가 있었다.

reviewWrite.jsp 글을 작성하는 페이지에서 multiple 속성을 준다.
multiple 속성을 주면 파일을 여러개 선택할 수 있다.

파일을 넘겨야 하니까 Action처리로 넘길 때 꼭 multipart/form-data 지정해주기.
private List<String> uploadedFilenames; // 다중 파일 이름 저장
여러 이미지를 저장이 필요한 Vo에 컬럼을 새로 추가 해줘야 한다.
ArrayList로 각 방에 하나씩 담을겁니다.
// 다중 파일 업로드 메서드
public static List<String> uploadFiles(String uploadPath, List<MultipartFile> files) throws Exception {
// 업로드된 파일들의 이름을 저장할 리스트
List<String> uploadedFileNames = new ArrayList<>();
// 1. 업로드할 파일 리스트 반복
for (MultipartFile file : files) {
// 2. 파일이 비어있지 않은 경우에만 처리
if (!file.isEmpty()) {
// (1) 업로드된 파일의 원래 이름 가져오기
String originalName = file.getOriginalFilename();
// (2) 파일 데이터를 바이트 배열로 읽기
byte[] fileData = file.getBytes();
// (3) 파일 저장 메서드를 호출하여 파일 업로드 처리
String uploadedFileName = uploadFile(uploadPath, originalName, fileData);
// (4) 업로드된 파일 이름을 리스트에 추가
uploadedFileNames.add(uploadedFileName);
}
}
// 3. 업로드된 모든 파일들의 이름 리스트 반환
return uploadedFileNames;
}
util 폴더에 있는 UploadFileUtiles는 하나의 이미지만 업로드 할 수 있는 메서드기 때문에 다중파일을 처리하는 메서드를 새로 생성해야 한다.
uploadPath : 파일을 저장할 디렉토리 경로.files : 업로드한 파일 리스트. 각 파일은 MultipartFile 객체로 표현된다.uploadedFileNames : 업로드된 파일들의 이름을 저장할 빈 리스트를 생성. 업로드가 완료되면 이 리스트를 반환한다.for (MultipartFile file : files) : files 리스트를 순회해서 각각의 파일을 처리.originalName : 업로드된 파일의 원래 이름을 가져온다.fileData : 업로드된 파일의 내용을 바이트 배열로 읽어오기.uploadFile(): 메서드를 호출해서 파일 데이터를 지정된 경로(uploadPath)에 저장하고, 저장된 파일 이름(또는 경로)을 반환받는다.<!-- 새로운 게시물 추가하는 쿼리 -->
<insert id="reviewInsert" parameterType="rv">
insert into bakery_review(bakery_name, review_contents, midx, filename,address,bakery_phone,operating_hours,menu_info,parking_info,latitude,longitude,area)
values(#{bakery_name}, #{review_contents}, #{midx}, #{filename}, #{address}, #{bakery_phone}, #{operating_hours}, #{menu_info}, #{parking_info}, #{latitude}, #{longitude}, #{area})
</insert>
여기엔 기본 filename 컬럼을 집어 넣어주면 된다.
@RequestMapping(value = "reviewWriteAction.aws")
public String reviewWriteAction(
ReviewVo rv, // 작성된 게시글 데이터를 담을 객체
@RequestParam("attachfile") MultipartFile[] attachfiles, // 첨부된 파일 배열
HttpServletRequest request, // 사용자 요청 객체 (세션 정보 사용)
RedirectAttributes rttr // 리다이렉트 시 메시지 전달을 위한 객체
) throws Exception {
// 2. 파일 업로드 경로 설정
String uploadPath = "D:/dev/bakeryUpload"; // 파일이 저장될 디렉토리 경로
// 3. 첨부파일 배열을 리스트로 변환
List<MultipartFile> fileList = Arrays.asList(attachfiles);
// 4. 파일 업로드 유틸리티를 통해 파일 업로드 처리
List<String> uploadedFileNames = UploadFileUtiles.uploadFiles(uploadPath, fileList);
// 5. 업로드된 파일 이름 리스트를 쉼표로 구분된 문자열로 변환
String uploadedFileNamesStr = String.join(",", uploadedFileNames);
// 6. 사용자 세션에서 midx(사용자 ID) 가져오기
String midx = request.getSession().getAttribute("midx").toString();
int midx_int = Integer.parseInt(midx); // 문자열을 정수로 변환
// 7. ReviewVo 객체에 데이터 설정
rv.setMidx(midx_int); // 사용자 ID 설정
rv.setFilename(uploadedFileNamesStr); // 업로드된 파일 이름 문자열 설정
// 8. 게시글 데이터 삽입 서비스 호출
int value = reviewService.reviewInsert(rv);
// 9. 삽입 결과에 따라 리다이렉트 경로 설정
String path;
if (value == 1) { // 삽입 성공 시 게시글 목록으로 리다이렉트
path = "redirect:/review/reviewList.aws";
} else { // 삽입 실패 시 다시 작성 페이지로 리다이렉트
rttr.addFlashAttribute("msg", "입력이 잘못되었습니다."); // 에러 메시지 전달
path = "redirect:/review/reviewWrite.aws";
}
// 10. 최종 리다이렉트 경로 반환
return path;
}
ReviewController에서 글쓰기 기능을 처리하는 메서드.
reviewWriteAction.aws로 들어오는 POST 요청을 처리해서 작성된 데이터를 ReviewVo 객체에 바인딩하고 첨부된 파일들을 리스트로 변환한다.
그리고 UploadFileUtiles.uploadFiles()를 통해 서버에 저장하고 파일 이름 리스트를 쉼표로 연결된 문자열로 변환해서 ReviewVo에 설정. 회원 세션에서 가져온 midx를 추가로 설정하고, 이를 기반으로 reviewService.reviewInsert(rv)를 호출해서 데이터베이스에 게시글을 저장한다. 결과에 따라 성공 시 목록 페이지로 가고, 실패하면 다시 작성 페이지로 리다이렉트 경로를 반환한다.
reviewList.jsp 파일에서는 게시글 목록을 나타내고 있는데, 넘겨 받은 이미지 파일 중에서 하나의 이미지만 꺼내서 보여줘야 한다.
<!-- 빵집 추천 미리보기 게시물 띄우기 -->
<section class="recommend-section">
<div class="card-container">
<%
int num = totalCount - (pm.getScri().getPage() - 1) * pm.getScri().getPerPageNum(); // 현재 페이지의 첫 번째 게시글 번호 계산
for (ReviewVo rv : rlist) { // 게시물 리스트 순회
// 여러 개의 이미지 파일 이름을 쉼표로 구분하여 배열로 나누기
String[] filenames = rv.getFilename().split(",");
// 첫 번째 이미지 파일만 가져오기
String firstImage = filenames.length > 0 ? filenames[0] : ""; // 첫 번째 이미지만 가져옴
%>
<a href="<%=request.getContextPath() %>/review/reviewContents.aws?review_id=<%=rv.getReview_id() %>" class="link">
<div class="card">
<!-- 첫 번째 이미지만 표시 -->
<img src="<%= request.getContextPath() %>/review/displayFile.aws?fileName=<%= firstImage %>" alt="추천 빵집">
<div class="card-content">
<div class="title-container">
<h3><strong><%=rv.getBakery_name()%></strong></h3>
</div>
<hr class="freview"> <!-- 미리보기 구분선 -->
<div class="contact-info">
<p><span class="label">주소:</span>
<span class="value"><%=rv.getAddress()%></span></p>
</div>
<hr class="freview1"> <!-- 미리보기 구분선 -->
<p><%=rv.getReview_contents()%></p>
</div>
</div>
</a>
<%
num = num-1;
}
%>
</div>

for (ReviewVo rv : rlist) : ReviewVo 객체들의 목록이 저장되어 있는 rlist로 게시물 리스트를 순회한다.
String[] filenames = rv.getFilename().split(","); : 하나의 게시물에서 여러 이미지 파일 이름을 쉼표(,)로 나눈 배열로 만든다.
rv.getFilename() : 이미지 파일 이름 문자열을 반환하고,
.split(",") : 쉼표를 기준으로 문자열을 분리해서 배열(String[])에 저장.
String firstImage = filenames.length > 0 ? filenames[0] : ""; : 분리된 이미지 파일 배열에서 첫 번째 이미지를 가져오기.
filenames.length > 0 : 이미지가 존재하는지 확인.
filenames[0] : 배열의 첫 번째 요소(첫 번째 이미지)
이미지가 없는 경우에는 "" 빈 문자열로 설정한다.
<img src="<%= request.getContextPath() %>/review/displayFile.aws?fileName=<%= firstImage %>" alt="추천 빵집"> : 첫 번째 이미지를 카드 내에 표시하기.
/review/displayFile.aws : 이미지를 표시하는 컨트롤러 경로.
fileName=<%= firstImage %> : 첫 번째 이미지 파일 이름을 매개변수로 전달해서 서버에서 이미지를 가져온다.
이제 reviewContents.jsp 게시물 내용을 보여주는 파일에서는 모든 이미지 파일을 꺼내서 보여주면 된다.
<div class="bakery-container">
<!-- 이미지 슬라이더 -->
<div class="image-slider">
<c:forEach var="fileName" items="${fileNames}">
<img src="${pageContext.request.contextPath}/review/displayFile.aws?fileName=${fileName}" alt="이미지">
</c:forEach>
<!-- 슬라이더 내비게이션 -->
<button class="prev">❮</button>
<button class="next">❯</button>
</div>
<c:forEach var="fileName" items="${fileNames}"> :
설명: JSP의 JSTL 태그입니다. 서버에서 전달된 이미지 파일 목록(fileNames)을 순회하며 각 파일 이름을 처리합니다.
var="fileName" : 각 반복(iteration)마다 fileName이라는 변수에 현재 파일 이름을 저장합니다.
items="${fileNames}" : fileNames는 서버에서 전달된 이미지 파일 이름 목록입니다. 이는 배열이나 List와 같은 컬렉션일 수 있습니다.
4. <img src="${pageContext.request.contextPath}/review/displayFile.aws?fileName=${fileName}" alt="이미지">
설명: 순회 중인 fileName을 사용하여 이미지 태그()를 동적으로 생성합니다.
src: 이미지 파일의 경로.
${pageContext.request.contextPath}: 현재 웹 애플리케이션의 루트 경로를 가져옵니다.
/review/displayFile.aws: 이미지를 제공하는 서버 경로.
fileName=${fileName}: fileName 변수에 저장된 파일 이름을 쿼리 매개변수로 전달하여 서버에서 이미지를 가져옵니다.
alt="이미지": 이미지가 표시되지 않을 때 대체 텍스트로 "이미지"를 제공합니다.
슬라이더 이미지가 제대로 동작하기 위해서는 script를 사용해야 한다.
document.addEventListener('DOMContentLoaded', () => {
let currentIndex = 0; // 현재 이미지 인덱스
const images = document.querySelectorAll('.image-slider img'); // 이미지들
const prevButton = document.querySelector('.prev');
const nextButton = document.querySelector('.next');
if (images.length > 0) {
images[currentIndex].classList.add('active'); // 초기 활성화 이미지 설정
prevButton.addEventListener('click', () => {
images[currentIndex].classList.remove('active'); // 현재 이미지 비활성화
currentIndex = (currentIndex === 0) ? images.length - 1 : currentIndex - 1; // 첫 번째 이미지로 돌아감
images[currentIndex].classList.add('active'); // 새로운 이미지 활성화
});
nextButton.addEventListener('click', () => {
images[currentIndex].classList.remove('active'); // 현재 이미지 비활성화
currentIndex = (currentIndex === images.length - 1) ? 0 : currentIndex + 1; // 마지막 이미지에서 첫 번째로 돌아감
images[currentIndex].classList.add('active'); // 새로운 이미지 활성화
});
}
});
let currentIndex = 0; // 현재 활성화된 이미지의 인덱스
const images = document.querySelectorAll('.image-slider img'); // 이미지 슬라이더 안의 모든 이미지 요소
const prevButton = document.querySelector('.prev'); // 이전 버튼
const nextButton = document.querySelector('.next'); // 다음 버튼
images: JSP에서 생성된 모든 <img> 요소를 가져옵니다.
prevButton, nextButton: HTML의 버튼 요소를 가져옵니다.
if (images.length > 0) {
images[currentIndex].classList.add('active'); // 초기 활성화
}
이미지가 존재하면 첫 번째 이미지를 활성화(active 클래스 추가)합니다.
active 클래스는 CSS에서 해당 이미지를 보여주거나 강조하는 데 사용됩니다.
prevButton.addEventListener('click', () => {
images[currentIndex].classList.remove('active'); // 현재 이미지 비활성화
currentIndex = (currentIndex === 0) ? images.length - 1 : currentIndex - 1; // 이전 이미지로 이동
images[currentIndex].classList.add('active'); // 새로운 이미지 활성화
});
현재 활성화된 이미지의 active 클래스를 제거합니다.
현재 인덱스가 0이라면 마지막 이미지로 이동, 그렇지 않으면 이전 인덱스로 이동합니다.
새로운 이미지에 active 클래스를 추가해 활성화합니다.
nextButton.addEventListener('click', () => {
images[currentIndex].classList.remove('active'); // 현재 이미지 비활성화
currentIndex = (currentIndex === images.length - 1) ? 0 : currentIndex + 1; // 다음 이미지로 이동
images[currentIndex].classList.add('active'); // 새로운 이미지 활성화
});
현재 활성화된 이미지의 active 클래스를 제거합니다.
현재 인덱스가 마지막 이미지라면 첫 번째로 이동, 그렇지 않으면 다음 인덱스로 이동합니다.
새로운 이미지에 active 클래스를 추가해 활성화합니다.