TODAY TOTAL
Back-End (44)
[Spring] AOP

AOP(Aspect-Oriented Programming) 관점 지향 프로그래밍라는 뜻이다.

 

기술면접에서 은근 많이 나오는 질문

:: 여러 모듈의 공통적으로 사용하는 기능을 분리하여 관리함으로써 재사용성을 높여주는 프로그래밍 기법

 

관점(Aspect) == 관심사(concern) 통용됨

 

관심사를 분리 해서  핵심로직만 집중 할 수 있게 도와준다. 그것이 AOP.

 

AOP 용어 개념 

 

  • Aspect : 애플리케이션 내 여러 군데에 흩어져 있는 코드. 여러 객체에 공통적으로 적용되는 관심 사항 
  • JoinPoint : 프로그램 실행 중일 때 발생하는 메서드 실행, 생성자 호출, 필드 값 변경 등 특별한 지점을 의미
  • Advice : 특정 JoinPoint의 Aspect에 의한 동작을 의미
  • Pointcut : JoinPoint의 정규 표현식 JoinPoint가 Pointcut에 일치 할 때마다 관련된 Advice 실행
  • Weaving : Aspect를 대상 객체에 연결시켜 관점지향 객체로 만드는 과정을 의미

 

 

AOP는 AspectJ 라이브러리의 도움을 많이 받는다.

 

:: AspectJ는 AOP의 확장 기능으로 단순함과 이용성을 강조하고 폭넓게 AOP를 쓸 수 있다.(표준)

 

 

SampleServiceImpl.java

 

@Service
public class SampleServiceImpl implements SampleService{

	@Override
	public Integer doAdd(String str1, String str2) throws Exception {
		return Integer.parseInt(str1) + Integer.parseInt(str2);
	}

}

 

:: 반드시 @service 어노테이션을 사용해 이것이 서비스인 것을 표시

:: 숫자형의 문자열을 인자 값으로 받으면 해당 값을 10진수의 Integer형으로 반환 해줍니다.

 

 

@Aspect
@Log4j
@Component
public class LogAdvice {

	//Aspect 어노테이션은 해당 객체가 Aspect를 구현한다는 것을 나타냄
	//Component는 빈으로 인식 시키기 위해
    
	@Before( "execution(*org.zerock.service.SampleService*.*(..))")
	//Before. BeforeAdvice를 구현한 메서드를 추가
	//[접근 제한자] 리턴 타입 [패키지 또는 클래스 명]메소드 명(..) <- args
	public void logBefore() {
		log.info("============================");
		//
	}
	//root-context.xml의 네임스페이스 aop,context를 추가..
	
	@Before( "execution(*org.zerock.service.SampleService*.doAdd(String,String)) && args(str1,str2)")
	//execution으로 시작하는 Pointcut 설정에 doAdd 메소드를 명시.
	//뒤쪽의 &&은 logBeforeWithParam파라미터 설정. 
	//하지만 간단하게 찾는데는 좋지만 여러종류의 메서드를 찾을 땐 좋지 않다.

	public void logBeforeWithParam(String str1,String str2) {
		log.info("str1:" + str1);
		log.info("str2:" + str2);
		//
	}
	
	@AfterThrowing(pointcut = "execution(* org.zerock.service.SampleService*.*(..))",
				throwing = "exception")
	//@AfterThrowing은 지정된 대상이 예외를 발생한 후에 동작하면서 문제를 찾을 수 있도록 도와줌.

	public void logException(Exception exception) {
		log.info("Exception...");
		log.info("exception:" + exception);
		//
	}
	
	

}

 

트랜젝션 'ACID'

 

원자성 : 어떤 트랜젝션이 A와 B로 구성된다면 A,B의 처리 결과는 동일한 결과여야 한다.

 

일관성 : 트랜젝션으로 처리된 데이터와 일반 데이터사이에는 전혀 차이가 없어야 한다.

 

격리 : 트랜젝션으로 처리되는 중간에 외부에서의 간섭 X

 

영속성 : 트랜젝션이 성공적이면 그 결과는 영속적 보관

 

트랜젝션 예로는 '계좌 이체' (나의 계좌에서 출금, 입금)

 

 

 

 

 

 

 

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] 댓글 구현 2  (0) 2021.08.29
[Spring] 댓글 구현  (0) 2021.08.29
[Spring] 검색 화면 처리  (0) 2021.08.11
[Spring] 검색 처리  (0) 2021.08.11
[Spring] 페이징 처리 및 번호 출력  (0) 2021.08.06
  Comments,     Trackbacks
[Spring] 댓글 구현 2

 

 

Service, dao, vo, mapper

 

mapper

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.study.reply.dao.IReplyDao">


	
	<select id = "getReplyListByParent" resultType="com.study.reply.vo.ReplyVO">
	<include refid="common.BEFORE_PAGING_QRY" />
	SELECT re_no
       , re_category
       , re_parent_no
       , re_mem_id
       , (SELECT mem_name FROM member WHERE mem_id = re_mem_id ) AS re_mem_name
       , re_content
       , re_ip
       , TO_CHAR(re_reg_date,'YYY.MM.DD') AS re_reg_date
	FROM reply
	WHERE re_category = #{reCategory}
	AND re_parent_no = #{reParentNo}
	ORDER BY re_no DESC
	<include refid="common.AFTER_PAGING_QRY" />	
	</select>	
	
	<select id="getReplyCountByParent" resultType="int" >
	SELECT count(1) AS cnt
	FROM reply
	WHERE re_category = #{reCategory}
	AND re_parent_no = #{reParentNo}
	</select>

<select id ="getReply" resultType="com.study.reply.vo.ReplyVO">

SELECT re_no
       , re_category
       , re_parent_no
       , re_mem_id
       , (SELECT mem_name FROM member WHERE mem_id = re_mem_id ) AS re_mem_name
       , re_content
       , re_ip
       , TO_CHAR(re_reg_date,'YYY.MM.DD') AS re_reg_date
	FROM reply
	WHERE re_no = #{reNo}
</select>


<insert id="insertReply" >
			 INSERT INTO reply (
		    re_no
		    , re_category
		    , re_parent_no
		    , re_mem_id
		    , re_content
		    , re_ip
		) VALUES (
			seq_reply.nextval
			,#{reCategory}
			,#{reParentNo}
			,#{reMemId}
			,#{reContent}
			,#{reIp}
		)
	</insert>
	
	
	<update id="updateReply">
	
		UPDATE reply
			SET re_content = #{reContent}
				,re_mod_date = SYSDATE
		 WHERE re_no = #{reNo}
	
	</update>
	
	
	<delete id="deleteReply">
		delete from reply
		where re_no = #{reNo} 
	</delete>
	
</mapper>

 

<include refid="common.BEFORE_PAGING_QRY" /> 는 공통 페이징

 

common.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<mapper namespace="common">
	
 	<sql id="BEFORE_PAGING_QRY">
 		SELECT *
			FROM ( SELECT rownum AS rnum
						, a.* 
							FROM ( 
 	</sql>
 	
 	<sql id="AFTER_PAGING_QRY">
 		<![CDATA[
 		  			) a  
		  	where rownum <= #{endRow}  
		  ) b  							
		  where rnum BETWEEN #{startRow} AND #{endRow}
		 ]]>  
 	</sql>
 	
	
</mapper>

 

Vo

package com.study.reply.vo;

import java.io.Serializable;

import org.apache.commons.lang3.builder.ToStringBuilder;

@SuppressWarnings("serial")
public class ReplyVO implements Serializable {

		private int reNo;       		//댓글번호
		private String reCategory;   	//분류
		private int reParentNo;      	// 부모 번호
		private String reMemId;  		//작성자ID
		private String reContent;    	//댓글 내용 
		private String reIp;  			//IP
		private String reRegDate;  		//댓글 등록일자
		private String reModDate; 		//댓글 수정일자
		private String reMemName;
        
        get,set,string... 생략...

 

SerarchVO

package com.study.reply.vo;

import com.study.common.vo.PagingVO;

public class ReplySearchVO extends PagingVO{

	private String reCategory;
	/* 분류(FREE, BOARD,PDS,..) */
	private int reParentNo;
	/* 부모 번호 */
	public String getReCategory() {
		return reCategory;
	}
	public void setReCategory(String reCategory) {
		this.reCategory = reCategory;
	}
	public int getReParentNo() {
		return reParentNo;
	}
	public void setReParentNo(int reParentNo) {
		this.reParentNo = reParentNo;
	}
}

 

Service

package com.study.reply.service;

import java.util.List;

import com.study.exception.BizAccessFailException;
import com.study.exception.BizNotFoundException;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;

public interface IReplyService {

	/** 댓글 목록 조회 <br>
	* <b>필수 : reCategory, reParentNo </b>
	*/
    
	public List<ReplyVO> getReplyListByParent(ReplySearchVO searchVO);
	/** 댓글등록 */
	public void registReply(ReplyVO reply) ;
    
	/** 댓글 수정 <br>
	* 댓글이 존재하지 않으면 BizNotFoundException
	* 댓글 작성자와 로그인 사용자가 다른 경우 BizAccessFailException
	 * @throws BizAccessFailException 
	*/
    
	public void modifyReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException, BizAccessFailException;
    
	/**
	* 댓글 삭제 <br>
	* 해당글의 존재하지 않으면 BizNotFoundException
	* 댓글 작성자와 로그인 사용자가 다른 경우 BizAccessFailException
	*/
    
	public void removeReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException;
}

 

ServiceImpl

 

package com.study.reply.service;

@Service
public class ReplyServiceImpl implements IReplyService{
	
	@Inject
	private IReplyDao replyDao;
	
	@Override
	public List<ReplyVO> getReplyListByParent(ReplySearchVO searchVO) {
	int rowCount = replyDao.getReplyCountByParent(searchVO);
	searchVO.setTotalRowCount(rowCount);
	searchVO.setting();
	return replyDao.getReplyListByParent(searchVO);
	}
	
	@Override
	public void registReply(ReplyVO reply) {
	replyDao.insertReply(reply);
	}
	
	@Override
	public void modifyReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException {
	ReplyVO vo = replyDao.getReply(reply.getReNo());
	if(vo == null) {
	throw new BizNotFoundException("글번호[" + reply.getReNo() + "]을 조회하지 못했습니다." );
	}
	if(!vo.getReMemId().equals(reply.getReMemId())) {
	throw new BizAccessFailException("해당 글의 작성자가 아닙니다." );
	}
	replyDao.updateReply(reply);
}
	@Override
	public void removeReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException {
		ReplyVO vo = replyDao.getReply(reply.getReNo());
		if(vo == null) {
		throw new BizNotFoundException("글번호[" + reply.getReNo() + "]을 조회하지 못했습니다." );
		}
		if(!vo.getReMemId().equals(reply.getReMemId())) {
		throw new BizAccessFailException("해당 글의 작성자가 아닙니다." );
		}
		replyDao.deleteReply(reply);
	}
}

 

:: DB에서 dao로 그리고 service단으로 올 때 값 조회가 되지 않을 시 발생

 

 

Dao

package com.study.reply.dao;

@Mapper
public interface IReplyDao {

	public int getReplyCountByParent(ReplySearchVO searchVO);
	public List<ReplyVO> getReplyListByParent(ReplySearchVO searchVO);
	public ReplyVO getReply(int reNo);
	public int insertReply(ReplyVO reply);
	public int updateReply(ReplyVO reply);
	public int deleteReply(ReplyVO reply);
}

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] AOP  (0) 2021.09.03
[Spring] 댓글 구현  (0) 2021.08.29
[Spring] 검색 화면 처리  (0) 2021.08.11
[Spring] 검색 처리  (0) 2021.08.11
[Spring] 페이징 처리 및 번호 출력  (0) 2021.08.06
  Comments,     Trackbacks
[Spring] 댓글 구현

댓글은 View.jsp 게시판 상세보기를 클릭 시 게시판 상세보기가 나오고 그 밑에

댓글이 나오는건데 댓글은 JavaScript에 있는 fn_reply_list() 함수를 호출하고 댓글정보, 댓글 개수를 DB에서 가져온다.

 

 

그 뒤론 더보기, 삭제, 수정, 저장 함수를 호출하는 구조

 

ajax는 많이 쓰니 이해하기!

 

 

Controller

@RestController //@Controller + @ResponsBody
public class ReplyController {
	
	@Inject
	private IReplyService replyService;
	
		@RequestMapping(value="/reply/replyList", produces = "application/json;charset=UTF-8" )
		public Map<String, Object> replyList(ReplySearchVO searchVO) throws Exception {
		List<ReplyVO> list = replyService.getReplyListByParent(searchVO);
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("result", true);
		map.put("data", list);
		map.put("count", list.size());
		return map;
		}
		// @PostMapping ("/reply/replyRegist")
		@RequestMapping(value="/reply/replyRegist", method = RequestMethod.POST)
		public Map<String, Object> replyRegist(ReplyVO reply, HttpServletRequest req, HttpSession session )
		throws Exception {
		reply.setReIp(req.getRemoteAddr());
		UserVO user = (UserVO)session.getAttribute("USER_INFO");
		reply.setReMemId(user.getUserId());
		replyService.registReply(reply);
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("result", true);
		map.put("msg", "정상 등록 되었습니다.");
		return map;
		}

 

@RestController : @Controller와 @ResponseBody가 합친 어노테이션. 쓰면 좋은 점은 메소드에 매번 @ResponseBody를 붙여 주지 않아도 된다.

 

@RequestBody : 요청한 Body 내용을 자바 오브젝트로 변환시켜주는 역할이다. 즉, POST방식의 HTTP 바디 안에 JSON으로 넘어오는 값을 VO에 바인딩한다. 요청하는 곳으로 바로 감. ( Get 요청일 경우 RequestBody를 사용 못함)

 

@ResponseBody : VO 객체를 JSON으로 바꿔서 HTTP body에 담는 스프링 @

메소드의 리턴 값을 Response의 body에 담는 역할

 

 

		reply.setReIp(req.getRemoteAddr());
		UserVO user = (UserVO)session.getAttribute("USER_INFO");
		reply.setReMemId(user.getUserId());

 

:: 로그인 한 후 댓글을 쓸 수 있고, 댓글 쓴 사람은 자기 댓글만 수정,삭제 할 수 있게 하기 위해 User 정보를 가져온다.

 

 

@RequestMapping(value="/reply/replyModify")
		public Map<String, Object> replyModify(ReplyVO reply, HttpSession session ) throws Exception {
		UserVO user = (UserVO)session.getAttribute("USER_INFO");
		reply.setReMemId(user.getUserId());
		Map<String, Object> map = new HashMap<String, Object>();
		try {
		replyService.modifyReply(reply);
		map.put("result", true);
		map.put("msg", "수정 되었습니다.");
		return map;
		} catch (BizNotFoundException e) {
		map.put("result", false);
		map.put("msg", "글이 존재하지 않습니다.");
		return map;
		} catch (BizAccessFailException e) {
		map.put("result", false);
		map.put("msg", "접근에 실패했습니다.");
		return map;
		}
	}
		
		@RequestMapping(value="/reply/replyDelete")
		public Map<String, Object> replyDelete(ReplyVO reply, HttpSession session ) throws Exception {
			UserVO user = (UserVO)session.getAttribute("USER_INFO");
			reply.setReMemId(user.getUserId());
			Map<String, Object> map = new HashMap<String, Object>();
			try {
			replyService.removeReply(reply);
			map.put("result", true);
			map.put("msg", "삭제 되었습니다.");
			return map;
			
			} catch (BizNotFoundException e) {
			map.put("result", false);
			map.put("msg", "글이 존재하지 않습니다.");
			return map;
			
			} catch (BizAccessFailException e) {
			map.put("result", false);
			map.put("msg", "접근에 실패했습니다.");
			return map;
			}
		}
}

::Key값을 string으로 value 값을 Object형으로 put 메소드를 통해 입력 할 수 있다.

 

 

 

view.jsp

 

<!-- START : 댓글 관련 영역 -->
	<div class="container">
		<!-- START : 댓글 입력 영역 -->
		<div class="panel panel-default">
			<div class="panel-body form-horizontal">
				<form name="frm_reply" action="<c:url value='/reply/replyRegist' />"
					method="post" onclick="return false;">
					<input type="hidden" name="reParentNo" value="${board.boNo}">
					<input type="hidden" name="reCategory" value="FREE">
					<div class="form-group">
						<label class="col-sm-2 control-label">댓글</label>
						<div class="col-sm-8">
							<textarea rows="2" name="reContent" class="form-control" ></textarea>
						</div>
						<div class="col-sm-2">
							<button id="btn_reply_regist" type="button" class="btn btn-sm btn-info">등록</button>
						</div>
					</div>
				</form>
			</div>
		</div>
		<!-- END : 댓글 입력 영역 -->
<!-- START : 댓글 목록 영역-->
		<div id="id_reply_list_area"></div>
		<!-- END : 댓글 목록 영역-->

		<!-- START : 더보기 버튼 영역-->
		<div class="row text-center" id="id_reply_list_more">
			<button id="btn_reply_list_more">
				<span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span>
				더보기
			</button>
		</div>
		<!-- END : 더보기 버튼 영역-->

 

<!-- START : 댓글 수정용 Modal-->
		<div class="modal fade" id="id_reply_edit_modal" role="dialog">
			<div class="modal-dialog">
				<!-- Modal content-->
				<div class="modal-content">
					<form name="frm_reply_edit"
						action="<c:url value='/reply/replyModify' />" method="post"
						onclick="return false;">
						<div class="modal-header">
							<button type="button" class="close" data-dismiss="modal">×</button>
							<h4 class="modal-title">댓글수정</h4>
						</div>
						
						<div class="modal-body">
							<input type="text" name="reNo" value="">
							<textarea rows="3" name="reContent" class="form-control"></textarea>
						</div>
						<div class="modal-footer">
							<button id="btn_reply_modify" type="button"
								class="btn btn-sm btn-info">저장</button>
							<button type="button" class="btn btn-default btn-sm"
								data-dismiss="modal">닫기</button>
						</div>
					</form>
				</div>
			</div>
		</div>
		<!-- END : 댓글 수정용 Modal-->
	</div> <!-- END : 댓글 container-->

 

:: 더보기, 수정, 삭제, 등록만 해도 이정도다;;; ㄷㄷ

 

 

그리고 자바스크립트 부분 (ajax 이해하기)

 

<!-- START : 댓글 목록 영역-->
<div id="id_reply_list_area"></div>
<!-- END : 댓글 목록 영역-->

 

 

<script type="text/javascript">
		
		<!-- START : 댓글 처리 스크립트 -->
        <!--페이지마다 댓글 보여줘야한다 -->
		var curPage = 1;
		var rowSizePerPage = 10 ;
		var parentNo = ${board.boNo};
		var sendParam = {"curPage" :  curPage
							, "rowSizePerPage" : rowSizePerPage
							, "reCategory" : "FREE"
							,  "reParentNo": ${board.boNo} };
        <!--댓글목록영역 -->
		var $reply_list_area = $('#id_reply_list_area');

 

// 댓글목록을 구하는 함수
		function fn_reply_list(){
			$.ajax({
				url : "<c:url value = '/reply/replyList' />",
				// 요청 페이지 URL정보
				dataType : 'json', // 서버로부터 전달받을 데이터 유형 (html, xml, json, script)
				data : sendParam, // 서버에 전송할 파라미터 정보
				success : function(data) {
					console.log('data.result', data.result);
					console.log('data.count', data.count);
					console.log('data.data', data.data);
					
					$.each(data.data,function(i,el){
						console.log(i,el);
						var str = '<div class="row" >'; 
							str += '<div class="col-sm-2 text-right" >'+el.reMemName+'</div>'; 
							str += '<div class="col-sm-6"><pre>'+el.reContent+'</pre></div>';
						   str += '<div class="col-sm-2" >'+el.reRegDate+'</div>';
						   str += '<div class="col-sm-2">';
						   //로그인 사용자 + 댓글 등록자
						   if('${sessionScope.USER_INFO.userId}' == el.reMemId){
						   str  += '<button name="btn_reply_edit" type="button" 
                           		class=" btn btn-sm btn-info" data-re-no="'+el.reNo+'">수정</button>';
						   str +='<button name="btn_reply_delete" type="button" 
                           		class="btn btn-sm btn-danger" data-re-no="'+el.reNo+'">삭제</button>';
						   }
						   str +='</div>';
			    			str +='</div>';
			    			$('#id_reply_list_area').append(str); //append
					}); //.each
					if(data.count < sendParam.rowSizePerPage){
					$('#id_reply_list_more').hide();
					}
				}, // 요청에 성공한 경우 호출되는 함수 (data, status, xhr )
				error : function(xhr,status,error) {
					console.log('xhr',xhr);
					console.log('status',status);
					console.log('error',error);
				}
				// 요청에 실패한 경우 호출되는 함수 (xhr, status, error)
				}); // ajax
			
		} // fn_reply_list

데이터 유형은 json , 서버에 전송할 파라미터 정보는 sendParam

성공 할 시 function(data) 함수를 실행하고

 

$.each는 JQuery를 사용해 배열을 관리하고자 할 때 each() 메서드를 사용 할 수 있다.

매개 변수로 받은 것을 사용해 for in 반복문과 같이 배열이나 객체의 요소를 검사 할 수 있는 메서드.

 

 

$.each(data.data,function(i,el)

 

컨트롤러단을 보면 data는 list다. list와 익명 i,el

 

회원이름 + 글 내용 + 작성일

 

 

	if('${sessionScope.USER_INFO.userId}' == el.reMemId){
		str  += '<button name="btn_reply_edit" 
        	type="button" class=" btn btn-sm btn-info" data-re-no="'+el.reNo+'">수정</button>';
		str +='<button name="btn_reply_delete" 
        	type="button" class="btn btn-sm btn-danger" data-re-no="'+el.reNo+'">삭제</button>';
	}

if 문은 접속한 아이디와 일치시 수정,삭제가 나타나게 함. (자기 댓글만 수정,삭제 가능)

 

수정

$(document).ready(function() {
			
		// 수정버튼 클릭
		$('#id_reply_list_area').on('click','button[name=btn_reply_edit]',function(e){
			/* $('#id_reply_edit_modal').modal('show') */
			$btn = $(this);
			$('form[name=frm_reply_edit] ').find('input[name=reNo]').val($btn.data('re-no') );
			$('form[name=frm_reply_edit] ').find('textarea[name=reContent]').val($btn.closest('div.row').find('pre').text() );
			$('#id_reply_edit_modal').modal('show');
		}); // btn_reply_edit.click
		
	
		// 모달창의 (수정)저장버튼 btn_reply_modify 클릭
		$("#btn_reply_modify").click(function(e) {
			var params = $('form[name =frm_reply_edit]').serialize();
			 // #btn_reply_regist.click
			 
			$.ajax({
				type :"POST", // 전송 방식 설정 (Defaut : GET)
				url : "<c:url value = '/reply/replyModify' />",
				dataType : 'json', // 서버로부터 전달받을 데이터 유형 (html, xml, json, script)
				data : params, // 서버에 전송할 파라미터 정보
				success : function(data) {
					if(data.result) {
						// 성공
						reNo = $('form[name=frm_reply_edit]').find('input[name=reNo]').val();
						reContent = $('form[name=frm_reply_edit] ').find('textarea[name=reContent]').val();
						$('button[data-re-no=' + reNo + ']').closest('div.row').find('pre').text(reContent);
					}else{
						alert(data.msg);
					}
					
					$('#id_reply_edit_modal').modal('hide');
					
				}, // 요청에 성공한 경우 호출되는 함수 (data, status, xhr )
				error : function(xhr,status,error) {
					console.log('xhr',xhr);
					console.log('status',status);
					console.log('error',error);
				}
				// 요청에 실패한 경우 호출되는 함수 (xhr, status, error)
				}); // ajax
			
			}); //("#btn_reply_regist").click

url : "<c:url value = '/reply/replyModify' />",

 

<c:url : URL에 자동으로 Context Path를 붙여줍니다. 편리한게 Context를 변경을 하더라도 URL을 수정 할 필요 X

 

if(data.result) {
		// 성공
		reNo = $('form[name=frm_reply_edit]').find('input[name=reNo]').val();
        
        //reply_edit 중 input name이 reNo인 것을 찾아 값 넣기
		reContent = $('form[name=frm_reply_edit] ').find('textarea[name=reContent]').val();
        //reply_edit 중 input name이 reContent인 것을 찾아 값 넣기
        
		$('button[data-re-no=' + reNo + ']').closest('div.row').find('pre').text(reContent);
			}else{
				alert(data.msg);
			}

.closest('div.row') : 선택한 요소를 포함하면서 가장 가까운 상위 요소를 선택

 

삭제

 

// 삭제버튼 클릭
		$('#id_reply_list_area').on('click','button[name=btn_reply_delete]',function(e){
			$btn = $(this);
			var params = {"reNo" : $btn.data('re-no')};
			res = confirm("정말로 삭제하시겠습니까?");
			if(res) {
				 
				 $.ajax({
					 type : "POST",
					 url : "<c:url value = '/reply/replyDelete' />",
					 dataType : "json",
					 data : params,
					 success : function(data) {
						 if(data.result){
							 reContent = $('form[name=frm_reply_edit] ').find('textarea[name=reContent]').val();
							 $('button[data-re-no=' + del + ']').closest('div.row').find('pre').text(reContent);
						 }
						 $("")
				 }
				 });
			}
			
		}); // btn_reply_delete.click

 

더보기

 

// 더보기 버튼 클릭
		$('#btn_reply_list_more').click(function(e) {
		sendParam.curPage =sendParam.curPage + 1;
		fn_reply_list();
		}); // #btn_reply_list_more.click

 

등록

 

// 등록버튼 클릭(아작스)
		$("#btn_reply_regist").click(function(e) {
			var params = $('form[name =frm_reply]').serialize();
		 // #btn_reply_regist.click
		$.ajax({
			type :"POST", // 전송 방식 설정 (Defaut : GET)
			url : "<c:url value = '/reply/replyRegist' />",
			// 요청 페이지 URL정보
			dataType : 'json', // 서버로부터 전달받을 데이터 유형 (html, xml, json, script)
			data : params, // 서버에 전송할 파라미터 정보
			success : function(data) {
				alert(data);
			}, // 요청에 성공한 경우 호출되는 함수 (data, status, xhr )
			error : function() {
				
			}
			// 요청에 실패한 경우 호출되는 함수 (xhr, status, error)
			}); // ajax
		
		}); //("#btn_reply_regist").click
		
		// 4. 초기화 함수 호출
		fn_reply_list();
		}); // ready
		
		</script>

'Back-End > Spring' 카테고리의 다른 글

[Spring] AOP  (0) 2021.09.03
[Spring] 댓글 구현 2  (0) 2021.08.29
[Spring] 검색 화면 처리  (0) 2021.08.11
[Spring] 검색 처리  (0) 2021.08.11
[Spring] 페이징 처리 및 번호 출력  (0) 2021.08.06
  Comments,     Trackbacks
[Spring] 검색 화면 처리

 

검색창을 게시판에 보여지게 하고 그 검색을 계속 끌고 가야한다.(페이지넘길때도 검색을 끌고 가기)

 

 

 

list.jsp

 <!-- 검색  -->
   <form id='searchForm' action="/board/list" method="get">
   <select name = "type">
     <option value=""${pageMaker.cri.type == null?"selected":""}>---</option>
     <option value="T"${pageMaker.cri.type eq 'T'?"selected":""}>제목</option>
     <option value="C"${pageMaker.cri.type eq 'C'?"selected":""}>내용</option>
     <option value="W"${pageMaker.cri.type eq 'W'?"selected":""}>작성자</option>
     <option value="TC"${pageMaker.cri.type == 'TC'?"selected":""}>제목+내용</option>
     <option value="TCW"${pageMaker.cri.type == 'TCW'?"selected":""}>제목+내용+작성자</option>
   </select>
     <input type = 'text' name = 'keyword' value='${pageMaker.cri.keyword }'>
     <input type = 'hidden' name = 'pageNum' value='${pageMaker.cri.pageNum }'>
     <input type = 'hidden' name = 'amount' value='${pageMaker.cri.amount }'>
     <button class='btn btn-default'>Search</button>
   </form>

 

BoardMapper.xml

<sql id="criteria">
  <trim prefix="(" suffix=") AND">
	<foreach collection="typeArr" item="type" separator="OR">
		<if test="type == 'T'.toString()">
			title like '%'||#{keyword}||'%'
		</if>
		
		<if test="type == 'C'.toString()">
			content like '%'||#{keyword}||'%'
		</if>
		
		<if test="type == 'W'.toString()">
			writer like '%'||#{keyword}||'%'
		</if>
	</foreach>
	</trim>
  </sql>

option - selected 속성은 페이지가 로드될 때 옵션 중에서 미리 선택되어지는 옵션을 말함

 

pageNum을 hidden으로 안보이게 하고 자바스크립트 함수를 만들어 페이지번호를 찾게 한다.

 

 

#searchForm 자바스크립트

var searchForm = $("#searchForm");
  	
  	$("#searchForm button").on("click",function(e){
  		e.preventDefault();
  		console.log(".....................");
  		
  		searchForm.find("input[name='pageNum']").val(1);
  		
  		searchForm.submit();
  	});

 

근데 여기서 페이지를 넘길때도 검색 값을 들고 가야한다.

 

그럴려면 actionForm에 type hidden을 줘서 keyword와 type을 써서 유지되게 한다.

 

<form id='actionForm' action="/board/list" method='get'>
  <input type='hidden' name='pageNum' value = '${pageMaker.cri.pageNum}'>
  <input type='hidden' name='amount' value = '${pageMaker.cri.amount}'>
  <input type='hidden' name='type' value = '${pageMaker.cri.type}'>
  <input type='hidden' name='keyword' value = '${pageMaker.cri.keyword}'>
</form>

 

 

get.jsp에도 똑같이 hidden type,keyword를 추가해야한다.

 

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] 댓글 구현 2  (0) 2021.08.29
[Spring] 댓글 구현  (0) 2021.08.29
[Spring] 검색 처리  (0) 2021.08.11
[Spring] 페이징 처리 및 번호 출력  (0) 2021.08.06
[Spring] 인덱스,힌트와 ROWNM  (0) 2021.08.05
  Comments,     Trackbacks
[Spring] 검색 처리

 

검색 할때는 특정 키워드를 입력 시 게시판에 있는 그  키워드에 맞게 나와야 한다.

 

그전에 먼저 알아야 할 동적 태그들이 있다 (MyBatis)

 

 

if

<if test="key == 'T'.toString()">
			title like #{val}
		</if>

test에 조건을 쓰고 그 조건이 true일 때.. 출력 (key가 T면 title lie #{val})

 

trim

<trim prefix="(" suffix=") AND">
	<foreach collection="typeArr" item="type" separator="OR">
		<if test="type == 'T'.toString()">
			title like '%'||#{keyword}||'%'
		</if>

trim은 단독으로 절대 사용안한다. if나 forEach,choose랑 같이 쓰인다.

 

prefix :: 쿼리 가장 앞에 붙여준다.

 

그러면 suffix는? -> 가장 뒤에 붙여준다.

 

trim 밑에 foreach는 리스트나 맵 배열 등 루프를 돌 때 사용

collection :: 전달받은 인자, 리스트나 배열 형태만 가능

item :: 배열은 키가 필요하지 않고 item에 별칭 즉 루프돌때 쓸 변수명 쓰기

separator :: 반복 되는 사이에 출력할 문자열 ( 루프돌 때마다 OR 붙이기)

 


 

<sql id="criteria">
  <trim prefix="(" suffix=") AND">
	<foreach collection="typeArr" item="type" separator="OR">
		<if test="type == 'T'.toString()">
			title like '%'||#{keyword}||'%'
		</if>
		
		<if test="type == 'C'.toString()">
			content like '%'||#{keyword}||'%'
		</if>
		
		<if test="type == 'W'.toString()">
			writer like '%'||#{keyword}||'%'
		</if>
	</foreach>
	</trim>
  </sql>

그렇게 해서 BoardMapper.xml에 SQL 검색 구문은 다음과 같은데

 

여기서 제일 위에 있는 sql태그 좀 생소한데

 

조각이라고 하는거 같다.

 

sql 태그 안에 검색 구문을 넣고 

 

기존 검색 구문 자리에

 

<include refid="criteria"></include>

써주면 조각이 불러온다.

이러면 코드가 간결해지고 중복되는 쿼리는 좀 더 편해진다.

 

 


 

 

태그 정리를 하다보니 먼저 해야하는데 깜빡했다.

 

Criteria.java

 

	private String type; 

	private String keyword;

여기서 중요한게 String타입이다 하지만 forEach로 돌릴려면 배열이나 리스트,맵,셋 으로 해야 하는데...

어떻게 해야할까?

 

public String[] getTypeArr() {
		return type == null? new String[] {}: type.split("");

String[] 타입에 getTypeArr() 추가하면 해결 된다. (검색조건은 한글자)

 

split :: 문자열 자르기

 

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] 댓글 구현  (0) 2021.08.29
[Spring] 검색 화면 처리  (0) 2021.08.11
[Spring] 페이징 처리 및 번호 출력  (0) 2021.08.06
[Spring] 인덱스,힌트와 ROWNM  (0) 2021.08.05
[Spring] 조회 처리 및 페이지 이동  (0) 2021.08.04
  Comments,     Trackbacks
[Spring] 페이징 처리 및 번호 출력

 

저번에도 올렸던 페이징 처리

 

아무래도 빠르게 보다보니 많이 까먹어서 다시 영상을 보면서 글을 올려본다.

 

별거 아닌 거 같지만 페이징 만큼은 난 많이 헷갈린다. 

 

처음에는 StartPage와 EndPage 그냥 보면 식만 외우면 될꺼 같은데 뭔가 어렵다.. ㅠ

 

먼저 매퍼쪽부터 보자

 

<select id ="GetListWithPaging" resultType = "org.zerock.domain.BoardVO">
  	<![CDATA[
	  SELECT * FROM
	(
	SELECT /*+ INDEX_DESC(tbl_board pk_board)*/ rownum rn, bno,title,writer,regDate,updateDate
	FROM tbl_board 
	WHERE bno > 0 and rownum > 0 and rownum <= (#{pageNum} * #{amount})
	)
	WHERE rn > (#{pageNum}-1) * #{amount}
	]]>
  </select>

BoardMapper.xml에 추가한 페이징 쿼리

 

참... 복잡해보인다 인라인뷰부터 시작해서 아무래도 이해를 확실히 해야할꺼같다.

 

::  resultType은 select절에는 항상 들어간다. 결과 데이터를 어떻게 처리해줘야하는지 명시해 줘야하는데 그때 쓰는게 resultType이다.

 

FROM부터 조회가 되기때문에 인라인뷰를 타는데 여기서 인라인뷰 조건절 끝에 #{pageNum} * #{amount}은

 

Criteria.java에 있는 필드인데 pageNum은 현재페이지, amount는 한페이지당 몇개씩 보자 정해주는 것이다.

 

그리고 <![CDATA[ 쿼리 ]]> 꺽새가 있는 쿼리에 쓰는데, 이게 >,<,&가 있을 경우 컴퓨터는 태그로 인식 하기 때문에

 

CDATA를 쓰면 "야! 이거 쿼리에 쓰는 거야 오해하지마" 라고 해주는 것!

 


public class Criteria {
	//현재 페이지 번호
	private int pageNum;
	private int amount;
	//한페이지당 10개씩 즉, 1~10개의 페이지 번호
	public Criteria() {
		this(1,10);
	}

	public Criteria(int pageNum, int amount) {
		super();
		this.pageNum = pageNum;
		this.amount = amount;
	}
	
}

 

Criteria.java

 

클래스이름 진짜 적응안되서 바꾸고 싶지만, 혹시나 헷갈리까바 냅뒀다.

 

Criteria는 페이징처리와 검색을 동시에 할 수 있는 클래스

 

 

PageDTO.java

 

public class PageDTO {
	
	private int startPage ,endPage;
	private boolean prev, next;
	private int total;
	private Criteria cri;
	
	public PageDTO(Criteria cri, int total) {
		this.cri = cri;
		this.total = total;
	
		this.endPage = (int)(Math.ceil(cri.getPageNum()/ 10.0)) * 10;
		
		this.startPage = endPage - 9;
		
		this.prev = this.startPage > 1;
		// 만약 총 페이지가 71건이면 71.0 / 10 = realEnd는 7.1이 됨 ceil 사용으로 올린다.
		
		int realEnd = (int) ( Math.ceil( (total * 1.0) / cri.getAmount()) ) ;
		
		this.endPage = realEnd <= endPage? realEnd : endPage;
		
		this.next = this.endPage < realEnd;
	}
}

실질적으로 페이징을 담당하는 요녀석 Criteria에 현재페이지, 한페이지에 보여줄 페이지를 가져와

밑에 게시판 1~10 페이징버튼을 만드는데 필요하다.

 

endPage가 은근 헷갈려서 고생했는데, 이해하고 나니 쉬운거였다.

 

현재페이지가 예로 4페이지라고 한다면 4/10.0 * 10 하면 그냥 똑같은 숫자가 나온다. 이러면 의미가 없고

 

0.4를 강제로 올림 해주는 Math.ceil를 쓴다. ceil은 반환형이 double형이기 때문에 캐스팅 int를 꼭 해줘야한다.

 

realEnd(마지막페이지 계산)

 

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] 검색 화면 처리  (0) 2021.08.11
[Spring] 검색 처리  (0) 2021.08.11
[Spring] 인덱스,힌트와 ROWNM  (0) 2021.08.05
[Spring] 조회 처리 및 페이지 이동  (0) 2021.08.04
[Spring] 한글 필터 설정 및 모달창  (0) 2021.08.03
  Comments,     Trackbacks
[Spring] 인덱스,힌트와 ROWNM

 

 

인덱스하면 난 책에 있는 목차가 떠오른다. 찾고자 하는 단어, 문장, 페이지를 빨리 찾고 하는 목차(인덱스)

 

DB에서의 인덱스도 같은 기능을 한다. 

 

먼저 DB에 있는 ORDER BY부터 보기

 

 

SELECT * FROM tbl_board ORDER BY bno asc;

SELECT * FROM tbl_board ORDER BY bno DESC;

ORDER BY (컬럼) asc/desc; :: 여기서 ORDER BY 기본값이 오름차순(asc)이기 때문에 asc 쓸땐 생략이 가능하다.

 

 

옵션에 있는 FULL은 전체를 검색한 것을 의미, 카디널리티(행의 수는 34개, 비용은 3 :: 비용은 적을 수록 좋다.)

(잘 이해가 되질 않을때는 계획 설명을 눌러 확인하면 과정을 알 수 있어 좋다)

 

 

ORDER BY는 오름차순 내림차순으로 정렬을 해준다. 하지만 지금은 데이터량이 엄청 작기 때문에 시간이 0.003초 나온다. 즉, 1만건 이하 정도는 속도가 어느정도 나오겠지만, 그 이상이 되면 시간이 많이 걸린다. 

 

사이트를 눌렀을때 하얀페이지에 페이지 불러오는게 2초이상 걸리기만 해도 분노의 F5누르는데 누가 기다릴까? ㅋㅋㅋ

 

그러니 DB에서 데이터를 불러오는 속도는 중요하다.

 

여기서 쓰는게 인덱스다.

 

 

SELECT /*+ INDEX_DESC(tbl_board pk_board)*/ rownum, bno,title 
FROM tbl_board 
WHERE bno > 0 
ORDER BY bno DESC;


SELECT /*+ FULL(tbl_board pk_board)*/ rownum, bno,title 
FROM tbl_board 
WHERE bno > 0 
ORDER BY bno DESC;

SELECT 앞에 이상하게 써져있는게 힌트라는 것인데

 

힌트는 컴퓨터에게 "난 너가 이 인덱스를 타고 했으면 좋겠어!!"라고 생각하면 된다

 

ORDER BY를 쓰지 않아도 인덱스_DESC을 썼기 떄문에 오름차순 정렬이 된다.

 

인덱스를 여러개 쓸 수 있지만, 대부분 한개만? 쓴다고 한다.

 

주의 할 점은 /* 하고 꼭 +를 붙여줘야한다. 안그러면 주석으로 처리된다. 

또한 /*+ 말고도 --+(싱글주석)도 가능하다.

 

INDEX_() DESC나 ASC를 쓸 수 있다. (오름,내림정렬)

 

FULL과 INDEX만 알아도 충분 (두개밖에 쓰질 않나보다)

 

FULL : 전체

 

INDEX 그 특정 부분 정렬


ROWNUM은 의사 컬럼만 참조, DB에 저장되지는 않는다.

 

거의 페이징을 위해 태어난 존재랄까?

 

 

여기서 보면 BNO는 오름차순이고, ROWNUM은 1이다. 즉, ROWNUM은 순위를 매기는 것 즉, 인덱스로 가장 먼저 온 애가 1등이 되버린다.

 

하지만 페이징을 할 경우에 이런 조건을 쓸 수 있는데,

 

WHERE bno > 0 and rownum <= 10; 

0 ~ 10 행을 보여준다. 근데 여기서 2페이지를 보여줄때 이렇게 써버리면 데이터가 나오지 않는다.

 

10~20 까지 (2페이지)

 

이렇게 생각 할 수 있는데, 가장 먼저 온 1등이 되기 때문에 오면 바로 1등이 되고 WHERE절에 적용이 안되 필터에 걸러진다.

 

 

그래서 페이징 할 때는 이렇게 쿼리를 작성한다.

(외우거나 계속 써보자!!! 페이징 할땐 필수니깐)

 

SELECT * FROM
(
SELECT /*+ INDEX_DESC(tbl_board pk_board)*/ rownum rn, bno,title,writer 
FROM tbl_board 
WHERE bno > 0 and rownum > 0 and rownum <=20
)
WHERE rn > 10
;

 

여기서 FROM절에 ( )안에 있는게 인라인뷰. 저 ( )에 있는 쿼리가 조건에 맞는 것을 실행 하고 그다음 rn > 10이 실행이 된다. ( rownum은 키워드라서 가짜로 컬럼을 만든다. rn)

 

쿼리 실행 순서

FROM - WHERE - GROUP BY - HAVING -SELECT - ORDER BY

-> -> ->

 

 

2Page

 

  Comments,     Trackbacks
[Spring] 조회 처리 및 페이지 이동

 

조회 = Get.jsp

파일이름처럼 Get.jsp(조회)는 무조건 Get방식으로!

 

 

조회는 게시판 리시트에 제목을 클릭 시 제목 내용 글쓴이를 확인 할 수 있는 화면

 

간단한 화면 list.jsp 내용을 가져오기만 하면 된다.

 

<div class="form-group">
       <label>BNO</label>
       <input class="form-control" name="bno" readonly="readonly" value='<c:out value="${board.bno}"/>'>
       </div>                            
                                 
       <div class="form-group">
         <label>Title</label>
         <input class="form-control" name="title" readonly="readonly" value='<c:out value="${board.title}"/>'>
       </div>
     
       <div class="form-group">
         <label>Content</label>
         <textarea class="form-control" rows="5" cols="50" name="content"><c:out value="${board.content}"/></textarea>
       </div>
     
     
       <div class="form-group">
         <label>Writer</label>
         <input class="form-control" name="writer" value='<c:out value="${board.writer}"/>'>
       </div>

	 <button type="submit" class="btn btn-default"><a href='/board/list'>List</a></button>
     <button type="reset" class="btn btn-default"><a href='/board/modify?bno=<c:out value="${board.bno}"/>'>Modify</a></button>

input 속성 중 readonly는 읽기만 가능 하고 값 변경은 불가능 하다. 

 

 

마지막 버튼 부분

 <button type="reset" class="btn btn-default">

<a href='/board/modify?bno=<c:out value="${board.bno}"/>'>Modify</a></button>

 

modify에서 클릭한 제목의 bno를 수정 하는 a 태그 처음에는 이해가 안됬는데 써보니 이해가 됬다.

그리고 작은 따옴표 주의!

 

 

 

BoardController에서는 

 

@GetMapping({"/get", "/modify"})
	public void get(@RequestParam("bno") Long bno, Model model) {
		
		model.addAttribute("board", service.get(bno));
	}

 

get은 게시물 번호만 조회를 하기 때문에 bno를 받아야 하기때문에 저렇게 적는다.

 

board라는 키에 bno 값 가져오기!

 

근데 여기서 @RequestParam를 쓰는 이유는 bno는 int 타입인데 int는 파라미터 수집이 안된다. 모델 어트리뷰트를 써야 수집이 된다.

 

@RequestParam :: 하나 이상의 타입을 적용 할 수 있다.

 

 

  Comments,     Trackbacks