SNS의 제일 중요한 기능 중 하나는 사진을 보여주는 기능이라고 생각한다.
사진 없이 글만 적는 기능만 있으면 너무 딱딱하고 재미 없을 것 같기때문에,,
또한, 중요한 점이 사용자가 정해놓은 순서대로 사진을 보여주는 것이라고 생각한다.
인스타그램에서도 글을 올릴 때 사진 순서를 항상 이리바꿔보고 저리바꿔보고 하면서 맘에드는 순서대로 업로드해왔었기 때문에,,,
이 점을 중요하게 생각하면서 코드를 짜보겠다!
일단 프론트에서 사진들은 순서에 맞게 서버 쪽에 넘겨줘야한다.
어떤식으로 넘겨줄까 고민을 많이 해봤는데,
처음에 저장할 때에는 크게 신경 쓸 필요가 없다. 왜냐하면 사용자가 넘겨준 사진을 그대로 넘겨주면 되기 때문!
하지만 수정할 때가 문제이다. 사용자가 이미 업로드된 사진들 중 일부분을 삭제하고 순서를 바꾸고 새로 업로드할 수 있어서 문제가 생김
그대로 전달하게 되면 서버쪽에서는 순서만 알 수 있고 어떤 사진이 삭제되었는지, 어떤 사진이 새로 업로드 된 사진인지 구별을 못한다.
고민끝에 생각해낸 방법은 인스타그램 방식대로 하는 것..!
(원래는 드래그앤 드랍으로 사진 순서를 바꾸는 걸 생각해봤지만 이 방식은 도저히 새로 업로드 된 사진과 이미 기존에 있었던 사진을 구별하는 방법이 떠오르지 않았다)
인스타그램 방식은 기존에 올려놓은 사진의 순서를 바꾸고 싶다면 이미 업로드된 사진들을 지우고 자신이 원하는 순서대로 다시 업로드 하는 방식이다. (아닐 수도... 인스타를 많이 안해봐가지고... 내가 기억하기론 그렇다)
이런 방식대로 구현을 해보겠다. 생성과 수정은 비슷한 로직이므로 더 어려운 수정만 설명하겠다.
<section class="image-container">
<button class="prev-btn" onclick="prevSlide()">❮</button>
<div class="slider-container">
<ul class="image-list">
<li class="image-item">
<div class="image-upload-button" onclick="openFileUploader()">+</div>
<p class="image-rule-text">사진을 업로드 해주세요!<br>최대 10개까지 업로드 가능합니다.</p>
</li>
<li class="image-preview" th:each="image, imageIndex:${board.imageUrls}">
<button class="image-delete-button">x</button>
<img class="image-item" th:src="${image}" th:value="${board.getImageNames[__${imageIndex.index}__]}" alt="첨부된 이미지">
<span class="image-index" th:text="${imageIndex.index+1}"></span>
</li>
</ul>
</div>
<button class="next-btn" onclick="nextSlide()">❯</button>
</section>
이 부분이 사진이 보여지는 html이다.
만약 사진이 업로드 된다면 image-list에 image-preview라는 이름으로 업로드 될 것이다.
기존에 있는 사진들은 서버로부터 s3에 저장된 사진 이름과 s3에 저장된 경로를 받게 된다. (board.imageUrls, board.getImageNames)
경로가 있는데 굳이 사진 이름까지 주는 이유는 이러하다.
- 기존 사진과 새로 업로드된 사진을 구별하기 위함
- 데이터베이스에서 삭제된 사진을 찾기 위함(사용자가 게시물을 업로드 할 시, 전달받은 사진 이름 중 포함되어있지 않은 사진 이름들은 데이터베이스에서 삭제)
function openFileUploader() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/**';
input.multiple = 'multiple';
input.onchange = handleFileUpload;
input.click();
}
function handleFileUpload(event) {
const files = event.target.files;
if (files.length > 0) {
const imageList = document.querySelector('.image-list');
const existingImages = imageList.querySelectorAll('.image-item');
// 이미지 리스트에 이미 있는 이미지의 개수
const existingImageCount = existingImages.length;
for (let i = 0; i < files.length; i++) {
if(existingImageCount+i>10){
alert("사진 업로드는 최대 10개까지 가능합니다.");
prevSlide();
return;
}
let imageItem = createImageItem(imageList,existingImageCount, i, files[i]);
const render = new FileReader();
// 파일 읽기 작업이 성공적으로 완료되었을 때 호출되는 콜백함수
// imageItem을 매개 변수로 받는다.
// e는 파일의 내용
// result에는 파일의 데이터 url이 포함된다.
render.onload = (function (aImg) {
return function (e) {
aImg.src = e.target.result;
}
})(imageItem);
//FileReader객체를 사용하여 파일을 읽는다.
render.readAsDataURL(files[i]);
}
prevSlide();
}
이것이 파일 업로드 하는 로직이다.
만약 <div class="image-upload-button">+</div> 이 div를 사용자가 누르게 된다면 파일 업로드 창이 뜰 것이고, handleFIleUpload가 실행된다.
- const files = event.target.files = 파일 업로드 이벤트에서 선택된 파일들을 가져온다.
- const imageList = document.querySelector('.image-list') = image-list밑에 img요소들 넣어야되기 때문에 가져온다.
- existingImages에 관련된 코드는 사진을 10개 이상 첨부하지 못하도록 하는 코드이다.
- createImageItem은 list안에 넣을 요소들을 구성해서 만들어주는 함수이다. 여기서는 그렇게 중요하지 않은 것 같아서 따로 첨부하지는 않겠다.
- render.onload = (function (aImg) { return function (e) { aImg.src = e.target.result; } })(imageItem) = FileReader 객체를 사용하여 파일을 읽어온 후, 이미지 요소의 src 속성을 파일 데이터의 URL로 설정한다.
- render.readAsDataURL(files[i]) = FileReader 객체를 사용하여 파일을 읽어온다. 파일의 내용을 읽은 후 onload 이벤트가 발생하면 이미지 요소의 src 속성이 설정된다.
- prevSlide() = 모든 파일에 대한 처리가 완료되면, 이전 슬라이드로 이동
이미지 업로드를 구현했으니 이젠 기존에 있는 이미지를 삭제하는 것을 구현해보자
function clickDeleteImage(deleteButton, li) {
const imageList = document.querySelector('.image-list');
deleteButton.addEventListener('click', function () {
const confirmation = confirm("삭제하시겠습니까?");
if (confirmation) {
// 삭제 버튼을 클릭했을 때 해당 이미지와 파일을 삭제
imageList.removeChild(li);
// 삭제된 이미지 이후의 이미지들의 인덱스를 업데이트
const imageItems = imageList.querySelectorAll('.image-preview');
imageItems.forEach((image, index) => {
const indexLabel = image.querySelector('span');
indexLabel.textContent = `${index+1}`;
});
}
prevSlide();
});
}
- imageList.removeChild(li)= 사용자가 삭제 버튼을 눌렀으면 imageList에서 li요소를 삭제한다.
- const imageItems = imageList.querySelectorAll('.image-preview') = 이미지 목록에서 클래스가 image-preview인 요소들을 모두 가져와 imageItems 변수에 할당한다. 이 요소들은 삭제된 이미지 이후의 이미지들이다.
- forEach문은 이미지 순서를 표시하는데 사용된다. 이미지의 순서를 나타내는 span 태그의 텍스트 내용을 현재 인덱스에 1을 더한 값으로 설정한다. 이를 통해 인덱스가 0부터 시작하는 것이 아니라 1부터 시작하도록 표시됨
이렇게 삭제할 사진들은 imageList에서 아예 삭제하도록 하였다. 이제는 이러한 사진 요소들을 서버에 보낼 차례이다.
function handleSubmit(url, method) {
const formData = new FormData(document.querySelector('.board-form'));
const imageList = document.querySelectorAll('.image-item');
if (imageList.length <= 1) {
alert("한 장 이상의 사진을 첨부해주세요.");
return;
}
for (let i = 1; i < imageList.length; i++) {
const file = imageList[i].file;
if (file) {
formData.append('images', file);
} else {
formData.append('imageUrls', imageList[i].getAttribute("value"));
}
}
fetch(url, {
method: method,
body: formData,
}).then(response => {
if (!response.ok) {
return response.text().then(msg => {
if (response.status === 401) {
alert(msg);
}else if(response.status===400){
alert(msg);
throw new Error(msg);
}else if(response.status===404){
alert("예상치 못한 에러가 발생하였습니다.");
}
});
} else {
return response.text();
}
}).then(url => {
if (url) {
window.location.replace(url);
}else{
window.location.replace("/");
}
}).catch(error => {
console.error(error);
});
}
- const formData = new FormData(document.querySelector('.board-form')) = 클래스가 board-form인 폼 요소의 데이터를 담기 위한 FormData 객체를 생성
- const imageList = document.querySelectorAll('.image-item') = HTML 문서 내의 모든 클래스가 image-item인 요소들을 선택하여 imageList에 저장
- if (imageList.length <= 1) = 이미지가 하나도 업로드되지 않았을 경우 경고 메세지 창 띄움 (1로 해놓은 경우는 image-item 중 제일 첫번째 요소는 사진 업로드 창(?)이기 때문)
- const file = imageList[i].file = 이미지 요소의 파일 객체를 가져온다. 여기가 제일 중요하다. 만약 새로 업로드된 파일이면 파일 객체가 존재하고, 기존에 있는 파일이라면 파일 객체가 없다.
for (let i = 1; i < imageList.length; i++) {
const file = imageList[i].file;
if (file) {
formData.append('images', file);
} else {
formData.append('imageUrls', imageList[i].getAttribute("value"));
}
}
중요하기 때문에 빼주었다.
- 만약 파일 객제가 있다면 images라는 이름으로 파일을 보내준다.
- 만약 파일 객체가 없다면(기존에 있는 이미지일 경우) imageUrls라는 이름으로 imageList의 사진 이름을 보내준다.
이렇게 되면 서버에서는 새로 업로드 된 파일과 기존에 있는 파일들을 구별 할 수 있게 된다.
포스팅이 너무 길어져서 서버쪽 로직은 다음 글에서 마저 작성하겠다.