먼저 게시글 테이블은 아래와 같이 구성되어있다.
Writer, CategoryId는 각각 Member와 Category테이블에 Join
되어있고, Rating과 Status는 각각 물건의 상태와 게시글의 상태(판매여부 등)를 나타내며 Enum
으로 구성되어있다.
게시글 작성 Form에 접근하게 되면 Url에 해당 카테고리의 ID값이 같이 전송된다. 해당 Param값으로 Category를 셀렉트 시켜 사용자가 편리하게 이용할 수 있도록 구현하였다.
<div class="dropdown" id="divCategory">
<label class="form-label" for="categoryName">종류</label>
<select class="form-control" id="categoryName" name="categoryName">
<option th:each="category : ${categoryList}"
th:value="${category.name}"
th:text="${category.name}"
th:selected="${category.id}==${#request.getParameter('category')}">
</option>
</select>
</div>
이후 제목과 내용, 가격, 거래장소 까지 입력할 수 있게 구현하였다. 가격 부분은 숫자만 입력되게 해야한다. 따라서 사용자가 숫자이외에 다른 문자를 입력할 경우 공백으로 치환되게 하였다. 그리고 가독성을 높이기 위해 3자리 마다 콤마표시를 만들었다. 해당 로직은 JS로 구성하였다.
/*가격 콤마생성*/
$("#price").on("focusout", function() {
$(this).val( $(this).val().replace(",","") );
$(this).val( $(this).val().replace(/[^-\.0-9]/gi,"") );
$(this).val( $(this).val().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") );
});
Db에는 Price가 number로 되어있다. 따라서 해당 form을 Db로 Post할 때 가격에 콤마를 제거하고 보내야한다.
/*db로 보낼 때 콤마 제거*/
function eraseComma(){
const temp = $("#price").val().replace(/,/gi,"");
$("input[name=price]").val(temp);
}
게시글 Content는 위지윅 에디터
를 적용시켜 구현하였다. 많은 에디터들 중에 Summernote
를 사용하였다. 처음에는 가장 대중적인 Ck Editor
를 사용하였는데, 이미지를 저장하는 부분에서 권한 문제에 부딪혀 변경하게 되었다.
$(document).ready(function () {
$('#content').summernote({
height: 500, // 에디터 높이
minHeight: null, // 최소 높이
maxHeight: null, // 최대 높이
focus: true, // 에디터 로딩후 포커스를 맞출지 여부
lang: "ko-KR", // 한글 설정
placeholder: '내용을 입력해 주세요', //placeholder 설정
callbacks: { //여기 부분이 이미지를 첨부하는 부분
onImageUpload: function (files) {
uploadSummernoteImageFile(files[0], this);
},
onPaste: function (e) {
let clipboardData = e.originalEvent.clipboardData;
if (clipboardData && clipboardData.items && clipboardData.items.length) {
let item = clipboardData.items[0];
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
e.preventDefault();
}
}
}
}
});
});
이미지 업로드 방식은 다음과 같다. 먼저 Js로 Ajax요청을 보낸다
/*이미지 파일 업로드*/
function uploadSummernoteImageFile(file) {
const data = new FormData();
data.append("file", file);
$.ajax({
data: data,
type: "POST",
url: "/api/uploadSummernoteImageFile",
contentType: false,
processData: false,
success: function (data) {
//항상 업로드된 파일의 url이 있어야 한다.
$("#content").summernote('insertImage', data.url);
}
});
}
이후 해당컨트롤러에서 서비스로 요청을 보내 로직 수행을 한다.
Controller
@ResponseBody
@PostMapping("/api/uploadSummernoteImageFile")
public HashMap<String, Object> uploadSummernoteImageFile(@RequestParam("file") MultipartFile multipartFile) {
return boardService.boardImageUpload(multipartFile);
}
Service
fileRoot는 이미지의 저장경로이다. 먼저 해당 파일명을 randomUUid
를 이용하여 파일명을 랜덤으로 변경시키다. 이후 fileRoot에 해당 이미지를 저장시키고 성공하였으면 response코드로 success를 리턴한다.
/*summernote 이미지 첨부*/
public HashMap<String, Object> boardImageUpload(MultipartFile multipartFile) {
HashMap<String, Object> map = new HashMap<>();
String fileRoot = "D:\\summernote_image\\";
String originalFileName = multipartFile.getOriginalFilename(); // 오리지널 파일명
String extension = originalFileName.substring(originalFileName.lastIndexOf(".")); //파일확장자
String savedFileName = UUID.randomUUID() + extension; //저장될 파일 명
File targetFile = new File(fileRoot + savedFileName);
try {
InputStream fileStream = multipartFile.getInputStream();
FileUtils.copyInputStreamToFile(fileStream, targetFile);
map.put("url", "/summernoteImage/" + savedFileName);
map.put("responseCode", "success");
} catch (IOException e) {
FileUtils.deleteQuietly(targetFile); //저장된 파일 삭제
map.put("responseCode", "error");
e.printStackTrace();
}
return map;
}
이후 정상적으로 업로드가 완료되었으면 ajax에서 아래와 같이 해당 링크를 연결하여 이미지를 첨부한다.
success: function (data) {
//항상 업로드된 파일의 url이 있어야 한다.
$("#content").summernote('insertImage', data.url);
}
해당 게시물을 작성할 때에는 거래장소도 미리 정하도록 설정하였다. 거래장소는 카카오 지도 Api를 이용하였으며 카카오지도 Api
에 대한 내용은 다른 포스트에서 자세히 다루도록 하겠다
거래 장소 정하기
버튼을 누르면 Popup창을 띄우는 JS코드를 작성하였다.
$("#mapButton").on('click',function (){
window.open("/board/map","map","width=900,height=550,left=300,top=100");
});
아래와 같은 창이 열린다
지도를 저장하는 로직은 아래와 같다
지도를 클릭 or 검색 or 현재위치 클릭 시 마커가 생성 -> 마커 클릭시 confirm창 생성 -> 확인 시 해당 좌표가 mylat, mylng에 저장되고 부모 Html로 전송 -> form을 post하면 해당 좌표가 Dto에 전달
/*지도 직접 클릭*/
kakao.maps.event.addListener(map, 'click', function (mouseEvent) {
showInfoWindow(mouseEvent.latLng);
// 마커를 클릭한 위치에 표시합니다
marker.setPosition(mouseEvent.latLng);
marker.setMap(map);
myLat = mouseEvent.latLng.getLat();
myLng = mouseEvent.latLng.getLng();
});
/*검색버튼 클릭*/
$("#searchBtn").on('click', function () {
const keyword = $("#address").val()
// 주소로 좌표를 검색합니다
geocoder.addressSearch(keyword, function (result, status) {
// 정상적으로 검색이 완료됐으면
if (status === kakao.maps.services.Status.OK) {
var coords = new kakao.maps.LatLng(result[0].y, result[0].x);
showInfoWindow(coords);
marker.setPosition(coords);
marker.setMap(null);
marker.setMap(map);
// 지도의 중심을 결과값으로 받은 위치로 이동시킵니다
map.setCenter(coords);
myLat = coords.getLat();
myLng = coords.getLng();
}
});
});
/*현재위치 버튼 클릭*/
$("#location").on('click', function () {
/*현재위치 받아오기*/
function locationLoadSuccess(pos) {
// 현재 위치 받아오기
const currentPos = new kakao.maps.LatLng(pos.coords.latitude, pos.coords.longitude);
showInfoWindow(currentPos);
// 지도 이동(기존 위치와 가깝다면 부드럽게 이동)
map.panTo(currentPos);
// 마커 생성
marker.setPosition(currentPos);
// 기존에 마커가 있다면 제거
marker.setMap(null);
marker.setMap(map);
myLat = currentPos.getLat();
myLng = currentPos.getLng();
};
function locationLoadError(pos) {
alert('위치 정보를 가져오는데 실패했습니다.');
};
navigator.geolocation.getCurrentPosition(locationLoadSuccess, locationLoadError);
});
//마커 클릭 이벤트
kakao.maps.event.addListener(marker, 'click', function (mouseEvent) {
if (confirm("이 위치로 하시겠습니까?")) {
alert("위치가 지정되었습니다");
opener.document.getElementById("myLat").value = myLat;
opener.document.getElementById("myLng").value = myLng;
close();
}
});
해당 값이 부모 클래스로 전달된다.