이어서 상품의 수정 및 삭제 기능 작업을 진행했는데 상품의 경우 api로 주고받는 값들이 많아보니 CRUD를 구현했을 때 코드가 상당히 복잡하고 길어졌다. 그에따라 JS파일을 기능별로 페이지를 분할하는 작업도 병행했다.
모달관련 페이지, 일반 페이지, 겟, 포스트, 업데이트, 딜리트 이렇게 6개 페이지로 나누어보았다.
각 페이지에 대한 js소스는 html에서 하나로 통합된다.
< script src = "/adminProductModal.js" ></ script >
< script src = "/adminProductPost.js" ></ script >
< script src = "/adminProductPut.js" ></ script >
< script src = "/adminProductDelete.js" ></ script >
< script src = "/adminProduct.js" ></ script >
< script src = "/adminProductCommon.js" ></ script >
기본적으로 수정과 삭제 기능은 수정이나 삭제를 하려는 특정 아이템 하나를 선택을 해서 기능을 실행해야하는데 그러기 위해서 해당 아이템의 _id 고유값을 확인할 수 있어야한다.
그리고 그 값은 목록으로 펼쳐져있는 아이템들로부터 얻을 수 있기 때문에 고유 아이디를 얻는 작업은 get 부분에서 이루어졌다.
수정의 경우 꽤 번거로우면서 복잡한 부분이 기존 아이템 항목의 정보를 다 불러와야한다. 그래야 그 정보를 수정할 수 있으니까.
따라서 모달이 열렸을 때 모든 필드에 다시 수정 정보들을 가져오는 작업이 진행되고
이어서 수정된 정보를 저장하는 버튼을 누르면 매개변수를 통해 고유 아이디 값을 editProduct 함수로 전달하는 작업이 이루어졌다.
// 상품 수정 모달 열기
const itemEdit = document . getElementById ( `item-edit ${ i } ` );
itemEdit . addEventListener ( "click" , () => {
console . log ( "수정할 아이템" , item );
// 인풋 필드에 값 설정
document . getElementById ( "item-sku" ). value = item . sku ;
document . getElementById ( "item-name" ). value = item . name ;
document . getElementById ( "item-description" ). value = item . description ;
document . getElementById ( "item-price" ). value = item . price ;
document . getElementById ( "item-status" ). value = item . status ;
// 이미지 미리 보기 설정
document . getElementById ( "image-container" ). innerHTML = `<img src=" ${ item . image } " alt="Uploaded Image">` ;
imgData = item . image ;
// 카테고리 값 설정
document . getElementById ( "item-category" ). value = item . category ;
categoryArray = item . category ;
// 기존 재고 정보 표시
stockContainer . innerHTML = "" ;
Object . keys ( item . stock ). forEach (( size , index ) => {
const stockInputs = `
<div class="stock-inputs" id="stock-data ${ index } ">
<span>
<label>사이즈</label>
<input type="text" class="stock-input" id="item-size ${ index } " value=" ${ size } ">
</span>
<span>
<label>수량</label>
<input type="number" class="stock-input" id="item-qty ${ index } " value=" ${ item . stock [ size ] } ">
</span>
<button id="stock-delete ${ index } ">삭제</button>
</div>
` ;
stockContainer . insertAdjacentHTML ( "beforeend" , stockInputs );
// 재고 삭제 기능 설정
document . getElementById ( `stock-delete ${ index } ` ). addEventListener ( "click" , () => {
console . log ( "삭제?" )
document . getElementById ( `stock-data ${ index } ` ). remove ();
updateStockValues ();
});
});
// 수정 버튼 클릭 시 모달 표시
modal . style . display = 'flex' ;
// 수정 완료 버튼 클릭 시 editProduct 함수 실행해서 수정 요청
const editBTN = document . getElementById ( "edit-item-btn" );
editBTN . addEventListener ( "click" , () => {
editProduct ( item . _id );
});
});
여기서 특이한 부분은 Object.keys() 부분인데 이 기능을 쓰면 인자로 넣어준 객체의 키 값들을 배열로 반환한다.
예를들어 지금 item.st ock 객체가 { L:2, M:2, S:5 } 이라면 [ L, M, S ] 로 반환하는 것이다.
이렇게 나온 사이즈 배열 값들을 forEach를 통해서 html 인풋을 채워주고 그에 대응하는 qty 값들은 item.stock[size] (키에 대한 값)으로 넣어주었다.
삭제 기능도 이와 같은 방식인데 비교적 쉽게 고유아이디만 매개변수로 deleteProduct 함수로 전달하면서 get 페이지 작업은 마무리.
// 상품 삭제
const itemDelete = document . getElementById ( `item-delete ${ i } ` );
itemDelete . addEventListener ( "click" , () => {
deleteModal . style . display = 'flex' ;
const cancleBTN = document . getElementById ( `cancle-btn` );
const deleteBTN = document . getElementById ( `delete-item-btn` );
cancleBTN . addEventListener ( "click" , () => {
deleteModal . style . display = 'none' ;
});
// 삭제 요청
deleteBTN . addEventListener ( "click" , () => {
console . log ( item . _id );
deleteProduct ( item . _id );
});
});
다음은 수정페이지의 코드이다.
수정은 위에서 설명한 것 처럼 전체 아이템 중 딱 하나를 골라서 요청해야하기 때문에 그 값을 백엔드에 알려줘야한다. 그 방법으로 path parameter, 일명 params라는 곳에 id값을 넣어서 보내게 된다.
fetch ( ` ${ URI } /api/product/ ${ editId } `
프론트엔드에서는 이걸 url주소 경로명으로 보내게되는데 그래서 path 파라미터라고 부르는 것 같다.
이런식으로 수정하고자하는 아이템의 고유 id값을 넣어서 요청하게되고 요청 방식은 PUT이다.
수정이나 삭제는 꽤 중요한 권한이기에 헤더에 토큰 정보도 같이 전송해서 인증된 사용자만 가능하도록 설정.
그리고 body에는 각 인풋들의 밸류를 원래 항목 변수들에 재 대입 시켜서 내용 업데이트를 요청하는 방식이다.
function editProduct ( editId ) {
// PUT 요청으로 데이터 전송
fetch ( ` ${ URI } /api/product/ ${ editId } ` , {
method : "PUT" ,
headers : {
"Content-Type" : "application/json" ,
"Authorization" : `Bearer ${ sessionStorage . getItem ( "token" ) } `
},
body : JSON . stringify ({
sku : document . getElementById ( "item-sku" ). value ,
name : document . getElementById ( "item-name" ). value ,
image : imgData ,
category : document . getElementById ( "item-category" ). value ,
description : document . getElementById ( "item-description" ). value ,
price : document . getElementById ( "item-price" ). value ,
stock : stockValues ,
status : document . getElementById ( "item-status" ). value
}),
})
. then (( response ) => response . json ())
. then (( data ) => {
console . log ( "PUT response:" , data );
if ( data . status === 'fail' ) {
// 오류 처리
document . getElementById ( "error-msg" ). innerHTML = `<p> ${ data . message } </p>` ;
} else {
// 성공 시 처리
modal . style . display = 'none' ;
updateStockValues ();
console . log ( "수정 완료!" );
// 수정된 내용을 반영하기 위해 상품 목록 새로 고침
getProducts ( currentPage );
}
})
. catch (( error ) => console . error ( "Error:" , error ));
}
딜리트도 비슷한 방식.
function deleteProduct ( deleteId ) {
fetch ( ` ${ URI } /api/product/ ${ deleteId } ` , {
method : "DELETE" ,
headers : {
"Content-Type" : "application/json" ,
"Authorization" : `Bearer ${ sessionStorage . getItem ( "token" ) } `
},
}). then (( response ) => response . json ())
. then (( data ) => {
console . log ( "DELETE response:" , data );
getProducts ( currentPage );
});
deleteModal . style . display = 'none' ;
};
이 요청을 응답하는 백엔드도 살펴보면
router . put ( "/:id" , authController . athenticate , authController . checkAdminPermission , productController . updateProduct );
router . delete ( "/:id" , authController . athenticate , authController . checkAdminPermission , productController . deleteProduct );
라우터를 통해서 수정, 삭제요청에 대해 응답하는 각 컨트롤러로 보내게 되는데, 둘 다 특정하게 선택한 아이템에 대해서 실행되어야 하기 때문에 아까 프론트에서 params로 요청 받은 id 값을 라우터로도 /:id처리를 해서 해당 컨트롤러에서 그 고유아이디 값을 받아서 처리할 수 있도록 해준다.
그리고 위에서 말한대로 인증된 사용자만 가능하도록 auth관련 미들웨어들도 중간에 배치해준다.
컨트롤러를 살펴보면
//상품 수정
productController . updateProduct = async ( req , res ) => {
try {
const productId = req . params . id ;
const { sku , name , image , category , description , price , stock , status } = req . body ;
const product = await Product . findByIdAndUpdate (
{ _id : productId },
{ sku , name , image , category , description , price , stock , status },
{ new : true }
);
if ( ! product ) {
throw new Error ( "상품이 없습니다." );
};
res . status ( 200 ). json ({ status : "ok" , data : product });
} catch ( error ) {
res . status ( 400 ). json ({ status : "fail" , message : error . message });
};
}
프론트엔드로부터 해당 아이템에 대한 고유 아이디 값을 req.params.id 로 불러오고 업데이트 된 body 정보들도 불러온다.
가져온 고유 아이디가 있다면 findByIdAndUpdate 라는 기능으로 아이디를 통해 값들을 업데이트 시킬 수 있다.
삭제기능도 이와 비슷하다.
//상품 삭제
productController . deleteProduct = async ( req , res ) => {
try {
const deleteItem = await Product . findByIdAndDelete ( req . params . id );
res . status ( 200 ). json ({ status : "ok" , data : deleteItem });
} catch ( error ) {
res . status ( 400 ). json ({ status : "fail" , message : error . message });
};
};
이번 상품 정보 관련 CRUD 작업을 해보면서 이런저런 오류로 우여곡절도 많았는데 프론트-백엔드 간 api 개념이 많이 정리된 것 같다. 특히 queryString, params, body 등 정보를 보내는 방식의 차이들을 명확하게 알게 된 계기가 되었다. 그와 더불어 cloudinary와 같은 멋진 서비스도 알게되어서 여러모로 많이 배운 케이스가 되겠다.