본문 바로가기

카테고리 없음

[Spring/maven]프로젝트 회고 1. 회원가입(유효성체크 & ajax Id 중복체크)

회원가입 로직

 

사용자가 회원가입 시 너무 많은 입력사항이 있을 경우 귀찮아 할 수 있다. 그래서 너무많은 정보를 요청하는 것은 옳지 않다. 하지만 사이트에 필요한 최소한의 정보는 반드시 요청해야한다. 유효성체크를 통해 홈페이지만의 회원가입 규칙을 정하고 규칙을 어긋난 회원가입을 막는다. ID중복체크를 통해 Id의 고유값(not null)을 가져오고 이 id와 sequence로 주어지는 user_no을 통해 사이트의 여러기능을 사용 할 때 회원정보를 불러온다.

 

SQL

설계를 끝내고 코드를 짜려면 회원가입에 필요한 data를 DB에 저장하기 위해 users 테이블을 만들어야 한다.

CREATE TABLE users(
user_no NUMBER PRIMARY KEY NOT null,
user_type NUMBER,
id varchar2(50),
nickname varchar2(50),
email varchar2(50),
password varchar2(50),
name varchar2(50),
joindate DATE,
gender varchar2(50),
zipcode varchar2(50),
addr varchar2(500),
addr_detail varchar2(500),
user_level varchar2(50),
user_auth NUMBER,
file_no NUMBER
);

 

Primary key에 시퀀스넘버도 주면 테이블 완성이다.

CREATE SEQUENCE seq_user_no INCREMENT BY 1 START WITH 1;
SELECT seq_user_no.nextval FROM dual;
SELECT seq_user_no.currval FROM dual;

 

 

 

XML 

먼저 DB에 직접적으로 명령을 수행할 쿼리문을 작성해준다.

<?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="mapper.users">
	<!-- 회원가입 -->
	<insert id="insertUsers"
		parameterType="com.kosta.petner.bean.Users">
		<![CDATA[
			INSERT INTO USERS(user_no, 
							 user_type, 
							 id, 
							 nickname, 
							 email, 
							 password, 
							 name, 
							 joindate, 
							 gender, 
							 zipcode, 
							 addr, 
							 addr_detail, 
							 user_level, 
							 user_auth)
				values(seq_user_no.nextval, 
							#{user_type}, 
							#{id},
							#{nickname}, 
							#{email}, 
							#{password},
							#{name},
							 sysdate, 
							#{gender}, 
							#{zipcode},
							#{addr}, 
							#{addr_detail},
							'BRONZE',
							 1)					
		]]>
	</insert>

	<!-- 중복체크 -->
	<select id="selectId" parameterType="String"
		resultType="com.kosta.petner.bean.Users">
	<![CDATA[
	select * from users where id =#{id}
	]]>
	</select>

insert를 통해 회원가입 로직에 맞게 data를 집어넣도록 하고, id에 대한 중복값을 막기위해 id로 유저정보를 조회할 수 있는 쿼리문 또한 함께 작성한다.

 

DAO/ DAOImpl

 

public interface UsersDAO {

		// 회원가입
	void insertUsers(Users users) throws Exception;

	// 아이디 중복체크
	Users selectId(String id) throws Exception;
@Repository
public class UsersDAOImpl implements UsersDAO{

	@Autowired
	private SqlSessionTemplate sqlSession;
	
	public void setSqlSession(SqlSessionTemplate sqlSession) {
		this.sqlSession = sqlSession;
	}
	//회원가입
	@Override
	public void insertUsers(Users users) throws Exception {
		sqlSession.insert("mapper.users.insertUsers", users);
		
	}
    	//아이디 중복체크
	@Override
	public Users selectId(String id) {
		return sqlSession.selectOne("mapper.users.selectId", id);
	}

 

앞으로 계속 쓸 세션을 autowire해주고, 쿼리문을 호출 할 DAO에 회원가입 메서드를 만들어준다.

                                                                (이제 DAO를 호출 하면 쿼리문이 실행 되는 것)

 

Service/ ServcieImpl

 

컨트롤러와 DAO를 연결시켜 줄 service부분을 작성한다.

 

public interface UsersService {
	// 회원가입
	public void joinUsers(Users users) throws Exception;
    	// 중복체크
	public boolean isDoubleId(String id) throws Exception;
@Service
public class UsersServiceImpl implements UsersService {

	@Autowired
	UsersDAO usersDAO;
	
	@Autowired
	BCryptPasswordEncoder bcryptPasswordEncoder;
	
	
	@Override
	public void joinUsers(Users users) throws Exception {
		Users usersvo = usersDAO.selectId(users.getId());
		if(usersvo!=null) throw new Exception("아이디 중복");
		usersDAO.insertUsers(users);
		
	}
    
    	@Override
	public boolean isDoubleId(String id) throws Exception {

		//userDAO를 통해 xml에서 가져온 id 값을 users에 담는다.
		Users users = usersDAO.selectId(id);
		if(users==null) return false;
		return true;
	}

impl은 부모속성을 물려받은 자식클래스이다. 그렇기때문에 어노테이션을 꼭 해줄것!

그냥 DAO의 insertUsers메서드를 호출 할 수도 있지만, 아이디 중복을 막기위해 회원정보를 조회하고,

만약 회원정보에 id값이 있다면 exception(오류)를 발생하게 한다.

xml과 dao에서 ID를 통해 회원목록을 불러온 중복체크 메서드는 boolean으로 참 거짓 값을 가져오게 한다.  

 

Controller

package com.kosta.petner.controller;



import java.util.Date;

import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.WebUtils;

//import com.kosta.petner.bean.ChatSession;
import com.kosta.petner.bean.Users;
import com.kosta.petner.dao.UsersDAO;
import com.kosta.petner.service.UsersService;



@Controller
public class UsersController {
	
	@Autowired
	BCryptPasswordEncoder bcryptPasswordEncoder;

	@Inject
	UsersService usersService;
	
	@Autowired
	HttpSession session;
	
	@Autowired
	UsersDAO usersDAO;

//회원가입 이동
	@RequestMapping(value = "/join", method = RequestMethod.GET)
	String joinForm() {
		return "users/join/joinForm";
	}
	
	//회원가입 
	@RequestMapping(value = "/joinpet", method = RequestMethod.POST)
	String join(@ModelAttribute Users users, Model model) {
		  System.out.println("암호화 전 : " + users);	
		try{		
			String passBcrypt = bcryptPasswordEncoder.encode(users.getPassword());
			   users.setPassword(passBcrypt);
			   usersService.joinUsers(users);
			   System.out.println("암호화 후 : " + users);
			
			
		} catch(Exception e) {
			e.printStackTrace();
			model.addAttribute("err", "회원가입 오류");
			model.addAttribute("page", "err");
		}
		
		return "users/join/joinSuccess";
	}
    
    	//중복체크
	@ResponseBody 
	@RequestMapping(value = "/checkId", method = RequestMethod.POST)
	   String checkId(@RequestParam("id") String id, Model model) {
		try {
			if(usersService.isDoubleId(id)) return "true";
		}catch(Exception e) {
			e.printStackTrace();
			model.addAttribute("page","err");
		}
	      return "false";
	}

역시나 컨트롤러 annotation과 import를 잊지말자! 우선은 회원가입폼으로 이동하는 Get방식의 mapping과 회원가입이 실행되는 Post방식의 mapping을 작성한다.

 

회원가입 컨트롤러에서는 객체를 가져오기위해 ModelAttribute를 쓴다. 중복체크에서는 id값만 가져오기 때문에 requestParam을 썼다. 

usersService.joinUsers(users);를 통해 xml-DAO-service로 연결된  회원가입 메서드가 실행된다.

(*Bcrypt는 스프링시큐리티에서 제공하는강력한  암호화 방법이다. 지금은 안써도되니 주석처리 해도 된다. 나중에 설명)

//암호화 부분
//String passBcrypt = bcryptPasswordEncoder.encode(users.getPassword());
//			   users.setPassword(passBcrypt);

 

JSP

JoinForm.jsp 전체코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>



<!DOCTYPE html>
<html lang="en">
<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">
  <link rel="stylesheet" href="resources/css/common.css">
  <link rel="stylesheet" href="resources/css/form_component.css">
  <script src="https://kit.fontawesome.com/064a55beb6.js" crossorigin="anonymous"></script>
  <title>회원가입</title>
  <script src= "https://code.jquery.com/jquery-3.4.1.js"></script>
  <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
  <script>
  
  
  function check() {
     //id유효성검사
	  var id =$("#id").val();   //id 입력 값
	  var reg_id = /^[A-Za-z]{1}[A-Za-z0-9_-]{3,19}$/;  //영문+숫자조합(4~20까지)
	  var checked_id = $("input[name='checked_id']").val();  //중복체크 여부
	  
	  if(id == "") {
	  	$('#checkid-msg').text("아이디를 입력해주세요").css("color", "red");
	  $("#id").focus();
	  return false;
	  
	  }if(!reg_id.test(id)){
			$('#checkid-msg').text("아이디는 영문과 숫자조합만 가능합니다.(4~20자)").css("color", "red");
	  $("#id").focus();
		return false;
		
	  }if(checked_id==''){
	  	$('#checkid-msg').text("아이디 중복확인을 먼저 해주세요").css("color", "red");
	      $("#id").focus();
			return false;
			
	  }else{
			$('#checkid-msg').text("사용가능한 아이디입니다.").css("color", "green");
	  }
	  
	//비밀번호 유효성검사
	  var password =$("#password").val();          //비밀번호 입력값
	  var ck_password = $("#ck_password").val();   //비밀번호 체크 입력값
	  var acceptPass = /^(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,16}$/; 
	  
	  if(!password){
	  	$('#checkpass-msg').text("비밀번호를 입력해주세요").css("color", "red");
	    $("#password").focus();
	    return false;
	    
	  }if(!acceptPass.test(password)){
			$('#checkpass-msg').text("비밀번호는 영어+숫자+특수문자를 조합해야 합니다(8자 이상)").css("color", "red");
		    $("#password").focus();
		  return false;
		  
	  }if(!ck_password){
	  	$('#checkpass-msg').text("비밀번호 확인을 입력해주세요").css("color", "red");
	    $("#ck_password").focus();
	    return false;
	    
	  }if(password !== ck_password){
	  	$('#checkpass-msg').text("비밀번호가 일치하지 않습니다").css("color", "red");
	      $("#password").focus();
	      return false;
	      
	  }else{
			$('#checkpass-msg').text("사용가능한 비밀번호입니다.").css("color", "green");
	  }
  
	  //email유효성검사 +email 합치기
	  var reg_email = /^[a-zA-Z]{1}[a-zA-Z0-9_]{4,11}$/; 
	  var email_id =$("#email_id").val();
	  var email_domain =$("#email_domain").val();
	  var email ="";
	 
	  if(!email_id){
	    $('#checkmail-msg').text("이메일을 입력해주세요").css("color", "red");
	    $("#email_id").focus();
	    return false;
	    
	  }if(!reg_email.test(email_id)){
	    $('#checkmail-msg').text("올바른 이메일을 입력해주세요").css("color", "red");
	      $("#email_id").focus();
	    return false;
	    
	  }else{
			$('#checkmail-msg').text("사용가능한 이메일입니다.").css("color", "green");
	  }
	  
	  email = email_id+"@"+email_domain;  //콤마뺴기
	  $("#email").val(email);  
	
		//이름 유효성 검사
	  var reg_name = /^[가-힣]+$/;
	  var name = $("#name").val();
		
	  if(!name){
	      $('#checkname-msg').text("이름을 입력해주세요").css("color", "red");
	      $("#name").focus();
	      return false;
	      
	    } if(!reg_name.test(name)){
	      $('#checkname-msg').text("이름은 한글로 입력해주세요").css("color", "red");
		      $("#name").focus();
	      return false;
	      
	    }else{
			$('#checkname-msg').text("사용가능한 이름입니다.").css("color", "green");
	    }
	    
	    //별명 유효성검사
	  	var reg_nickname = /^[가-힣]{2,6}$/;
		var nickname = $("#nickname").val();
		
		 	 if(!nickname){
	    $('#checknickname-msg').text("별명을 입력해주세요").css("color", "red");
	    $("#nickname").focus();
	    return false;
	    
	  } if(!reg_name.test(nickname)){
	    $('#checknickname-msg').text("별명은 한글로 지어주세요(2~6)").css("color", "red");
	      $("#nickname").focus();
	    return false;
	  }
	 
	  //주소 유효성검사
	  	var add3 = $("#add3").val();
		
		 	 if(!add3){
	    $('#checkaddress-msg').text("상세주소를 입력해주세요").css("color", "red");
	    $("#add3").focus();
	    return false;
	    
		}else{
		 return true;
	  }
  } //check()끝
  
  //주소검색 API

    function Zipcode() {
        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 + ')';
                    }
                    // 조합된 참고항목을 해당 필드에 넣는다.
                    document.getElementById("add4").value = extraAddr;
                
                } else {
                    document.getElementById("add4").value = '';
                }

              			  // 우편번호와 주소 정보를 해당 필드에 넣는다.
             			  $("#add1").val(data.zonecode);
             			  $("#add2").val(addr);
                
        	}
  	  }).open();
	}


  
  $(function () {
	   $('#joinForm').submit(function() {
		
			  }); //유효성체크 끝
		
	 	//중복아이디 = ajax 쓰는이유: data를 서버에 주고 다시 받아올때 비동기방법을 씀
	   	$('#doubleId').click(function() {
	   		var id = $("#id").val();  //사용자가 입력한 아이디를 변수 id에 담음
	   		var reg_id = /^[A-Za-z]{1}[A-Za-z0-9_-]{3,19}$/; 
	   		
	   		$("input[name=checked_id]").val('y'); //중복체크  yes

	   		if(id=="") {
	   			$('#checkid-msg').text("아이디를 입력해주세요").css("color", "red");
	   			$("#id").focus();
	   			return;

	   		}if(!reg_id.test(id)){
				$('#checkid-msg').text("아이디는 영문과 숫자조합만 가능합니다.(4~20자)").css("color", "red");
				$("#id").focus();
				return;
	   		}
	   		$.ajax({
	   			type:"post",
	   			url:"http://localhost:8088/petner/checkId",
	   			data:{id:id},   //id(key):id(value)
	   			success:function(data,textStatus) {
	   				if(data=="true") {
	   					$('#checkid-msg').text("중복아이디 입니다").css("color", "red");
	   				}else{ 
	   					$('#checkid-msg').text("사용 가능한 아이디입니다").css("color", "green");
	   				}	
	   				
	   			} 
			}) //ajax 끝
		}); // 중복체크 끝
	});//function ready 끝
  

</script>
</head>



 </head>


<body>
  <div id="wrapper">

    <div class="login_form w50">      
      <h3 class="form_title">회원가입</h3>
      <form id= "joinForm" action="./joinpet" method="POST" class="join_form" onsubmit = "return check()">
      
        <div class="f_row">
          <p class="fc_title">&#128586 펫트너 가족되기 &#128586</p>
          <p class="tip"><i class="fa-solid fa-asterisk"></i> 회원가입후 추가로 정보를 등록할 수 있어요</p>
          <div class="flex_col">
            <label class="fcRadio1 mb10" style= "width:800px;"><input type="radio" name="user_type" id="user_type" value= "2" checked><span>보호자로 등록하기 ( 펫시터를 찾고있어요 )  </span></label>
            <label class="fcRadio1 " style= "width:800px;"><input type="radio" name="user_type" value= "1"><span>펫시터로 등록하기 ( 돌봐줄 동물을 찾고있어요 )</span></label>
          </div>          
        </div>
        <!-- TEXT/PASSWORD -->
        
        <div class="f_row">
          <p class="fc_title">아이디</p>
          <p class="flex_agn_center">
          <input type="hidden" name="checked_id" value="">
          <input class="mr12" type="text" placeholder="ID" name ="id" id="id"/>
            <span class="pet_btn second_btn transition02" id="doubleId">중복확인</span>
          </p>
          <p><small id="checkid-msg" class="form-error"></small></p>
        </div>
        <div class="f_row flex_col" style= "width:200px;">
          <p class="fc_title">비밀번호</p>
          <input class="mb10" type="password" name ="password" id="password" placeholder="비밀번호입력"/>
          <input type="password" name="ck_password" id="ck_password" placeholder="비밀번호확인"/>
          <p><small id="checkpass-msg" class="form-error"></small></p>
        </div>
        <div class="f_row">
          <p class="fc_title">이메일</p>
          <p class="flex_agn_center">
          	<input type ="hidden" id= "email" name= "email"/>
            <input type="text" placeholder="" id="email_id" name=""/>
            <span style="margin:0 10px;">@</span>
            <select class="fcc_select" name="" id="email_domain">
                <option value="naver.com">naver.com</option>
   				<option value="gmail.com">gmail.com</option>
    			<option value="hanmail.net">hanmail.net</option>
    			<option value="hotmail.com">hotmail.com</option>
   			 	<option value="korea.com">korea.com</option>
   				<option value="nate.com">nate.com</option>
   				<option value="yahoo.com">yahoo.com</option>
            </select>
          </p>
          <p><small id="checkmail-msg" class="form-error"></small></p>
        </div>
        <div class="f_row">
          <p class="fc_title">이름</p>
          <input type="text" id="name" name="name"/>
          <p><small id="checkname-msg" class="form-error"></small></p>
        </div>
        <div class="f_row">
          <p class="fc_title">나의별명</p>
          <input type="text" id="nickname" name="nickname"/>
         <p><small id="checknickname-msg" class="form-error"></small></p>
        </div>
        <div class="f_row">
          <p class="fc_title">성별</p>
          <label class="fcRadio1 mr12">
            <input type="radio" name="gender" id="gender" value="male" checked><span>남자</span>
			</label>          
            <label class="fcRadio1 mr12">
            <input type="radio" name="gender" value="female"><span>여자</span>
          </label>
        </div>
        <div class="f_row">
          <p class="fc_title">주소입력</p>
          <div class="flex_col">
            <p class="mb10">
              <input class="mr12" type="text" id="add1" name="zipcode" readonly/><input type="button" class="pet_btn second_btn transition02" onclick="Zipcode()" value= "주소찾기"/>
            </p>            
            <input class="mb10" type="text" id ="add2" name="addr" readonly/>
            <input class="mb10" type="text" id ="add3" name="addr_detail" placeholder="상세주소입력"/>
            <input type="hidden" id="add4" name="address" placeholder="참고항목">
             <p><small id="checkaddress-msg" class="form-error"></small></p>
          </div>
           <input type="hidden" name="joindate" id="joindate"/>
           
           
        </div>
        
        <input type="submit" class="pet_btn submit_btn transition02" value= "회원가입하기"/>
      </form>
    </div>
  </div>
</body>

회원가입은 유효성체크, 주소API, ID중복체크, css 등 많은 부분이 구현되어 있어 코드가 굉장히 길어졌다.

(* 단순 가입만 원한다면 <body></body>안에 있는 코드만 넣어도 된다.)

 

유효성체크는 여러 정규식을 통해 회원가입 규칙에 맞도록 작성해준다. 유효성체크는 여러번 검사를 통해 꼼꼼히 작성하였고 통과하지 못 할 경우 onsubmit() 태그를 통하여 다시 submit을 막는다.

 

주소API는 카카오톡(또는 다음)에서 제공하는 API를 썼고 내 회원가입 양식에 맞게 여러 코드를 제공하고 있다.

 

중복체크는 Ajax를 통해 실제 data를 와 대조 후 Id가 있으면 중복에 걸리게 하였다.

 

모든 유효성체크를 지나면 submit이 실행되고 회원가입 완성 폼으로 넘어간다.

(email은 유효성체크 뿐만아니라 email_id와 email_domain으로 나뉜 두개의 데이터를 email 이라는 id를 주어 합쳐야 한다.

그렇지 않으면 id, domain 식의 두 데이터가 칼럼 하나에 저장된다.) <조금 애를 먹은 부분

 

JoinSuccess.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="cssPath" value="${pageContext.request.contextPath}/resources/css"/>
<c:set var="imgPath" value="${pageContext.request.contextPath}/resources/images"/>

<!DOCTYPE html>
<html>
<head>
	<c:import url='/WEB-INF/views/include/common_head.jsp'/>
	<title>회원가입 성공!</title>

</head>
<style>
	#findId input[type=text]{width:100%;}
	.desc{text-align:center; margin-bottom:35px;}
	.find_id{width:120px !important; text-align:center;}
	.flex_between{flex-wrap:nowrap}
	.submit_btn{position: relative; top:180px;}
</style>
<body>
  <div id="wrapper">
    <!-- HEADER BASIC -->
    <c:import url='/WEB-INF/views/include/header.jsp'/>
    <!-- CONTAINER -->

		<div class="container">
			<div class="w45">
				<h3 class="form_title">회원가입 성공</h3>
				<div class="desc">	
				<c:choose>
				<c:when test="${users.user_type ==2}">
					<p class="mb10" style="color:#ADD8E6;">[💁🏻‍♂️ 보호자]</p>
					<p class="mb10" style="color: orange;">&#128054 ${users.id}님의 가입을 축하드립니다! &#128049</p>
					</c:when>
					<c:otherwise>
					<p class="mb10"style="color:#ADD8E6;">[🙋 펫시터]<p>
					<p class="mb10" style="color: orange;">&#128054 ${users.id}님의 가입을 축하드립니다! &#128049</p>
					</c:otherwise>
				</c:choose>
			
				</div>
				
			
			</div>
		</div>



		<!-- FOOTER BASIC -->
    <c:import url='/WEB-INF/views/include/footer.jsp'/>
    
  </div>
</body>
</html>

 

 

구현 화면

 

joinForm.jsp
주소API
각종 유효성 체크