✅ 상품등록
- 상품등록의 UI는 다음과 같이 구현하였다
위의 UI 부분에서 가장 어려웠던 사진등록 UI부분 + 카테고리 + 검색태그등록 UI + 가격 정규표현식에 대해서만 작성해보겠다
<img src="<%=request.getContextPath()%>/images/productregist/imgregist.png"
class="upload" width="250px" height="250px">
<input type="file" id="inputFile" class="real-upload"
accept="image/*" required multiple style="display: none;">
<!-- multiple -> 사용자가 둘 이상의 값을 입력할 수 있음을 명시 -->
<!-- email, file 타입에만 적용 가능함 -->
// 사진 불러오기 작업
let prouductImgCnt = 0;
const dataTransfer = new DataTransfer();
function getImageFiles(e) {
const files = e.currentTarget.files;
const imagePreview = document.querySelector('.image-preview');
const newFiles = [...files];
let currentCount = dataTransfer.items.length;
for (let file of newFiles) {
// 파일 타입 검사
if (!file.type.match("image/.*")) {
alert('이미지 파일만 업로드가 가능합니다.');
continue;
}
// 중복 검사: 이미 dataTransfer에 같은 파일 있는지 확인
let isDuplicate = false;
for (let i = 0; i < dataTransfer.items.length; i++) {
const existingFile = dataTransfer.items[i].getAsFile();
if (
existingFile.name === file.name &&
existingFile.lastModified === file.lastModified &&
existingFile.size === file.size
) {
isDuplicate = true;
break;
}
}
if (isDuplicate) {
alert(`이미 업로드한 이미지입니다: ${file.name}`);
continue;
}
// 총 이미지 수 검사 (현재 있는 이미지 + 새로 추가할 이미지)
if (currentCount >= 10) {
alert('이미지는 최대 10개까지 업로드 가능합니다.');
break; // 루프 종료 (계속하면 추가될 수도 있음)
}
// FileReader로 미리보기 추가
const reader = new FileReader();
reader.onload = (e) => {
const preview = createElement(e, file);
imagePreview.appendChild(preview);
};
reader.readAsDataURL(file);
dataTransfer.items.add(file); // 실제 파일 저장
currentCount++; // 새로 추가된 이미지 개수 증가
prouductImgCnt++;
$(".imgCount").text("(" + prouductImgCnt + "/10" + ")");
}
}
// 태그 만들어주는 함수
function createElement(e, file) {
const li = document.createElement('li'); // li 태그 만들기
const img = document.createElement('img'); // img 태그 만들기
img.setAttribute('src', e.target.result); // 만든 img 태그에 경로 속성 값 넣어줌
img.setAttribute('data-file', file.name); // 만들 ing 태그에 파일 이름 속성 값 넣어줌
img.setAttribute('data-modified', file.lastModified);
img.setAttribute('data-size', file.size);
checkProductRegist.productImg = true;
img.addEventListener("click", e => { // 해당 이미지 클릭시
console.log(prouductImgCnt);
prouductImgCnt--; // 이미지 삭제시 개수 감소
$(e.target).parent().remove(); // li안의 img까지 삭제
$(".imgCount").text("(" + prouductImgCnt + "/10" + ")");
for(var i=0; i<dataTransfer.files.length; i++){
if(dataTransfer.files[i].name==e.target.dataset.file){
dataTransfer.items.remove(i)
break;
}
}
if(dataTransfer.files.length == 0){
checkProductRegist.productImg = false;
}
});
li.appendChild(img); // 이미지가 있는 li 태그 완성하여 li 리턴
return li;
}
const realUpload = document.querySelector('.real-upload');
const upload = document.querySelector('.upload');
upload.addEventListener('click', () => realUpload.click()); // 이미지등록 클록시 input file타입 호출
realUpload.addEventListener('change', getImageFiles); // file타입에서 값 변경시키면 getImageFiles() 함수 호출
<div class="cate">
<h4 class="h4Size">카테고리 *</h4>
<select class="mainCate" onchange="chageSubCate(this.value);">
<!-- this.value -> 선택된 option의 밸류값을 매개변수로 넣음 -->
<%
<!-- categorys는 getAttribute를 통해서 디비안의 값들을 갖고온다 -->
if (!categorys.isEmpty()) {
for (int i = 0; i < categorys.size(); i++) {
%>
<option value="<%=categorys.get(i).getCategoryId()%>"><%=categorys.get(i).getCategoryName()%></option>
<%
}
}
%>
</select>
<select class="middleCate" name="subCate">
<!-- 이안의 내용은 서브카테고리의 내용들이 출력된다 -->
</select>
// 카테고리 선택하는 작업
$(() => {
$(".mainCate").trigger("change", $(".mainCate:selected").val()); // 페이지로딩되었을때, 자동으로 change 함수 실행
// 대상값은 현재 그 select에 선택된 값
})
function chageSubCate(value) {
console.log(value);
$.ajax({
url: "findSubCate",
data: { "cateId": value },
success: function(result) {
const subCate = result.split(","); // 문자열로 넘어온 값들을 ,를 구분자로 배열을 만듬
$(".middleCate option").remove(); // 메인카테고리 선택할때마다 옵션들 다 삭제(초기화)
for (let i = 0; i < subCate.length; i++) { // 배열에 있는 서브카테고리들을 option value로 만들어줌
var option = $("<option value=" + subCate[i] + ">" + subCate[i] + "</option>");
$(".middleCate").append(option);
}
},
error: function() {
console.log("카테고리 선택 오류발생");
}
})
}
컨트롤러
// findSubCate 서블릿
String cateId = request.getParameter("cateId");
List<String> subCategorys = new ProductRegistService().selectSubCate(cateId); // 서브카테고리들이 리스트로 나옴
// 리스트안에 문자열 형식으로 받아야함
String result = subCategorys.stream().map(n->String.valueOf(n)).collect(Collectors.joining(","));
// 리스트를 -> 문자열로 만들어줌 (문자열로 만들어줄때 앞뒤로 공백을 없애고 ,로 구분해줌)
// [컴퓨터, 노트북, 스마트폰, 소프트웨어, 기타 주변기기] -> 컴퓨터,노트북,스마트폰,소프트웨어,기타 주변기기
Mapper
<select id="selectSubCate" resultType="string" parameterType="string">
SELECT SUBCATEGORY_NAME FROM SUBCATEGORY WHERE CATEGORY_ID = #{cateId}
</select> <!--반환타입 : 문자열, 매개변수 타입 : 문자열 -->
<!-- 타입을 정수형으로 할 경우 -> _int -->
,
로 숫자를 구별해줌<input type="text" id="priceId" oninput="inputNumberFormat(this);"
placeholder="숫자만 입력해주세요." name="price">
<!-- oninput 속성을 통해 정규표현식을 적용하였다 -->
// ==== 가격 입력했을 때, 숫자만입력되고, 3자리수마다 ,로 구분해주는 작업
function comma(str) {
str = String(str);
return str.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, "$1,");
}
function uncomma(str) {
str = String(str);
return str.replace(/[^\d]+/g, "");
}
function inputNumberFormat(obj) {
obj.value = comma(uncomma(obj.value));
}
// 가격이 입력될때마다 예외처리
const priceValue = document.getElementById("priceId")
const spanPrice = $("#spanPrice");
priceValue.addEventListener("keyup", function() {
if (priceValue.value.length == 0) {
spanPrice.text("");
checkProductRegist.productPrice=false;
}else{
replacePrice = priceValue.value.replace(",","");
if(replacePrice <= 0){
spanPrice.text("0원보다 크게 입력하세요").css("color","red");
checkProductRegist.productPrice=false;
}else{
spanPrice.text("○").css("color","green");
checkProductRegist.productPrice=true;
}
}
});
<div class="relativeTag">
<h4 class="h4Size">
상품태그
</h4>
<input type="text" id="searchTag" placeholder="연관 태그를 입력해주세요" autocomplete="on">
<div id="relativeTagDiv">
<!-- 태그들이 등록되는 곳 -->
</div>
</div>
<div class="autocomplete"><!-- 자동완성 검색어들이 나오는 곳 --></div>
// 상품태그 검색 관련 js
const dataList = [
"#패션", "#패션의류", "#자켓", "#상의", "#스포츠", "#도서", "#전자기기", "#노트북", "#가구", "#생활", "#차량", "#악세서리",
"#캠핑", "#등산", "#모니터", "#마우스", "#키보드", "#에어컨", "#헤드셋", "#레고", "#피규어", "#슬리퍼", "#책", "#소설", "#가방"
];
let registTagList = [];
const $searchTag = document.querySelector("#searchTag");
const $autoComplete = document.querySelector(".autocomplete");
const $relativeTagDiv = document.querySelector("#relativeTagDiv");
let nowIndex = 0;
let matchDataList = [];
$searchTag.addEventListener("keyup", (event) => {
const value = $searchTag.value.trim();
switch (event.keyCode) {
case 38: // ↑
nowIndex = Math.max(nowIndex - 1, 0);
break;
case 40: // ↓
nowIndex = Math.min(nowIndex + 1, matchDataList.length - 1);
break;
case 13: // Enter
if (matchDataList.length === 0) return;
const selectedTag = matchDataList[nowIndex] || value;
addTag(selectedTag);
resetAutoComplete();
return;
case 27: // ESC
resetAutoComplete();
return;
default:
matchDataList = value
? dataList.filter((tag) => tag.toLowerCase().includes(value.toLowerCase()))
: [];
nowIndex = 0;
break;
}
showList(matchDataList, value, nowIndex);
});
// 검색창 외부 클릭 시 자동완성 닫기
document.addEventListener("click", (event) => {
const isClickInside = $searchTag.contains(event.target) || $autoComplete.contains(event.target);
if (!isClickInside) {
resetAutoComplete();
}
});
$autoComplete.addEventListener("click", (e) => {
let selected = e.target.textContent.trim();
addTag(selected);
resetAutoComplete();
});
function showList(data, keyword, highlightIndex) {
const regex = new RegExp(`(${keyword})`, "gi");
$autoComplete.innerHTML = data
.map((tag, index) => {
const highlighted = tag.replace(regex, "<label>$1</label>");
return `<div class="${highlightIndex === index ? "active" : ""}">${highlighted}</div>`;
})
.join("");
// ✅ 현재 highlight된 항목이 보이도록 스크롤
const activeItem = $autoComplete.querySelector(".active");
if (activeItem) {
activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}
function addTag(tag) {
if (!tag || registTagList.includes(tag)) {
alert("이미 존재하는 키워드입니다.");
return;
}
if (registTagList.length >= 5) {
alert("태그는 최대 5개까지만 추가 가능합니다.");
return;
}
registTagList.push(tag);
const $li = document.createElement("li");
const $label = document.createElement("label");
$label.textContent = tag;
const $button = document.createElement("button");
$button.type = "button";
$button.style.border = "none";
$button.style.backgroundColor = "transparent";
const $img = document.createElement("img");
$img.src = (typeof contextPath !== "undefined" ? contextPath : "") + "/images/productregist/xbtn.png";
$img.width = 15;
$img.height = 15;
$img.addEventListener("click", (e) => {
const tagText = e.target.closest("li").querySelector("label").textContent;
registTagList = registTagList.filter((t) => t !== tagText);
$li.remove();
});
$button.appendChild($img);
const $hiddenInput = document.createElement("input");
$hiddenInput.type = "hidden";
$hiddenInput.name = "tags";
$hiddenInput.value = tag;
$li.appendChild($label);
$li.appendChild($button);
$li.appendChild($hiddenInput);
$relativeTagDiv.appendChild($li);
}
function resetAutoComplete() {
matchDataList.length = 0;
nowIndex = 0;
$searchTag.value = "";
$autoComplete.innerHTML = "";
}
HttpSession session = request.getSession();
String path=getServletContext().getRealPath("/upload/productRegist"); // -> /upload/productRegist 안에다 업로드되는 이미지 넣음
System.out.println(path);
MultipartRequest mr= new MultipartRequest(request, path, 1024*1024*60,"UTF-8",new DefaultFileRenamePolicy());
// MultipartRequest 객체 사용하려면, 이 서블릿을 요청시킨 form태그에 enctype="multipart/form-data" 를 넣어야함
String replacePrice = mr.getParameter("price");
replacePrice = replacePrice.replace(",",""); // ,있는 돈 문자열을 ,를 ""로 대체함
Member m = (Member) session.getAttribute("loginMember");// 세션에서 현재로그인한 정보 갖고옴
String productId = mr.getParameter("productId");
String userId = m.getUserId();
String title = mr.getParameter("title");
String subCate = mr.getParameter("subCate");
String place = mr.getParameter("place");
String state = mr.getParameter("state");
int price = Integer.parseInt(replacePrice); // 정수일경우 앞에 Integer.parseInt 로 형변환해야함
String explan = mr.getParameter("explan");
String tag = mr.getParameter("tag");
List<ProductFile> files= new ArrayList();
Enumeration<String> names=mr.getFileNames(); // 해당 파일객체들의 키값을 하나씩 출력 (Enumeration -> 열거객체 순환)
while(names.hasMoreElements()) { // 다중 파일들의 이미지를 접근 가능
String key=names.nextElement(); // key-> 해당 파일은 객체로 저장되잇음 각각 파일의 키를 저장함
// mr.getFilesystemName(key)); new 파일
// mr.getOriginalFileName(key)); ori파일
files.add(ProductFile.builder().imageName(mr.getFilesystemName(key)).build());
// ProductFile 객체 안에 멤버변수들을 builder를 통해서 각각 넣어줌 (파일이름, 등등)
// builder 함수 사용시 마지막에 .build() 해줘야함
}
Product p = Product.builder().title(title).productStatus(state).price(price).explanation(explan)
.keyword(tag).areaName(place).subCategoryName(subCate).files(files).build();
// Product 객체 안에 멤버변수들을 builder를 통해서 넣어줌 (마지막에는 files 멤버변수도 builder를 통해서 해당맞는 타입의 값을 넣어줌)
// Product 클래스안에 결국 ProudctFile 값들도 들어있는것임
// 그러기 때문에 Product만 객체만 넣어줘도 됨
int result = new ProductRegistService().insertProduct(p,userId); // 상품등록 및 상품이미지첨부파일 데이터 추가 하는 작업
response.getWriter().print(result); // 해당 반환되는 0 또는 1의 값을 다시 js로 반환됨 (ajax이기 때문에 해줘야함)-> js에서 정수 값을 통해 분기처리
특히