728x90
글쓰기 버튼을 눌렀을때! 요청 주소로 넘어가기 위해서 location 을 이용할 것이다!
🤔 location이란 ?
현재 활성화된 웹페이지의 URL 정보를 포함하는 window 객체의 프로퍼티로
location.href로 현재 주소를 확인할 수 있다.
예시) location.href='/member/login' (해당 주소 요청(GET방식))
* 주소를 작성할 수 있는 방법 아래 2개 *
location.href = "/board2/" + location.pathname.split("/")[2];
location.href = `/board2/${location.pathname.split("/")[2]}/insert`
📂 결과 : /board2/1/insert
📝 js 코드
if (insertBtn != null) {
// 글쓰기 버튼 클릭 시
insertBtn.addEventListener("click", () => {
location.href = `/board2/${location.pathname.split("/")[2]}/insert`
})
}
💡 js 코드에서 중요한 건 ! insertBtn이 있는지 확인하기 위해서 != null을 작성해야한다!
이제 본격적으로 게시글 작성페이지를 만들어보자
이전에 게시글 조회했을때와 같이 @PathVariable를 이용할 예정이다.
🤔 그렇다면 @PathVariable이 머였지 ?
URL 경로에 있는 값을 매개변수로 이용할 수 있게 하는 어노테이션 + request scope에 세팅
📚 VS code
📕 boardWrite. jsp
❗ form 주소에 ! ${boardCode}는 @PathVariable에서왔다!
❗ 무언가를 제출한다면 ! 꼭 form 태그에 enctype = "multipart/form-date" 작성해주기 !
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<c:forEach items="${boardTypeList}" var="boardType">
<c:if test="${boardType.BOARD_CODE == boardCode}" >
<c:set var="boardName" value="${boardType.BOARD_NAME}"/>
</c:if>
</c:forEach>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${boardName}</title>
<link rel="stylesheet" href="/resources/css/board/boardWrite-style.css">
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<%-- ${boardCode} : @PathVariable --%>
<form action="/board2/${boardCode}/insert" method="POST"
class="board-write" id="boardWriteFrm" enctype = "multipart/form-date">
<%-- 무언가를 업로드 해야한다면 ! enctype = "multipart/form-date" : 제출 데이터 인코딩 x 생각하기 !
-> 파일 제출 가능
-> MultiPartResolver 가 문자열, 파일을 구분
--> 문자열 -> String, int, DTO, Map(HttpMessageConverter)
--> 파일 -> MultiPartFile 객체 -> transferTo() (파일을 서버에 저장!)
--%>
<h1 class="board-name">${boardName}</h1>
<!-- 제목 -->
<h1 class="board-title">
<input type="text" name="boardTitle" placeholder="제목" value="" id="boardTitle">
</h1>
<!-- 썸네일 영역 -->
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<label for="img0">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img0" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<div class="boardImg">
<label for="img1">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img1" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img2">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img2" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img3">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img3" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img4">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img4" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 내용 -->
<div class="board-content">
<textarea name="boardContent" id="boardContent"></textarea>
</div>
<!-- 버튼 영역 -->
<div class="board-btn-area">
<button type="submit" id="writebtn">등록</button>
</div>
</form>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script src="/resources/js/board/boardWrite.js"></script>
</body>
</html>
📕 boardWrite. css
/* 상세조회 전체 영역 */
.board-write{
width: 1000px;
min-height: 700px;
border: 1px solid #ccc;
margin: 50px auto;
padding: 20px;
}
/* 게시글 제목 */
.board-title{
margin : 0;
padding: 20px 0;
border-bottom : 3px solid #ccc;
}
.board-title > input{
width: 100%;
border: 0;
outline: 0;
font-size: 24px;
}
.board-title:focus-within{
border-bottom-color: #455ba8;
}
.img-box{
display: flex;
justify-content: space-between;
}
/* 이미지를 감싸는 div */
.boardImg{
width: 230px;
height: 230px;
display: inline-block;
text-align: center;
position: relative;
}
.boardImg > label{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #ccc;
cursor: pointer;
}
.inputImage{
display: none;
}
/* 이미지 */
.boardImg img{
max-width: 100%;
max-height: 100%;
}
/* 썸네일만 이미지 크게 */
.thumbnail{
width: 300px;
height: 300px;
}
/* 내용 */
.board-content{
padding: 30px 0;
margin: 30px 0;
border-top : 3px solid #ccc;
border-bottom : 3px solid #ccc;
font-size: 18px;
}
[name="boardContent"]{
width: 100%;
height: 400px;
resize: none;
font-size: 1.1em;
}
/* 버튼 영역 */
.board-btn-area{
text-align: right;
}
/* 버튼 */
.board-btn-area button{
width: 80px;
height: 30px;
font-weight: bold;
border: 0;
background-color: #455ba8;
color : white;
cursor: pointer;
}
.board-btn-area button:hover{
background-color: white;
color : #455ba8;
border: 2px solid #455ba8;
}
/* 삭제 버튼 */
.delete-image{
position: absolute;
top: 0;
right: 7px;
font-size: 20px;
cursor: pointer;
}
📕 boardList. js
❗ 만약 id 값고 name값만 있다면 id 속성값을 넣어줘도 되지만,
querySelector를 이용해서 [name ='boardContent']를 이용하면 된다.
// 미리보기 관련 요소 모두 얻어오기
// img 5개
const preview = document.getElementsByClassName("preview");
// file 5개
const inputImage = document.getElementsByClassName("inputImage");
// x 버튼 5개
const deleteImage = document.getElementsByClassName("delete-image");
// ->위에 얻어온 요소들의 개수가 같음 == 인덱스가 일치
for(let i=0;i<inputImage.length;i++){
// 파일이 선택되거나, 선택 후 취소 되었을때
inputImage[i].addEventListener("change", e => {
const file = e.target.files[0]; //선택 된 파일의 데이터
if( file != undefined ){// 파일이 선택 되었을때
const reader = new FileReader() // 파일을 읽는 객체
reader.readAsDataURL(file);
// 지정된 파일을 읽은 후 result 변수에 URL 형식으로 저장
reader.onload = e =>{ // 파일을 다 읽은 후 수행
preview[i].setAttribute ("src",e.target.result);
}
}else{ // 선택 후 취소 되었을때
// -> 선택된 파일 없음 -> 미리보기
preview[i].removeAttribute("src");
}
})
// 미리보기 삭제 버튼(x버튼)
deleteImage[i].addEventListener("click",()=>{
// 미리보기 이미지가 있을 경우
if(preview[i].getAttribute("src")!=""){
// 미리보기 삭제
preview[i].removeAttribute("src");
// input type = "file"태그의 value를 삭제
// ** input type ="file"의 value ""(빈칸)만 대입 가능
inputImage[i].value = "";
}
})
}
// 게시글 등록 시 제목, 내용 작성 여부 검사
const boardWriteFrm = document.getElementById("boardWriteFrm");
//const boardTitle = document.getElementById("#boardWriteFrm > h1.board-title > input[type=text]");
const boardTitle = document.querySelector("[name='boardTitle']");
const boardContent = document.querySelector("[name='boardContent]");
boardWriteFrm.addEventListener("submit",e=>{
if(boardTitle.value.trim().length==0){
alert("제목을 입력해주세요");
boardTitle.value="";
boardTitle.focus();
e.preventDefault();
return;
}
if(boardContent.value.trim().length==0){
alert("내용을 입력해주세요");
boardTitle.value="";
boardTitle.focus();
e.preventDefault();
return;
}
});
📚 Spring
📗 게시글 작성시 xss 방지 스크립트 / 파일명 변경 메소드 Util.class
package edu.kh.project.common.utility;
import java.text.SimpleDateFormat;
public class Util {
// Cross Site Scripting(XSS) 방지 처리
// - 웹 애플리케이션에서 발생하는 취약점
// - 권한이 없는 사용자가 사이트에 스크립트를 작성하는 것
public static String XSSHandling(String content) {
// 스크립트나 마크업 언어에서 기호나 기능을 나타내는 문자를 변경 처리
// & - &
// < - <
// > - >
// " - "
content = content.replaceAll("&", "&");
content = content.replaceAll("<", "<");
content = content.replaceAll(">", ">");
content = content.replaceAll("\"", """);
return content;
}
// 파일명 변경 메소드
public static String fileRename(String originFileName) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String date = sdf.format(new java.util.Date(System.currentTimeMillis()));
int ranNum = (int) (Math.random() * 100000); // 5자리 랜덤 숫자 생성
String str = "_" + String.format("%05d", ranNum);
String ext = originFileName.substring(originFileName.lastIndexOf("."));
return date + str + ext;
}
}
📗 boardController2.jsp
@Controller
@RequestMapping("/board2")
@SessionAttributes({"loginMember"})
public class boardController2 {
@Autowired
private BoardService2 service;
// 게시글 작성 화면 전환
@GetMapping("/{boardCode:[0-9]+}/insert")
public String boardInsert(@PathVariable("boardCode") int boardCode) {
//@PathVariable : 주소 값 가져오기 + request scope에 값을 올리기
return "board/boardWrite";
}
//게시글 작성
@PostMapping("/{boardCode:[0-9]+}/insert")
public String boardInsert(
@PathVariable("boardCode") int boardCode
,Board board // 커맨드 객체(필드에 파라미터 담겨 있음)
,@RequestParam(value="images",required = false) List<MultipartFile> images
,@SessionAttribute("loginMember") Member loginMember
,RedirectAttributes ra
,HttpSession session)
throws IllegalStateException, IOException{
// 파라미터 : 제목, 내용, 파일(0~5개)
// 파일저장경로 : HttpSession
// 세션 : 로그인한 회원의 번호
// 리다이렉트 시 데이터 전달 : RedirectAttributes
// 작성 성공 시 이동할 게시판 코드 : @PathVariable("boardCode")
/*List<MultipartFile>
* - 업로드된 이미지가 없어도 List에 요소 MultipartFile 객체가 추가됨
*
* - 단, 업로드된 이미지가 없는 MultipartFile객체는
* 파일크기(size)가 0 또는 파일명(getOriginalFileName())이 ""
*
* */
// 1. 로그인한 회원 번호를 얻어와 board에 세팅
board.setMemberNo(loginMember.getMemberNo());
// 2. boardCode도 board에 세팅
board.setBoardCode(boardCode);
// 3. 업로드된 이미지 서버에 실제로 저장되는 경로
// + 웹에서 요청 시 이미지를 볼 수 있는 경로(웹 접근경로)
String webPath ="/resources/images/board/";
String filePath = session.getServletContext().getRealPath(webPath);
// 게시글 삽입 서비스 호출 후 삽입된 게시글 번호 반환 받기
int boardNo = service.boardInsert(board,images,webPath,filePath);
// 게시글 삽입 성공 시
// -> 방급 삽입한 게시글의 상세 조회 페이지 리다이렉트
// -> /board/{boardCode}/{boardNo}
String message = null;
String path = "redirect:";
if(boardNo>0) {
message = "게시글이 등록되었습니다.";
path += "/board/" +boardCode + "/"+ boardNo;
}else {
message = "게시글 등록 실패되었습니다.";
path += "insert";
}
ra.addFlashAttribute("message",message);
return path;
}
}
📗 BoardService2.jsp
public interface BoardService2 {
/** 게시글 삽입
* @param board
* @param images
* @param webPath
* @param filePath
* @return boardNo
*/
int boardInsert(Board board, List<MultipartFile> images, String webPath, String filePath) throws IllegalStateException, IOException;
}
💭 게시글(제목+내용) 삽입하기
📗 BoardServiceImpl2.jsp
@Service
public class BoardServiceImpl2 implements BoardService2 {
@Autowired
private BoardDAO2 dao;
// 게시글 삽입
@Transactional(rollbackFor = Exception.class)
@Override
public int boardInsert(Board board, List<MultipartFile> images
, String webPath
, String filePath)
throws IllegalStateException, IOException {
// 0. XSS 방지 처리
board.setBoardContent(Util.XSSHandling(board.getBoardContent()));
board.setBoardTitle(Util.XSSHandling(board.getBoardTitle()));
//시퀀스 조회 --> 마이바티스트가 알아서 해줘서 할 필요 없다!
// 1. BOARD 테이블 INSERT 하기(제목, 내용, 작성자, 게시판코드)
// -> boardNo(시퀀스로 생성한 번호) 반환 받기
int boardNo = dao.boardInsert(board);
return boardNo;
}
}
🤔 BoardNo(시퀀스) 을 알아야지 어떤 boardNo에 글을 삽입하는지 알 수 있다 !
예전에는 DB를 두번 갔다왔는데 ! 마이바티스가 알아서 해준다!
📝 동적 SQL
프로그램 수행 중 SQL을 변경하는 기능
(마이바티스트의 가장 강력한 기능)
📍 useGeneratedKeys 태그
DB 내부적으로 생성한 키 (시퀀스)
전달된 파라미터의 필드로 대입 가능 여부 지정 (true = 대입가능!)
📍 selectKey 태그
INSERT/UPDATE 시 사용할 키(시퀀스)를 조회해서 파라미터의 지정된 필드에 대입
➰ order 속성
메인 SQL이 수행되기 전/후에 selectkey가 수행되도록 지정
전 : BEFORE / 후 : AFTER
➰keyProperty 속성
selectKey 조회 결과를 저장할 파라미터의 필드명
📗 board-mapper.xml
<insert id="boardInsert" parameterType="Board" useGeneratedKeys ="true">
<selectKey order="BEFORE" resultType="_int" keyProperty="boardNo">
SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO BOARD
VALUES(#{boardNo},
#{boardTitle},
#{boardContent},
DEFAULT, DEFAULT,DEFAULT,DEFAULT,
#{memberNo},
#{boardCode})
</insert>
💭 게시글(이미지) 삽입하기
📗 BoardServiceImpl2.jsp
package edu.kh.project.board.model.service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.board.model.dao.BoardDAO2;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.board.model.exception.FileUploadException;
import edu.kh.project.common.utility.Util;
@Service
public class BoardServiceImpl2 implements BoardService2 {
@Autowired
private BoardDAO2 dao;
// 게시글 삽입
@Transactional(rollbackFor = Exception.class)
@Override
public int boardInsert(Board board
, List<MultipartFile> images
, String webPath, String filePath)
throws IllegalStateException, IOException {
// 0. XSS 방지 처리
board.setBoardContent(Util.XSSHandling(board.getBoardContent()));
board.setBoardTitle(Util.XSSHandling(board.getBoardTitle()));
//시퀀스 조회 --> 마이바티스트가 알아서 해줘서 할 필요 없다!
// 1. BOARD 테이블 INSERT 하기(제목, 내용, 작성자, 게시판코드)
// -> boardNo(시퀀스로 생성한 번호) 반환 받기
int boardNo = dao.boardInsert(board);
//2. 게시글 삽입 성공시
// 업로드된 이미지가 있다면 BOARD_IMG테이블에 삽입된 DAO 호출
if(boardNo>0) { // 게시글 삽입 성공
// List<MultipartFile> images
// -> 업로드 된 파일이 담김 객체 MultipartFile 5개 존재
// 단, 업로드된 파일이 없어도 MultipartFile 객체는 존재
// 실제 업로드된 파일의 정보를 기록한 List
List<BoardImage> uploadList = new ArrayList<BoardImage>();
// images에 담겨 있는 파일 중 실제 업로드 된 파일만 분류
for(int i= 0 ;i<images.size();i++) {
// i번째 요소에 업로드한 파일이 있다면
if(images.get(i).getSize()>0) {
BoardImage img = new BoardImage();
// img에 파일 정보를 담아서 uploadList에 추가
img.setImagePath(webPath); // 웹 접근 경로
img.setBoardNo(boardNo); // 게시글 번호
img.setImageOrder(i); // 이미지 순서
//파일 원본명
String fileName = images.get(i).getOriginalFilename();
img.setImageOriginal(fileName); // 원본명
img.setImageReName(Util.fileRename(fileName)); // 변경명
uploadList.add(img);
}
}// 분류 for문종료
// 분류 작업 후 uploadList가 비어있지 않은 경우
// == 업로드한 파일이 있다.
if(!uploadList.isEmpty()) {
//BOARD_IMG 테이블에 INSERT하는DAO 호출
int result = dao.insertImageList(uploadList);
//result == 삽입된 행의 개수 == uploadList.size()
// 전체 insert 성공 !
if(result == uploadList.size()) {
// 서버에 파일을 저장(transferTo()메소드 이용)
// images : 실제 파일이 담긴 객체 리스트
// (업로드가 안된 인덱스 빈칸)
// uploadList : 업로드된 파일의 정보 리스트
// (원본명,변경명,순서,경로,게시글번호)
// 순서 == images 업로드된 인덱스
for(int i=0;i<uploadList.size();i++) {
// 인덱스 번호
int index = uploadList.get(i).getImageOrder();
//파일로 변환
String rename = uploadList.get(i).getImageReName();
images.get(index).transferTo(new File(filePath+rename));
}
}else { // 일부&전체 insert 실패
// ** 웹 서비스 수행 중 1개라도 실패하면 전체 실패 **
// --> rollback 필요!
// @ Transactional(rollbackFor = Exception.class)
// -> 예외가 발생해야지만 롤백
// [결론]
// 예외를 강제 발생시켜서 rollback 해야한다.
// -> 사용자 정의 예외 생성
throw new FileUploadException(); //예외 강제 발생
}
}
}
return boardNo;
}
}
📝 동적 SQL
📍 <foreach> 태그
특정 SQL 구문을 반복할 때 사용 반복되는 사이에 구분자(separator)를 추가할 수 있음.
collection : 반복할 객체의 타입 작성(list, set, map...)
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)
open : 반복 전에 출력할 sql
close : 반복 종료 후에 출력한 sql
separator : 반복 사이사이 구분자
📗 board-mapper.xml
<!-- 이미지 리스트(여러개 삽입) -->
<insert id="insertImageList" parameterType="List">
INSERT INTO BOARD_IMG
SELECT SEQ_IMG_NO.NEXTVAL, A.*
FROM(
<foreach collection="list" item="img" separator=" UNION ALL ">
SELECT #{img.imagePath} IMG_PATH,
#{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageOrder} IMG_ORDER,
#{img.boardNo} BOARD_NO
FROM DUAL
</foreach>
) A
</insert>
📗 Util.java
package edu.kh.project.common.utility;
import java.text.SimpleDateFormat;
public class Util {
// Cross Site Scripting(XSS) 방지 처리
// - 웹 애플리케이션에서 발생하는 취약점
// - 권한이 없는 사용자가 사이트에 스크립트를 작성하는 것
public static String XSSHandling(String content) {
// 스크립트나 마크업 언어에서 기호나 기능을 나타내는 문자를 변경 처리
// & - &
// < - <
// > - >
// " - "
content = content.replaceAll("&", "&");
content = content.replaceAll("<", "<");
content = content.replaceAll(">", ">");
content = content.replaceAll("\"", """);
return content;
}
// 파일명 변경 메소드
public static String fileRename(String originFileName) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String date = sdf.format(new java.util.Date(System.currentTimeMillis()));
int ranNum = (int) (Math.random() * 100000); // 5자리 랜덤 숫자 생성
String str = "_" + String.format("%05d", ranNum);
String ext = originFileName.substring(originFileName.lastIndexOf("."));
return date + str + ext;
}
}
📗 FileUploadException.java
package edu.kh.project.board.model.exception;
// 사용자 정의 예외를 만드는 법
// -> Exception 관련 클래스를 상속받으면 된다.
// tip! unchecked exception을 만들고 싶으면
// RuntimeException을 상속받아 구현하면 된다.
// unchecked exception : 예외 처리 선택
// checked exception : 예외 처리 필수
// 예외 처리 : try-catch / throws
public class FileUploadException extends RuntimeException {
public FileUploadException() {
super("파일 업로드 중 예외 발생");
}
public FileUploadException(String message) {
super(message);
}
}
📗 servlet-context.xml
servlet-context.xml의 하단 Namespaces 탭에서 있는 aop와 tx를 체크해 준다.
그 후, 아래의 코드를 작성해주면 된다.
<!-- nameapaces 탭에서 aop,tx 체크 -->
<!-- @Transactional 어노테이션 인식, 활성화 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- AOP Proxy를 이용한 관점 제어 자동화 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
728x90
'ON > spring' 카테고리의 다른 글
[ Spring ] 댓글 / 대 댓글 기능 ⑬ (0) | 2023.08.30 |
---|---|
[ Spring ] 게시글 수정 ⑫ (0) | 2023.08.29 |
공공 데이터 - OPEN API (0) | 2023.08.25 |
[ Spring ] 프로필 이미지 추가 | 변경 | 삭제 ⑨ (1) | 2023.08.24 |
[ Spring ] 게시글 상세 좋아요 | 조회수 ⑧ (0) | 2023.08.23 |