쌍용교육(JAVA)/SpringBoot

쌍용교육 -JSP수업 92일차 ch15SpringPage(3)

구 승 2024. 6. 27. 12:48

MemberController.java 내용추가

/*===============================
	 * 회원정보 수정
	 ===============================*/
	//수정 폼 호출
	@GetMapping("/member/update")
	public String formUpdate(HttpSession session, Model model) {
		MemberVO user = (MemberVO)session.getAttribute("user");
		MemberVO memberVO = memberService.selectMember(user.getMem_num());
		model.addAttribute("memberVO",memberVO);
		return "memberModify";
	}

view/member =>memberModify.jsp

<%@ 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="form" uri="http://www.springframework.org/tags/form" %>
<!-- 회원정보 수정 시작 -->
<div class="page-main">
	<h2>회원정보 수정</h2>
	<form:form action="update" id="member_modify"
	                            modelAttribute="memberVO">
		<ul>
			<li>
				<form:label path="name">이름</form:label>
				<form:input path="name"/>
				<form:errors path="name" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="nick_name">별명</form:label>
				<form:input path="nick_name"/>
			</li>
			<li>
				<form:label path="phone">전화번호</form:label>
				<form:input path="phone"/>
				<form:errors path="phone" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="email">이메일</form:label>
				<form:input path="email"/>
				<form:errors path="email" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="zipcode">우편번호</form:label>
				<form:input path="zipcode"/>
				<input type="button" onclick="execDaumPostcode()"
				               value="우편번호 찾기" class="default-btn">
				<form:errors path="zipcode" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="address1">주소</form:label>
				<form:input path="address1"/>
				<form:errors path="address1" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="address2">상세주소</form:label>
				<form:input path="address2"/>
				<form:errors path="address2" cssClass="error-color"/>
			</li>
		</ul> 
		<div class="align-center">
			<form:button class="default-btn">전송</form:button>
			<input type="button" value="마이페이지"
			  class="default-btn"
			  onclick="location.href='myPage'"><!-- tile-def/member.xml에 있는 mypage를 가져옴 -->
		</div>                           
	</form:form>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-3.7.1.min.js"></script>
	<!-- 우편번호 시작 -->
	<!-- iOS에서는 position:fixed 버그가 있음, 적용하는 사이트에 맞게 position:absolute 등을 이용하여 top,left값 조정 필요 -->
<div id="layer" style="display:none;position:fixed;overflow:hidden;z-index:1;-webkit-overflow-scrolling:touch;">
<img src="//t1.daumcdn.net/postcode/resource/images/close.png" id="btnCloseLayer" style="cursor:pointer;position:absolute;right:-3px;top:-3px;z-index:1" onclick="closeDaumPostcode()" alt="닫기 버튼">
</div>

<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
    // 우편번호 찾기 화면을 넣을 element
    var element_layer = document.getElementById('layer');

    function closeDaumPostcode() {
        // iframe을 넣은 element를 안보이게 한다.
        element_layer.style.display = 'none';
    }

    function execDaumPostcode() {
        new daum.Postcode({
            oncomplete: function(data) {
                // 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.

                // 각 주소의 노출 규칙에 따라 주소를 조합한다.
                // 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
                var addr = ''; // 주소 변수
                var extraAddr = ''; // 참고항목 변수

                //사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
                if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
                    addr = data.roadAddress;
                } else { // 사용자가 지번 주소를 선택했을 경우(J)
                    addr = data.jibunAddress;
                }

                // 사용자가 선택한 주소가 도로명 타입일때 참고항목을 조합한다.
                if(data.userSelectedType === 'R'){
                    // 법정동명이 있을 경우 추가한다. (법정리는 제외)
                    // 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
                    if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
                        extraAddr += data.bname;
                    }
                    // 건물명이 있고, 공동주택일 경우 추가한다.
                    if(data.buildingName !== '' && data.apartment === 'Y'){
                        extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
                    }
                    // 표시할 참고항목이 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
                    if(extraAddr !== ''){
                        extraAddr = ' (' + extraAddr + ')';
                    }
                    //(주의)address1에 참고항목이 보여지도록 수정
                    // 조합된 참고항목을 해당 필드에 넣는다.
                    //(수정) document.getElementById("address2").value = extraAddr;
                
                } 
                //(수정) else {
                //(수정)    document.getElementById("address2").value = '';
                //(수정) }

                // 우편번호와 주소 정보를 해당 필드에 넣는다.
                document.getElementById('zipcode').value = data.zonecode;
                //(수정) + extraAddr를 추가해서 address1에 참고항목이 보여지도록 수정
                document.getElementById("address1").value = addr + extraAddr;
                // 커서를 상세주소 필드로 이동한다.
                document.getElementById("address2").focus();

                // iframe을 넣은 element를 안보이게 한다.
                // (autoClose:false 기능을 이용한다면, 아래 코드를 제거해야 화면에서 사라지지 않는다.)
                element_layer.style.display = 'none';
            },
            width : '100%',
            height : '100%',
            maxSuggestItems : 5
        }).embed(element_layer);

        // iframe을 넣은 element를 보이게 한다.
        element_layer.style.display = 'block';

        // iframe을 넣은 element의 위치를 화면의 가운데로 이동시킨다.
        initLayerPosition();
    }

    // 브라우저의 크기 변경에 따라 레이어를 가운데로 이동시키고자 하실때에는
    // resize이벤트나, orientationchange이벤트를 이용하여 값이 변경될때마다 아래 함수를 실행 시켜 주시거나,
    // 직접 element_layer의 top,left값을 수정해 주시면 됩니다.
    function initLayerPosition(){
        var width = 300; //우편번호서비스가 들어갈 element의 width
        var height = 400; //우편번호서비스가 들어갈 element의 height
        var borderWidth = 5; //샘플에서 사용하는 border의 두께

        // 위에서 선언한 값들을 실제 element에 넣는다.
        element_layer.style.width = width + 'px';
        element_layer.style.height = height + 'px';
        element_layer.style.border = borderWidth + 'px solid';
        // 실행되는 순간의 화면 너비와 높이 값을 가져와서 중앙에 뜰 수 있도록 위치를 계산한다.
        element_layer.style.left = (((window.innerWidth || document.documentElement.clientWidth) - width)/2 - borderWidth) + 'px';
        element_layer.style.top = (((window.innerHeight || document.documentElement.clientHeight) - height)/2 - borderWidth) + 'px';
    }
</script>
	<!-- 우편번호 끝 -->
</div>
<!-- 회원정보 수정 끝 -->

WEB-INF/tile-def => member.xml 내용추가

<definition name="memberModify" extends="myPage">
		<put-attribute name="title" value="회원정보수정"/>
		<put-attribute name="body" value="/WEB-INF/views/member/memberModify.jsp"/>
	</definition>

수정폼이 나온다. 아직 수정 기능은 구현안함.

MemberController.java 내용추가

	//수정 폼에서 전송된 데이터 처리
	@PostMapping("/member/update")
	public String submitUpdate(@Valid MemberVO memberVO, BindingResult result, HttpSession session) {
		
		log.debug("<<회원정보 수정>> :"+ memberVO);
		
		//유효성 체크 결과 오류가 있으면 폼 호출
		if(result.hasErrors()) {
			return "memberModify";
		}
		
		MemberVO user = (MemberVO)session.getAttribute("user");
		memberVO.setMem_num(user.getMem_num());
		
		//회원정보 수정
		memberService.updateMember(memberVO);
		
		//세션에 저장된 정보 변경(바꾸지않으면 기존정보로 바꾸면 수정되도록)
		user.setNick_name(memberVO.getNick_name());
		user.setEmail(memberVO.getEmail());
		return "redirect:/member/myPage";
	}

수정 및 유효성 처리가 잘 되는 것을 볼 수 있음

 

 

 

webapp => image_bundle 폴더 생성 (기본 이미지 처리를 위해)

기본 이미지 처리를 하려면 webapp에 이미지를 넣어야된다. static/images에 넣으면 결과로 나오지않는다.

이 이미지를 생성한 폴더에 넣는다.

kr.spirng.util => FileUtil

package kr.spring.util;

import java.io.FileInputStream;
import java.io.IOException;

import org.springframework.core.style.ToStringCreator;

import lombok.extern.slf4j.Slf4j;

@Slf4j //로그 처리를위해
public class FileUtil {
	//지정한 경로의 파일을 읽어들여 byte 배열로 변환
	public static byte[] getbytes(String path) {
		FileInputStream fis = null;
		byte[] readbyte = null;
		try {
			fis = new FileInputStream(path);
			readbyte = new byte[fis.available()];
			fis.read(readbyte);
		}catch(Exception e) {
			log.error(e.toString());
		}finally {
			if(fis!=null)try {fis.close();}catch(IOException e) {}
		}
		
		return readbyte;
	}
}

MemberController.java 수정

/*===============================
	 * 프로필 사진 출력
	 ===============================*/
	//프로필 사진출력(로그인 전용)
	@GetMapping("/member/photoView")
	public String getProfile(HttpSession session,HttpServletRequest request, Model model ) {
		MemberVO user = (MemberVO)session.getAttribute("user");
		log.debug("<<프로필 사진 출력>>"+user);
		if(user==null) {//로그인이 되지 않은 경우 (정확히는 로그인이 풀린경우)
			getBasicProfileImage(request, model);
		}else {//로그인이 된 경우(로그인이 되어있는 상태)
			MemberVO memberVO = memberService.selectMember(user.getMem_num());
			
			viewProfile(memberVO, request, model);
		}
		
		return "imageView";
	}
	
	
	//프로필 사진 처리를 위한 공통 코드
	public void viewProfile(MemberVO memberVO, HttpServletRequest request, Model model) {
		if(memberVO==null || memberVO.getPhoto_name()==null) {
			//DB에 저장된 프로필 이미지가 없기 때문에 기본 이미지 호출
			getBasicProfileImage(request, model); //바로 아래에 있는 메서드를 호출
		}else {
			//업로드한 프로필 이미지 읽기
			model.addAttribute("imageFile",memberVO.getPhoto());
			model.addAttribute("filename",memberVO.getPhoto_name());
		}
		
	}
	
	//기본 이미지 읽기
		public void getBasicProfileImage(HttpServletRequest request, Model model) {
			byte[] readbyte = FileUtil.getbytes(request.getServletContext().getRealPath(
					   "/image_bundle/face.png"));
			model.addAttribute("imageFile",readbyte);
			model.addAttribute("filename","face.png");
		}

kr.spring.view => ImageView

package kr.spring.view;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;

@Component
public class ImageView extends AbstractView{

	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		
		byte[] file = (byte[])model.get("imageFile");
		String filename= (String)model.get("filename");
		
		String ext = filename.substring(filename.lastIndexOf(".")); //face.png: 확장자 앞에 .이 있기 때문에 .으로 구분하여 확장자 추출
		if(ext.equalsIgnoreCase(".gif")) { //확장자가 .GIF인 경우도 있어서 대소문자 구문 없이
			ext="image/gif";
		}else if(ext.equalsIgnoreCase(".png")){
			ext="image/png";
		}else {
			ext = "image/jpeg";
		}
		
		response.setContentType(ext);
		response.setContentLength(file.length);
		
		String file_name = new String(filename.getBytes("utf-8"),"iso-8859-1");
		
		response.setHeader("Content-Disposition","attachment; filename=\""+file_name+"\"");
		response.setHeader("Content-Transfer-Encoding","binary");
		
		OutputStream out = response.getOutputStream();
		InputStream input = null;
		try {
			input = new ByteArrayInputStream(file);
			IOUtils.copy(input, out);
			out.flush();
		}finally {
			if(out!=null)out.close();
			if(input!=null)input.close();
		}
	}

}

nav_mypage.jsp 오타수정

onclick부분에 location을 명시하지않았어서 명시해줌

<ul>
		<li><input type="button" class="menu-btn" value="비밀번호변경"
			onclick="location.href='changePassword'">
		</li>
		<li><input type="button" class="menu-btn" value="회원탈퇴"
			onclick="location.href='delete'">
		</li>
	</ul>

다른 이름으로 저장하기를 클릭한다.
브라우저에서 자동으로 face라는 이름을 인식하고 저장해뒀기 때문에 face라고 자동으로 나온다.

static/js=>member.profile.js

$(function(){
   /*===================
	* MY페이지 프로필 사진 등록 및 수정
	*===================*/
	//수정 버튼 이벤트 처리
	$('#photo_btn').click(function(){
		$('#photo_choice').show();
		$(this).hide();
	});
});
  • 버튼을 클릭하면 사진을 보여주고 버튼을 가린다

nav_mypage.jsp 내용추가

<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/member.profile.js"></script>
<!-- MyPage 메뉴 끝 -->

카메라버튼 클릭시 카메라 사진은 사라지고

 

static/js=>member.profile.js 추가

//처음 화면에 보여지는 이미지 읽기
	let photo_path = $('.my-photo').attr('src');
	let my_photo; //업로드 하고자 선택한 이미지 저장
	//파일 선택 이벤트 연결
	$('#upload').change(function(){
		my_photo = this.files[0]; //선택한 이미지 저장
		if(!my_photo){
			$('.my-photo').attr('src',photo_path);
			return;
		}
		
		if(my_photo.size > 1024*1024){
			alert(Math.round(my_photo.size/1024)+'kbytes(1024kbytes까지만 업로드 가능)');
			$('.my-photo').attr('src',photo_path);
			$(this).val('');
			return;
		}
		
		//이미지 미리보기 처리
		const reader = new FileReader();
		reader.readAsDataURL(my_photo);
		
		reader.onload=function(){
			$('.my-photo').attr('src',reader.result);
		};
	});//end of change

파일 업로드 상태에서 다른 파일로 교체하려다가 취소를 누르면 기본 이미지로도 바뀐다.

//파일 업로드 처리
	$('#photo_submit').click(function(){
		if($('#upload').val()==''){
			alert('파일을 선택하세요!');
			$('#upload').focus();
			return;
		}
		//서버에 전송할 파일 선택
		const form_data = new FormData();
		form_data.append('upload',my_photo);
		
		//서버와 통신
		$.ajax({
			url:'../member/updateMyPhoto',
			type:'post',
			data:form_data,
			dataType:'json',
			contentType:false,
			processData:false,
			success:function(param){
				if(param.result == 'logout'){
					alert('로그인 후 사용하세요');
				}else if(param.result =='success'){
					alert('프로필 사진이 수정되었습니다.');
					//교체된 이미지 저장
					photo_path = $('.my-photo').attr('src');
					$('#upload').val('');
					$('#photo_choice').hide();
					$('#photo_btn').show();
				}else{
					alert('파일 전송 오류 발생');
				}
			},
			error:function(){
				alert('네트워크 오류발생')
			}
		});
		
	});//end of click - 파일 전송
	//취소버튼 처리
	$('#photo_reset').click(function(){
		$('.my-photo').attr('src',photo_path);
		$('#upload').val('');
		$('#photo_choice').hide();
		$('#photo_btn').show();
	});//end of click - 취소 버튼 처리

MemberMapper.java와 MemberService.java에 명시

//자동 로그인 처리
	public void updateAu_id(String au_id,Long mem_num);
	public void selectAu_id(String au_id);
	public void deleteAu_id(Long mem_num);
	
	//비밀번호 찾기
	public void updateRandomPassword(MemberVO member);
	
	//프로필 이미지 업데이트
	public void updateProfile(MemberVO member);

MemberServiceImpl 역시 add method 추가

MemberMapper.java SQL문 명시

//프로필 이미지 업데이트
	@Update("UPDATE spmember_detail SET photo=#{photo},photo_name=#{photo_name} WHERE mem_num =#{mem_num}")	
	public void updateProfile(MemberVO member);

MemberServiceImpl 

@Override
	public void updateProfile(MemberVO member) {
		memberMapper.updateProfile(member);
		
	}

MemberAjaxController.java 내용추가

//프로필 사진 업로드 작업
	@PostMapping("/member/updateMyPhoto")
	@ResponseBody
	public Map<String,String> processProfile(MemberVO memberVO,HttpSession session){
		
		Map<String,String> mapAjax = new HashMap<String,String>();
		MemberVO user = (MemberVO)session.getAttribute("user");
		if(user==null) {
			mapAjax.put("result","logout");
		}else {
			memberVO.setMem_num(user.getMem_num());
			memberService.updateProfile(memberVO);
			
			mapAjax.put("result","success");
		}
		
		return mapAjax;
	}

 

header.jsp

마이페이지 버튼 옆에 프로필사진 보이게하려고함.

링크는 controller에서 지정해둔 /member/photoView를 지정

<img src="${pageContext.request.contextPath}/member/photoView" width="25" height="25" class="my-photo">

MemberMapper.java 내용추가

	@Update("UPDATE spmember_detail SET passwd=#{passwd} WHERE mem_num=#{mem_num}")
	public void updatePassword(MemberVO member);

MemberServiceImpl.java 내용추가

@Override
	public void updatePassword(MemberVO member) {
		memberMapper.updatePassword(member);
		
	}

MemberController.java 내용추가

/*===============================
		 * 비밀번호 변경
		 ===============================*/
		//비밀번호 변경 폼 호출
		@GetMapping("/member/changePassword")
		public String formChangePasswd() {
			
			return "memberChangePassword";
		}

tile-def=>member.xml 내용추가

<definition name="memberChangePassword" extends="myPage">
		<put-attribute name="title" value="비밀번호 변경"/>
		<put-attribute name="body" value="/WEB-INF/views/member/memberChangePassword.jsp"/>
	</definition>

 

member => memberChangePassword.jsp

<%@ 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="form" uri="http://www.springframework.org/tags/form" %>
<!-- 비밀번호 변경 시작 -->
<div class="page-main">
    <h2>비밀번호 변경</h2>
    <form:form action="changePassword" id="member_change" modelAttribute="memberVO">
        <form:errors element="div" cssClass="error-color"/>
        <ul>
            <li>
                <form:label path="now_passwd">현재 비밀번호</form:label>
                <form:password path="now_passwd"/>
                <form:errors path="now_passwd" cssClass="error-color"/>
            </li>
            <li>
                <form:label path="passwd">비밀번호</form:label>
                <form:password path="passwd"/>
                <form:errors path="passwd" cssClass="error-color"/>
            </li>
            <li>
                <!-- 자바빈에 없기 때문에 기본 태그를 사용해야됨. 비밀번호와 같은지 확인용도. JavaScript로 확인 -->
                <label for="confirm_passwd">새비밀번호 확인</label>
                <input type="password" id="confirm_passwd">
                <span id="message_password"></span>
            </li>
            <li>
                <div id="captcha_div">
                    <img src="getCaptcha" id="captcha_img" width="200" height="90">
                </div>
                <input type="button" value="새로고침" id="reload_btn">
                <!-- 새로고침을 누르면 위 이미지 태그가 리로드되도록 -->
                <script>
                    $(function() {
                        $('#reload_btn').click(function() {
                            $.ajax({
                                url: 'getCaptcha',
                                type: 'get',
                                success: function() { //captcha_div는 캡챠의 이미지주소가 있는 div이다.
                                					 //' #captcha_div' #앞에 공백이 없으면 페이지 전체로 인식하기 때문에
                                					 //한 칸 띄우고 작성해야된다.
                                    $('#captcha_div').load(location.href + ' #captcha_div');
                                },
                                error: function() {
                                    alert('네트워크 오류 발생');
                                }
                            });
                        });
                    });
                </script>
            </li>
            <li>
                <form:label path="captcha_chars">캡챠 문자 확인</form:label>
                <form:input path="captcha_chars"/>
                <form:errors path="captcha_chars" cssClass="error-color"/>
            </li>
        </ul>
        <div class="align-center">
            <form:button>전송</form:button>
            <input type="button" value="MY페이지" onclick="location.href='myPage'">
        </div>
    </form:form>
</div>
<!-- 비밀번호 변경 끝 -->

MemberVO 내용추가(captcha 조건체크를 만들지않아서 만들어줌)

//비밀번호 변경에만 조건체크
	@Pattern(regexp="^[A-za-z0-9]$")
	private String captcha_chars;

captcha 이미지가 나오질않음.

captcha 이미지 띄우기

CaptchaUtil.java
0.00MB

네이버에서 공유하는 captchaUtil. api키만 받으면 사용가능

kr.spring.util에 넣으면된다.

웹서비스 주소는 캡차를 쓰는 localHost주소를 썼다.
ID와 Secret를 Controller에 넣어준다.

 

MemberController.java 내용추가

//비밀번호 변경 폼에서 전송된 데이터 처리
		
		/*===============================
		 * 네이버 captcha(캡챠) API 사용
		 ===============================*/
		//캡챠 이미지 호출
		@GetMapping("/member/getCaptcha")
		public String getCaptcha(Model model, HttpSession session) {
			String clientId = "Cc1wS4rT2GXzIGRPGXca";
			String clientSecret = "AHEsY00rCk";
			
			String code ="0"; //키 발급시 0, 캡챠 이미지 비교시 1로 세팅
			String key_apiURL = "https://openapi.naver.com/v1/captcha/nkey?code="+code;
			
			Map<String,String> requestHeaders = new HashMap<String,String>();
			requestHeaders.put("X-Naver-Client-Id",clientId);
			requestHeaders.put("X-Naver-Client-Secret",clientSecret);
			String responseBody = CaptchaUtil.get(key_apiURL, requestHeaders);
			
			log.debug("<<responseBody>> :"+responseBody);
			
			JSONObject jObject = new JSONObject(responseBody);
			try {
				//https://openapi.naver.com/v1/captcha/nkey 호출로 받은 키값("key")을 넣어준다.
				String key = jObject.getString("key");
				//이미지의 나온 값과 내가입력한 값이 맞는지 확인하기 위해 키값을 세션에 저장해둠
				session.setAttribute("captcha_key", key);
				String apiURL = "https://openapi.naver.com/v1/captcha/ncaptcha.bin?key="+key;
				
				Map<String,String> requestHeaders2 = new HashMap<String,String>();
				requestHeaders.put("X-Naver-Client-Id",clientId);
				requestHeaders.put("X-Naver-Client-Secret",clientSecret);
				
				byte[] reponse_byte = CaptchaUtil.getCaptchaImage(apiURL, requestHeaders2);
				model.addAttribute("imageFile",reponse_byte);
				model.addAttribute("filename","captcha.jpg");
			}catch(Exception e) {
				log.error(e.toString());
			}
			return "imageView";
		}

MemberController 코드 중 JSONObject jObject = new JSONObject(); 

이부분이 라이브러리가 없어서 pom.xml에 라이브러리를 추가해야됨.

<dependency>
			<groupId>org.json</groupId>
			<artifactId>json</artifactId>
			<version>20230227</version>
		</dependency>
		<!-- 라이브러리 추가 끝 -->

하루 1000번까지 캡차 이미지 사용가능하다.