YJ의 새벽
Spring 17 ( 게시글 작성/수정) 본문
-- 게시글 작성과, 수정이 쿼리파라미터 ( mode-insert / mode-update ) 로 구분된다.
-- 일단 먼저, 글쓰기 버튼을 눌렀을때, 받는 주소를 연결해주자. ( 글목록 글쓰기 버튼 )
-- 수정 버튼 눌렀을때, 받는 주소 수정. ( 글목록 타이틀 클릭후 나오는 수정 버튼 )
---- 수정버튼 누르면, 다시 글쓰기로 ! 이동하는걸 확인하자.
---- BoardController 에서, GetMapping.
--- 글쓰기 버튼으로 들어온 글쓰기폼과,
수정하기버튼으로 들어온 글수정폼을 하나의 컨트롤러로 맵핑해주었다.
--- '수정하기' 버튼으로 들어왔다면 ( mode='update' )
미리 게시글정보를 받아온 service 를 호출하여 데이터를 깔아주자. ( title, image , content )
--- '글쓰기' 버튼으로 들어왔다면 ( mode='insert' )
주소값에 ( 쿼리파라미터 no ) 가 없을수도 있으니, 기본값 ( defaulValue ) 을 넣어둔다.
------------------------------------------------------------------------------------------------
---게시글을 작성해보자 .
---- boardTitle , images[0~5] , boardContent 의 파라미터를 얻어온다.
---boardController . 서비스 넘어가기전 컨트롤러 클래스.
--1) 로그인한 회원번호 얻어와서 . 미리 detail VO 에 받아놓자 . ( 클래스위에 SessionAttributes )
--2) 이미지 저장경로 얻어오기 ( webPath, folderPath )
--3) 글의 삽입인지? 수정인지? if ( mode=='insert' )
-- 글의 삽입( mode =' insert' ) 이라면 . 게시판 부분삽입 ( 제목, 내용, 회원번호 , 게시판코드 ) (insert)
후 게시글번호 ( boardNo ) 을 반환받아. 이를 이용 이미지를 넣자.(insert)
-- 두번의 insert 중 한번이라도 실패하면 전체 rollback 트랜잭션 처리 !!
--4) 글의 수정 ( mode='update' ) 이라면 ??
-- 쿼리파라미터로 게시글의 번호가 따라온다. 수정결과만 반환받자.
// 글쓰기 ( 등록 )
@PostMapping("/write/{boardCode}")
public String boardWrite( @PathVariable("boardCode") int boardCode,
@ModelAttribute BoardDetail detail, // boardTitle,content,boardNo( hidden에 수정)
@RequestParam (value="images",required = false) List<MultipartFile> imageList, // 업로드이미지 리스트
String mode,
@ModelAttribute("loginMember") Member loginMember,
RedirectAttributes ra ,
HttpServletRequest req , // 이미지저장 경로용
@RequestParam(value="cp",required = false,defaultValue = "1") int cp,
@RequestParam(value="deleteList",required = false) String deleteList
) throws IOException{
// 1) 로그인한 회원번호 얻어와서, detail 에 세팅
detail.setMemberNo( loginMember.getMemberNo() );
// 2) 이미지 저장 경로 얻어오기 ( webPath, folderPath )
String webPath = "/resources/images/board/";
String folderPath = req.getSession().getServletContext().getRealPath(webPath);
// 3) 삽입이냐 ?? 수정이냐 ??
if ( mode.equals("insert")) { // 삽입
// 게시글 부분 삽입 ( 제목,내용,회원번호,게시판코드 )
// --> 삽입된 게시글의 번호 (boardNo) 반환 , 삽입이 끝나면 게시글상세조회로 리다이렉트
// 게시글에 포함된 이미지 정보 삽입 ( 0~5 개, 게시글 번호 필요 )
// --> 실제 파일로 변환해서 서버에 저장
// 두번의 insert 중 한번이라도 실패하면 , 전체 rollback 트랜잭션 처리 !!!
int boardNo = service.insertBoard(detail, imageList, webPath, folderPath);
String path = null;
String message = null;
if ( boardNo > 0 ) {
// /board/write/1 --> board/detail/1/1500
path ="../detail/" +boardCode+ "/" +boardNo;
message = "게시글이 등록 되었습니다";
}else {
path = req.getHeader("referer");
message = "게시글 삽입 실패..";
}
ra.addFlashAttribute("message",message);
return "redirect:"+ path;
}else { // "update" 수정
// 게시글 수정 서비스 호출
// 게시글 번호를 알고있기때문에 수정결과만 반환받으면 된다.
int result = service.updateBoard(detail, imageList, webPath,folderPath, deleteList);
String path = null;
String message = null;
if ( result > 0 ) {
// /board/write/{boardCode}
//--> /board/detail/{boardCode}/{boardNo}?cp=10
path ="../detail/" +boardCode+ "/" +detail.getBoardNo() + "?cp="+ cp ;
message = "게시글이 수정!! 되었습니다";
}else {
path = req.getHeader("referer");
message = "게시글 수정!! 실패..";
}
ra.addFlashAttribute("message",message);
return "redirect:"+ path;
}
}
---boardService 인터페이스.
// 게시글 삽입 + 이미지 삽입
int insertBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath) throws IOException;
// 게시글 수정 서비스.
int updateBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath,
String deleteList) throws IOException;
}
--- Controller 에서 넘어온 boardServiceImpl , 서비스 클래스 .
// 게시글 삽입 + 이미지 삽입 서비스 구현
// Spring 에서 트랜잭션 처리하는 방법 ?!
// *** AOP (관점 지향 프로그래밍) 을 이용해서 DAO -> Service 또는 Service코드 수행 시점에
// 예외가 발생했을때 rollback 을 수행
// 방법 1 )))) <tx:advice> XML 을 이용한 방법 -> 패턴을 지정하여 일치하는 메서드 호출 시 자동으로 트랜잭션 제어
// 방법 2 )))) @Transactional 선언적 트랜잭션 처리방법
// --> RuntimeException ( Unchecked Exception) 처리를 기본값으로 갖음.
// checked Exception : 예외처리가 필수 (transFerTo()) --> SQL 관련 예외, 파일업로드 관련예외
//unchecked Exception : 예외처리가 선택 ( int a = 10/0; )
// rollbackFor : rollback 을 수행하기 위한 예외의 종류를 작성
@Transactional(rollbackFor = { Exception.class })
@Override
public int insertBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath) throws IOException {
// 1. 게시글 삽입 .
// 1) XSS 방지 처리 + 개행문자 처리
detail.setBoardTitle( Util.XSSHandling( detail.getBoardTitle() ));
detail.setBoardContent( Util.XSSHandling( detail.getBoardContent()));
detail.setBoardContent( Util.newLineHandling( detail.getBoardContent() )); // 개행문자 처리
// 2) 게시글 삽입 DAO 호출 후 게시글 번호 반환받기.
// --1. 서비스 결과 반환 후 컨트롤러에서 상세조회로 리다이렉트 하기위해.
// --2. 동일한 시간에 삽입이 2회 이상 진행된 경우 시퀀스 번호가
// 의도와 달리 여러번 증가해서
// 이후에 작성된 이미지삽입코드에 영향을 미치는걸 방지하기위해.
int boardNo = dao.insertBoard(detail);
if ( boardNo > 0 ) {
// 2. 이미지 삽입 실행
// imageList : 실제 파일이 담겨있는 리스트 .
// boardImageList : DB 에 삽입할 이미지 정보만 담겨있는 리스트.
// reNameList : 변경될 파일명이 담겨있는 리스트.
List<BoardImage> boardImageList = new ArrayList<BoardImage>();
List<String> reNameList = new ArrayList<String>();
// imageList 에 담겨있는 파일정보 중 실제 업로드된 파일만 분류하는 작업.
for (int i=0; i < imageList.size(); i++) {
if ( imageList.get(i).getSize() > 0 ) { // i 번째 요소에 업로드된 이미지가 있을경우
// 변경된 파일명 저장. ( 20250343013_20 )
String reName = Util.fileRename( imageList.get(i).getOriginalFilename() );
reNameList.add(reName);
// BoardImage 객체 생성하여 값 세팅후 boardImageList 추가 .
BoardImage img = new BoardImage();
img.setBoardNo(boardNo); // 게시글번호
img.setImageLevel(i); // 이미지 순서 ( 0,1,2,3,4 )
img.setImageOriginal(imageList.get(i).getOriginalFilename()); // 원본 파일명
img.setImageReName( webPath+reName); // 웹접근경로 + 변경된 파일명
boardImageList.add(img);
}
}
// 분류작업 종료 후 boardImageList 가 비어있지 않은 경우 == 파일이 업로드가 된경우
if ( !boardImageList.isEmpty() ) {
int result = dao.insertBoardImageList(boardImageList);
// result == 삽입 성공한 행의 개수.
if ( result == boardImageList.size() ) { // 삽입된 행의갯수와 업로드 이미수가 같을경우
// 서버에 이미지 저장
for ( int i=0; i< boardImageList.size(); i++) {
int index = boardImageList.get(i).getImageLevel();
imageList.get(index).transferTo(new File(folderPath+reNameList.get(i)));
}
}else { // 이미지 삽입실패시 rollback.
// 강제를 예외를 발생시켜 rollback 수행하게 함.
// --> 사용자 정의 예외 만들기.
throw new InsertFailException();
}
}
}
return boardNo;
}
// 게시글 수정
@Transactional(rollbackFor = {Exception.class}) // 모든 종류 예외 발생시 롤백.
@Override
public int updateBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath,
String deleteList) throws IOException {
// 1) XSS 방지 처리 + 개행문자 처리
detail.setBoardTitle( Util.XSSHandling( detail.getBoardTitle() ));
detail.setBoardContent( Util.XSSHandling( detail.getBoardContent()));
detail.setBoardContent( Util.newLineHandling( detail.getBoardContent() )); // 개행문자 처리
// 2) 게시글만 수정하는 DAO 호출
// ( 제목, 내용, 마지막수정일(sysdate)) 만 수정
int result = dao.updateBoard(detail);
if ( result > 0 ) {
// 3) 업로드된 이미지만 분류하는 작업 수행
List<BoardImage> boardImageList = new ArrayList<BoardImage>();
List<String> reNameList = new ArrayList<String>();
// imageList 에 담겨있는 파일정보 중 실제 업로드된 파일만 분류하는 작업.
for (int i=0; i < imageList.size(); i++) {
if ( imageList.get(i).getSize() > 0 ) { // i 번째 요소에 업로드된 이미지가 있을경우
// 변경된 파일명 저장. ( 20250343013_20 )
String reName = Util.fileRename( imageList.get(i).getOriginalFilename() );
reNameList.add(reName);
// BoardImage 객체 생성하여 값 세팅후 boardImageList 추가 .
BoardImage img = new BoardImage();
img.setBoardNo(detail.getBoardNo()); // 게시글번호
img.setImageLevel(i); // 이미지 순서 ( 0,1,2,3,4 )
img.setImageOriginal(imageList.get(i).getOriginalFilename()); // 원본 파일명
img.setImageReName( webPath+reName); // 웹접근경로 + 변경된 파일명
boardImageList.add(img);
}
}
// 4) deleteList를 이용해서 삭제된 이미지 delete
if ( !deleteList.equals("")) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("boardNo", detail.getBoardNo());
map.put("deleteList", deleteList);
result = dao.deleteBoardImage(map);
}
if ( result > 0 ) {
// 5) boardImageList를 순차 접근하면서 하나씩 update
for( BoardImage img : boardImageList) {
result = dao.updateBoardImage(img); // 변경명, 원본명, 게시글번호, 레벨
// 결과 1 -> 수정 O -> 기존 이미지가 있었다.
// 결과 2 -> 수정 X -> 기존 이미지가 없었다. --> insert 작업.수행
// 6) update 실패하면 insert .
if ( result == 0 ) {
result = dao.insertBoardImage(img);
// -> 값을 하나씩 대입해서 삽입하는경우 , 결과가 0이 나올수는 없다.!
// 단, 예외 (제약조건,sql문법오류) 는 발생할수 있다.
}
}
// 7) 업로드된 이미지가 있다면 서버에 저장.
if ( !boardImageList.isEmpty() && result !=0 ) {
// 서버에 이미지 저장
for ( int i=0; i< boardImageList.size(); i++) {
int index = boardImageList.get(i).getImageLevel();
imageList.get(index).transferTo(new File(folderPath+reNameList.get(i)));
}
}
}
}
return result;
}
---boardDAO
// 게시글 삽입 DAO
public int insertBoard(BoardDetail detail) {
int result = sqlSession.insert("boardMapper.insertBoard", detail); // 0 or 1
if ( result > 0 ) result = detail.getBoardNo(); // 이미지넣기위한 boardNo 반환.
return result;
}
// 게시글 이미지 삽입 DAO
public int insertBoardImageList(List<BoardImage> boardImageList) {
return sqlSession.insert("boardMapper.insertBoardImageList",boardImageList);
}
// 게시글 수정 DAO
public int updateBoard(BoardDetail detail) {
return sqlSession.update("boardMapper.updateBoard",detail);
}
// 게시글 이미지 삭제 DAO
public int deleteBoardImage(Map<String, Object> map) {
return sqlSession.delete("boardMapper.deleteBoardImage",map);
}
// 게시글 이미지 1개 수정
public int updateBoardImage(BoardImage img) {
return sqlSession.update("boardMapper.updateBoardImage",img);
}
// 게시글 이미지 1개 삽입
public int insertBoardImage(BoardImage img) {
return sqlSession.insert("boardMapper.insertBoardImage",img);
}
---board-mapper
<!-- 게시글 삽입 -->
<!--
useGeneratedKeys 속성 : DB내부적으로 생성한 키 (ex 시퀀스 ) 를 받는
JDBC getGeneratedKeys() 메서드를 사용하도록 설정. ( 기본값 false )
동적 SQL 중 <selectKey>
- <selectKey> : INSERT/UPDATE에 사용된 키 (ex 시퀀스 ) 를 원하는 변수/필드에 담아 반환하는태그
- keyProperty : Key를 담을 변수/필드 지정하는 속성
- order : INSERT/UPDATE 에 작성된 메인 SQL 이 수행되기 전(BEFORE) 또는 후(AFTER) 에
<selectKey> 내부 SQL 이 수행되도록 순서를 지정하는 속성.
-->
<insert id="insertBoard" parameterType="detail" useGeneratedKeys="true">
<selectKey keyProperty="boardNo" resultType="_int" order="BEFORE">
SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO BOARD VALUES(
#{boardNo}, #{boardTitle}, #{boardContent},
DEFAULT, DEFAULT, DEFAULT, DEFAULT, #{memberNo}, #{boardCode}
)
</insert>
<!-- 게시글 이미지 삽입 (리스트)-->
<!--
동적 SQL중 <foreach>
- 특정 SQL 구문을 반복할때 사용 .
- 반복되는 사이에 구분자추가 가능 . (separator)
collection : 반복할 객체의 타입 작성 ( list,set,map... )
item : collection 에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 ( 0,1,2,3... )
open : 반복 전에 출력할 SQL
close : 반복 후에 출력할 SQL
separator : 반복 사이사이 들어갈 구분자
-->
<insert id="insertBoardImageList" parameterType="list">
INSERT INTO BOARD_IMG
SELECT SEQ_IMG_NO.NEXTVAL IMG_NO, A.* FROM(
<foreach collection="list" item="img" separator="UNION ALL" >
SELECT #{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageLevel} IMG_LEVEL,
#{img.boardNo} BOARD_NO
FROM DUAL
</foreach>
) A
</insert>
<!-- 게시글 수정 -->
<update id="updateBoard">
UPDATE BOARD SET
BOARD_TITLE = #{boardTitle},
BOARD_CONTENT = #{boardContent},
UPDATE_DT = SYSDATE
WHERE BOARD_NO = #{boardNo}
</update>
<!-- 게시글 이미지 삭제 (IN 구문작성시 삭제되는 deleteList에 ' ' 가 없도록 주의) -->
<delete id="deleteBoardImage">
DELETE FROM BOARD_IMG
WHERE BOARD_NO = ${boardNo}
AND IMG_LEVEL IN ( ${deleteList} )
</delete>
<!-- 게시글 이미지 1개 수정 -->
<update id="updateBoardImage">
UPDATE BOARD_IMG SET
IMG_RENAME = #{imageReName},
IMG_ORIGINAL = #{imageOriginal}
WHERE BOARD_NO = #{boardNo}
AND IMG_LEVEL = #{imageLevel}
</update>
<!-- 게시글 이미지 1개 삽입 -->
<insert id="insertBoardImage">
INSERT INTO BOARD_IMG VALUES(
SEQ_IMG_NO.NEXTVAL,
#{imageReName},
#{imageOriginal},
#{imageLevel},
#{boardNo}
)
</insert>
</mapper>
--- 실행이 안됐을때 생성한 예외클래스 .
'Spring > Spring' 카테고리의 다른 글
Spring 19 ( 댓글 , 대댓글 삽입,수정,삭제 ) (1) | 2023.05.10 |
---|---|
Spring 18 ( 게시글 삭제 ) (0) | 2023.05.09 |
Spring 16 ( pagination + 게시글조회 + 조회수증가(중복x)) (0) | 2023.05.02 |
Spring 15 ( interceptor ) , 게시판 페이지 목록 (0) | 2023.05.01 |
Spring 14 ( log4jdbc 설정 ) (0) | 2023.05.01 |