Study Web Development

1월 25일

이전으로

Servlet & JSP

9. MVC

mvcPage

프로젝트 생성 및 설정
  1. 이클립스에서 Project Explorer 오른쪽 클릭하고 Import-WAR file 선택하고 mvcPage.war 선택 후 Finish
  2. mvcPage 프로젝트의 src/main/webapp 오른쪽 클릭하고 새 폴더 sql 생성 후 새 파일 table.sql 생성
/* 회원 관리 */
CREATE TABLE zmember(
	mem_num NUMBER NOT NULL,
	id VARCHAR2(12) UNIQUE NOT NULL, /* 탈퇴하더라도 아이디를 보존하여 재가입을 방지 */
	auth NUMBER(1) DEFAULT 2 NOT NULL, /* 회원 등급; 0: 탈퇴, 1: 정지, 2: 일반, 3: 관리자 */
	CONSTRAINT zmember_pk PRIMARY KEY (mem_num)
);

CREATE TABLE zmember_detail( /* 개인 정보를 별도의 테이블로 분리하여 탈퇴시에 개인 정보만 삭제 */
	mem_num NUMBER NOT NULL,
	name VARCHAR2(30) NOT NULL,
	passwd VARCHAR2(12) NOT NULL,
	phone VARCHAR2(15) NOT NULL,
	email VARCHAR2(50) NOT NULL,
	zipcode VARCHAR2(5)  NOT NULL,
	address1 VARCHAR2(90) NOT NULL,
	address2 VARCHAR2(90) NOT NULL,
	photo VARCHAR2(150), /* 프로필 사진 파일명 */
	reg_date DATE DEFAULT SYSDATE NOT NULL,
	modify_date DATE,
	CONSTRAINT zmember_detail_pk PRIMARY KEY (mem_num),
	CONSTRAINT zmember_detail_fk FOREIGN KEY (mem_num) REFERENCES zmember (mem_num)
);

CREATE SEQUENCE zmember_seq;
  1. WEB-INF 폴더의 member.properties를 열고 다음의 내용을 작성
# main
/main/main.do=kr.main.action.MainAction
# member
/member/checkId.do=kr.member.action.CheckDuplicatedIdAction
/member/registerUserForm.do=kr.member.action.RegisterUserFormAction
  1. css 폴더의 layout.css 하단에 다음의 내용을 추가
/* 메인 */
a:link {
	text-decoration: none;
	color: #000;	
}
a:visited {
	text-decoration: none;
	color: #000;	
}
a:hover {
	text-decoration: underline;
	color: #999;	
}

/* My Page */
.mypage-div {
	width: 48%;
	float: left;
	padding: 5px;
}
.my-photo {
	object-fit: cover;
	/* 정사각형이 아니라 직사각형일 경우 원 안에 보여지게 할 중심 이미지의 위치를 지정 */
	object-position: top;
	/* 사각형의 모서리 둥근 정도를 지정; 50% 지정시 완전한 원이 됨 */
	border-radius: 50%;
}
  1. webapp 오른쪽 클릭하고 새 JSP 파일 index.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	response.sendRedirect(request.getContextPath() + "/main/main.do");
%>
  1. C:\javaWork\apps에서 jackson-core-asl-1.9.11.jar, jackson-mapper-asl-1.9.11.jar를 복사하여 WEB-INFlib 폴더에 붙여넣기
  2. webapp 폴더 오른쪽 클릭하고 새 폴더 js 생성 후 jquery-3.6.0.min.js 파일을 해당 폴더로 이동
자바빈
  1. src/main/java 오른쪽 클릭하고 새 패키지 kr.member.vo 생성 후 새 클래스 MemberVO 생성
package kr.member.vo;

import java.sql.Date;

public class MemberVO {
	private int mem_num; // 회원 번호
	private String id; // 아이디
	private int auth; // 회원 등급; 0: 탈퇴, 1: 정지, 2: 일반, 3: 관리자
	private String name; // 이름
	private String passwd; // 비밀번호
	private String phone; // 전화번호
	private String email; // 이메일
	private String zipcode; // 우편번호
	private String address1; // 주소1
	private String address2; // 주소2
	private String photo; // 프로필 사진의 파일명
	private Date reg_date; // 가입일
	private Date modify_date; // 회원 정보 수정일
	
	// 비밀번호 일치 여부 체크
	public boolean isCheckedPassword(String UserPasswd) {
		if(auth>1 && passwd.equals(UserPasswd)) { // 탈퇴 및 정지 회원은 서비스 이용 불가하도록 처리
			return true;
		}
		return false;
	}
	
	public int getMem_num() {
		return mem_num;
	}
	public void setMem_num(int mem_num) {
		this.mem_num = mem_num;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public int getAuth() {
		return auth;
	}
	public void setAuth(int auth) {
		this.auth = auth;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPasswd() {
		return passwd;
	}
	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}
	public String getPhone() {
		return phone;
	}
	public void setPhone(String phone) {
		this.phone = phone;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getZipcode() {
		return zipcode;
	}
	public void setZipcode(String zipcode) {
		this.zipcode = zipcode;
	}
	public String getAddress1() {
		return address1;
	}
	public void setAddress1(String address1) {
		this.address1 = address1;
	}
	public String getAddress2() {
		return address2;
	}
	public void setAddress2(String address2) {
		this.address2 = address2;
	}
	public String getPhoto() {
		return photo;
	}
	public void setPhoto(String photo) {
		this.photo = photo;
	}
	public Date getReg_date() {
		return reg_date;
	}
	public void setReg_date(Date reg_date) {
		this.reg_date = reg_date;
	}
	public Date getModify_date() {
		return modify_date;
	}
	public void setModify_date(Date modify_date) {
		this.modify_date = modify_date;
	}
}
DAO
  1. kr.util 패키지에 새 클래스 DBUtil 생성
package kr.util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

public class DBUtil {
	// context.xml에서 설정 정보를 읽어들여 커넥션 풀로부터 커넥션을 할당받음
	public static Connection getConnection() throws Exception {
		Context initCtx = new InitialContext();
		DataSource ds = (DataSource)initCtx.lookup("java:comp/env/jdbc/xe");
		return ds.getConnection();
	}
	
	// 자원 정리
	public static void executeClose(ResultSet rs, PreparedStatement pstmt, Connection conn) {
		if(rs!=null) try {rs.close();} catch(SQLException e) {}
		if(pstmt!=null) try {pstmt.close();} catch(SQLException e) {}
		if(conn!=null) try {conn.close();} catch(SQLException e) {}
	}
}
  1. src/main/java 오른쪽 클릭하고 새 패키지 kr.member.dao 생성 후 새 클래스 MemberDAO 생성
package kr.member.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import kr.member.vo.MemberVO;
import kr.util.DBUtil;

public class MemberDAO {
	// 싱글턴 패턴
	private static MemberDAO instance = new MemberDAO();
	public static MemberDAO getInstance() {
		return instance;
	}
	private MemberDAO() {}
	
	// 사용자
	// 회원 가입
	public void insertMember(MemberVO member) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		PreparedStatement pstmt2 = null;
		PreparedStatement pstmt3 = null;
		ResultSet rs = null;
		String sql = null;
		int num = 0; // 시퀀스 번호 저장
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// 오토 커밋 해제
			conn.setAutoCommit(false);
			
			// 회원 번호 생성
			// SQL문 작성
			sql = "SELECT zmember_seq.NEXTVAL FROM dual";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// SQL문을 실행해서 결과 행을 ResultSet에 담아 반환
			rs = pstmt.executeQuery();
			if(rs.next()) {
				num = rs.getInt(1);
			}
			
			// zmember 테이블에 데이터 저장
			// SQL문 작성
			sql = "INSERT INTO zmember (mem_num, id) VALUES (?, ?)";
			// PreparedStatment 객체 생성
			pstmt2 = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt2.setInt(1, num);
			pstmt2.setString(2, member.getId());
			// SQL문 실행
			pstmt2.executeUpdate();
			
			// zmember_detail 테이블에 데이터 저장
			// SQL문 작성
			sql = "INSERT INTO zmember_detail (mem_num, name, passwd, phone, email, zipcode, address1, address2) "
				+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
			// PreparedStatement 객체 생성
			pstmt3 = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt3.setInt(1, num);
			pstmt3.setString(2, member.getName());
			pstmt3.setString(3, member.getPasswd());
			pstmt3.setString(4, member.getPhone());
			pstmt3.setString(5, member.getEmail());
			pstmt3.setString(6, member.getZipcode());
			pstmt3.setString(7, member.getAddress1());
			pstmt3.setString(8, member.getAddress2());
			// SQL문 실행
			pstmt3.executeUpdate();
			
			// SQL문 실행이 모두 성공하면 커밋
			conn.commit();
		}
		catch(Exception e) {
			// SQL문 실행이 하나라도 실패하면 롤백
			conn.rollback(); // rollback() 메서드는 try~catch 의무이지만, insertMember() 메서드 차원에서 throws Exception이므로 생략 가능
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt3, null);
			DBUtil.executeClose(null, pstmt2, null);
			DBUtil.executeClose(rs, pstmt, conn);
		}
	}
	
	// 아이디 중복 체크 및 로그인 처리
	public MemberVO checkMember(String id) throws Exception {
		MemberVO member = null;
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// SQL문 작성
			sql = "SELECT * FROM zmember m LEFT OUTER JOIN zmember_detail d "
				+ "ON m.mem_num=d.mem_num WHERE m.id=?"; // zmember_detail에서 정보가 삭제된 아이디(=탈퇴한 경우)도 조회되어야 아이디 중복 체크가 가능하므로 OUTER JOIN			
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setString(1, id);			
			// SQL문을 실행해서 결과 행을 ResultSet에 담아 반환
			rs = pstmt.executeQuery();
			if(rs.next()) {
				member = new MemberVO();
				member.setMem_num(rs.getInt("mem_num"));
				member.setId(rs.getString("id"));
				member.setAuth(rs.getInt("auth"));
				member.setPasswd(rs.getString("passwd"));
				member.setPhoto(rs.getString("photo"));
			}
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(rs, pstmt, conn);
		}
		
		return member;
	}
	
	// 회원 상세 정보
	public MemberVO getMember(int mem_num) throws Exception {
		MemberVO member = null;
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// SQL문 작성
			sql = "SELECT * FROM zmember m JOIN zmember_detail d "
				+ "ON m.mem_num=d.mem_num WHERE m.mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setInt(1, mem_num);
			// SQL문을 실행해서 결과 행을 ResultSet에 담아 반환
			rs = pstmt.executeQuery();
			if(rs.next()) {
				member = new MemberVO();
				member.setMem_num(rs.getInt("mem_num"));
				member.setId(rs.getString("id"));
				member.setAuth(rs.getInt("auth"));
				member.setPasswd(rs.getString("passwd"));
				member.setName(rs.getString("name"));
				member.setPhone(rs.getString("phone"));
				member.setEmail(rs.getString("email"));
				member.setZipcode(rs.getString("zipcode"));
				member.setAddress1(rs.getString("address1"));
				member.setAddress2(rs.getString("address2"));
				member.setPhoto(rs.getString("photo"));
				member.setReg_date(rs.getDate("reg_date"));
				member.setModify_date(rs.getDate("modify_date"));
			}
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(rs, pstmt, conn);
		}
		
		return member;
	}
	
	// 회원 정보 수정
	public void updateMember(MemberVO member) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// SQL문 작성
			sql = "UPDATE zmember_detail SET name=?, phone=?, email=?, "
				+ "zipcode=?, address1=?, address2=?, "
				+ "modify_date=SYSDATE WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setString(1, member.getName());
			pstmt.setString(2, member.getPhone());
			pstmt.setString(3, member.getEmail());
			pstmt.setString(4, member.getZipcode());
			pstmt.setString(5, member.getAddress1());
			pstmt.setString(6, member.getAddress2());
			pstmt.setInt(7, member.getMem_num());
			// SQL문 실행
			pstmt.executeUpdate();
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt, conn);
		}
	}
	
	// 비밀번호 수정
	public void updatePassword(String passwd, int mem_num) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// SQL문 작성
			sql = "UPDATE zmember_detail SET passwd=? WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setString(1, passwd); // 새 비밀번호
			pstmt.setInt(2, mem_num); // 회원 번호
			// SQL문 실행
			pstmt.executeUpdate();
		}
		catch (Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt, conn);
		}
	}

	// 프로필 사진 수정
	public void updateMyPhoto(String photo, int mem_num) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// SQL문 작성
			sql = "UPDATE zmember_detail SET photo=? WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setString(1, photo);
			pstmt.setInt(2, mem_num);
			// SQL문 실행
			pstmt.executeUpdate();
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt, conn);
		}
	}
	
	// 회원 탈퇴(=회원 정보 삭제)
	public void deleteMember(int mem_num) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		PreparedStatement pstmt2 = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// 오토 커밋 해제
			conn.setAutoCommit(false);
			
			// zmember의 auth 값 변경
			// SQL문 작성
			sql = "UPDATE zmember SET auth=0 WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setInt(1, mem_num);
			// SQL문 실행
			pstmt.executeUpdate();
			
			// zmember_detail의 레코드 삭제
			// SQL문 작성
			sql = "DELETE FROM zmember_detail WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt2 = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt2.setInt(1, mem_num);
			// SQL문 실행
			pstmt2.executeUpdate();
			
			// SQL문 실행이 모두 성공하면 커밋
			conn.commit();
		}
		catch(Exception e) {
			// SQL문 실행이 하나라도 실패하면 롤백
			conn.rollback();
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt2, null);
			DBUtil.executeClose(null, pstmt, conn);
		}
	}
	
	// 관리자
	// 총 회원 수
	public int getMemberCountByAdmin(String keyfield, String keyword) throws Exception {
		int count = 0;
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		String sql = null;
		String sub_sql = "";
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			
			// 검색 처리
			if(keyword!=null && !"".equals(keyword)) {
				if(keyfield.equals("1")) sub_sql = "WHERE id LIKE ?";
				else if(keyfield.equals("2")) sub_sql = "WHERE name LIKE ?";
				else if(keyfield.equals("3")) sub_sql = "WHERE email LIKE ?";
			}
			
			// 전체 또는 검색 결과 레코드 수
			// SQL문 작성
			sql = "SELECT COUNT(*) FROM zmember LEFT OUTER JOIN zmember_detail "
				+ "USING(mem_num) " + sub_sql;
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			if(keyword!=null && !"".equals(keyword)) {
				pstmt.setString(1, "%" + keyword + "%");
			}
			// SQL문을 실행해서 결과 행을 ResultSet에 담아 반환
			rs = pstmt.executeQuery();
			if(rs.next()) {
				count = rs.getInt(1);
			}
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(rs, pstmt, conn);
		}
		
		return count;
	}
	
	// 회원 목록
	public List<MemberVO> getListMemberByAdmin(int startRow, int endRow, String keyfield, String keyword) throws Exception {
		List<MemberVO> list = null;
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		String sql = null;
		String sub_sql = "";
		int cnt = 0; // 검색 여부에 따라 ?의 번호가 가변적인 것에 대응하기 위한 변수
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			
			// 검색 처리
			if(keyword!=null && !"".equals(keyword)) {
				if(keyfield.equals("1")) sub_sql = "WHERE id LIKE ?";
				else if(keyfield.equals("2")) sub_sql = "WHERE name LIKE ?";
				else if(keyfield.equals("3")) sub_sql = "WHERE email LIKE ?";
			}
			
			// SQL문 작성
			sql = "SELECT * FROM (SELECT z.*, ROWNUM AS rnum "
				+ "FROM (SELECT * FROM zmember LEFT OUTER JOIN zmember_detail "
				+ "USING(mem_num) " + sub_sql + " ORDER BY reg_date DESC NULLS LAST) z) "
				+ "WHERE rnum >= ? AND rnum <=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			if(keyword!=null && !"".equals(keyword)) {
				pstmt.setString(++cnt, "%" + keyword + "%");
			}
			pstmt.setInt(++cnt, startRow);
			pstmt.setInt(++cnt, endRow);
			// SQL문을 실행해서 결과 행들을 ResultSet에 담아 반환
			rs = pstmt.executeQuery();
			list = new ArrayList<MemberVO>();
			while(rs.next()) {
				MemberVO member = new MemberVO();
				member.setMem_num(rs.getInt("mem_num"));
				member.setId(rs.getString("id"));
				member.setAuth(rs.getInt("auth"));
				member.setPasswd(rs.getString("passwd"));
				member.setName(rs.getString("name"));
				member.setPhone(rs.getString("phone"));
				member.setEmail(rs.getString("email"));
				member.setZipcode(rs.getString("zipcode"));
				member.setAddress1(rs.getString("address1"));
				member.setAddress2(rs.getString("address2"));
				member.setPhoto(rs.getString("photo"));
				member.setReg_date(rs.getDate("reg_date"));
				member.setModify_date(rs.getDate("modify_date"));
				// 자바빈을 ArrayList에 저장
				list.add(member);
			}
		}
		catch(Exception e) {
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(rs, pstmt, conn);
		}
		
		return list;
	}
	
	// 회원 정보 수정
	public void updateMemberByAdmin(MemberVO member) throws Exception {
		Connection conn = null;
		PreparedStatement pstmt = null;
		PreparedStatement pstmt2 = null;
		String sql = null;
		
		try {
			// 커넥션 풀로부터 커넥션 할당
			conn = DBUtil.getConnection();
			// 오토 커밋 해제
			conn.setAutoCommit(false);
			
			// 회원 등급 변경
			// SQL문 작성
			sql = "UPDATE zmember SET auth=? WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt.setInt(1, member.getAuth());
			pstmt.setInt(2, member.getMem_num());
			// SQL문 실행
			pstmt.executeUpdate();
			
			// 회원 정보 변경
			// SQL문 작성
			sql = "UPDATE zmember_detail SET name=?, phone=?, email=?, "
				+ "zipcode=?, address1=?, address2=?, modify_date=SYSDATE "
				+ "WHERE mem_num=?";
			// PreparedStatement 객체 생성
			pstmt2 = conn.prepareStatement(sql);
			// ?에 데이터를 바인딩
			pstmt2.setString(1, member.getName());
			pstmt2.setString(2, member.getPhone());
			pstmt2.setString(3, member.getEmail());
			pstmt2.setString(4, member.getZipcode());
			pstmt2.setString(5, member.getAddress1());
			pstmt2.setString(6, member.getAddress2());
			pstmt2.setInt(7, member.getMem_num());
			// SQL문 실행
			pstmt2.executeUpdate();
			
			// SQL문 실행이 모두 성공하면 커밋
			conn.commit();
		}
		catch (Exception e) {
			// SQL문 실행이 하나라도 실패하면 롤백
			conn.rollback();
			throw new Exception(e);
		}
		finally {
			// 자원 정리
			DBUtil.executeClose(null, pstmt2, null);
			DBUtil.executeClose(null, pstmt, conn);
		}
	}
	
}
Model
  1. src/main/java 오른쪽 클릭하고 새 패키지 kr.main.action 생성 후 새 클래스 MainAction 생성
package kr.main.action;

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

import kr.controller.Action;

public class MainAction implements Action {

	@Override
	public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// JSP 경로 반환
		return "/WEB-INF/views/main.jsp";
	}

}
  1. src/main/java 오른쪽 클릭하고 새 패키지 kr.member.action 생성 후 새 클래스 CheckDuplicatedIdAction 생성
package kr.member.action;

import java.util.HashMap;
import java.util.Map;

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

import org.codehaus.jackson.map.ObjectMapper;

import kr.controller.Action;
import kr.member.dao.MemberDAO;
import kr.member.vo.MemberVO;

public class CheckDuplicatedIdAction implements Action {

	@Override
	public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 전송된 데이터 인코딩 처리
		request.setCharacterEncoding("UTF-8");
		
		// 전송된 데이터 반환
		String id = request.getParameter("id");
		
		MemberDAO dao = MemberDAO.getInstance();
		MemberVO member = dao.checkMember(id);
		
		Map<String, String> mapAjax = new HashMap<String, String>();
		if(member==null) { // 아이디가 중복되지 않은 경우
			mapAjax.put("result", "idNotFound");
		}
		else { // 아이디가 중복된 경우
			mapAjax.put("result", "idDuplicated");
		}
		
		/*
		 * JSON 형식으로 변환하기를 원하는 문자열을 HashMap에 key와 value의 쌍으로 저장하고, 
		 * ObjectMapper의 writeValueAsString() 메서드에 Map 객체를 전달하여 
		 * 일반 문자열 데이터를 JSON 형식의 데이터로 변환 후 반환
		 */
		ObjectMapper mapper = new ObjectMapper();
		String ajaxData = mapper.writeValueAsString(mapAjax);
		
		request.setAttribute("ajaxData", ajaxData);
		
		// JSP 경로 반환
		return "/WEB-INF/views/common/ajax_view.jsp"; // 모델 클래스가 직접 Ajax 데이터를 클라이언트에 전송할 수 없으므로 서버 프로그램인 JSP가 필요; Ajax 데이터를 전송하는 JSP의 경우, 별도의 가공을 하지 않기 때문에 여러 모델 클래스에서 공유 가능
	}

}
  1. kr.member.action 패키지에 새 클래스 RegisterUserFormAction 생성
package kr.member.action;

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

import kr.controller.Action;

public class RegisterUserFormAction implements Action {

	@Override
	public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// JSP 경로 반환
		return "/WEB-INF/views/member/registerUserForm.jsp";
	}

}
View
  1. WEB-INF 폴더 오른쪽 클릭하고 새 폴더 views 생성 후 하위 폴더 main 생성하고 새 JSP 파일 main.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>메인</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/layout.css">
</head>
<body>
<div class="page-main">
	<jsp:include page="/WEB-INF/views/common/header.jsp"/>
	<div>
		<h3>메인 화면</h3>
	</div>
</div>
</body>
</html>
  1. views 폴더의 하위 폴더로 common 생성 후 새 JSP 파일 header.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!-- header 시작 -->
<h1 class="align-center"><a href="${pageContext.request.contextPath}/main/main.do">회원제 게시판</a></h1> <%-- 메뉴는 여러 파일에 include되기 때문에 상대 경로 대신 Context Path부터의 경로를 명시해야 함 --%>
<div class="align-right">
	<c:if test="${!empty user_num && !empty user_photo}"> <%-- EL의 empty 연산자는 객체가 null이거나 값이 비어 있는지 여부를 확인 --%>
		<img src="${pageContext.request.contextPath}/upload/${user_photo}" width="25" height="25" class="my-photo">
	</c:if>
	<c:if test="${!empty user_num && empty user_photo}">
		<img src="${pageContext.request.contextPath}/images/face.png" width="25" height="25" class="my-photo">
	</c:if>
	<a href="${pageContext.request.contextPath}/board/list.do">게시판</a>
	<c:if test="${!empty user_num}">
		[<span>${user_id}</span>]
		<a href="${pageContext.request.contextPath}/member/logout.do">로그아웃</a>
	</c:if>
	<c:if test="${empty user_num}">
		<a href="${pageContext.request.contextPath}/member/registerUserForm.do">회원 가입</a>
		<a href="${pageContext.request.contextPath}/member/loginForm.do">로그인</a>
	</c:if>
	<c:if test="${!empty user_num && user_auth == 2}">
		<a href="${pageContext.request.contextPath}/member/myPage.do">MY페이지</a>
	</c:if>
	<c:if test="${!empty user_num && user_auth == 3}">
		<a href="${pageContext.request.contextPath}/member/memberList.do">회원 관리</a>
	</c:if>
</div>
<!-- header 끝 -->
  1. common 폴더에 새 JSP 파일 ajax_view.jsp 생성
<%@ page language="java" contentType="text/plain; charset=UTF-8"
    pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
${ajaxData}
  1. views 폴더의 하위 폴더로 member 생성 후 새 JSP 파일 registerUserForm.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원 가입</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/layout.css">
<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
	$(function() {
		let idChecked = 0;
		
		// 아이디 중복 체크
		$('#id_check').click(function name() {
			if($('#id').val().trim()=='') {
				alert('아이디를 입력하세요!');
				$('#id').val('').focus();
				return;
			}
			
			$.ajax({
				url:'checkId.do',
				type:'post',
				data:{id:$('#id').val()},
				dataType:'json',
				cache:false,
				timeout:30000,
				success:function(param) {
					if(param.result=='idNotFound') {
						idChecked = 1;
						$('#message_id').css('color', '#000').text('아이디 사용 가능');
					}
					else if(param.result=='idDuplicated') {
						idChecked = 0;
						$('#message_id').css('color', 'red').text('아이디 중복');
						$('#id').val('').focus();
					}
					else {
						idChecked = 0;
						alert('아이디 중복 체크 오류 발생!');
					} 
				},
				error:function() {
					idChecked = 0;
					alert('네트워크 오류 발생!');
				}
			}); // end of ajax
			
		}); // end of click
		
		// 아이디 중복 안내 메시지 초기화 및 아이디 중복 여부 초기화
		$('#register_form #id').keyup(function() {
			idChecked = 0;
			$('#message_id').text('');
		});
		
		// 회원 정보 등록 유효성 체크
		$('#register_form').submit(function() {
			let isValid = true; // submit()의 return 값 지정
			
			$('li').each(function() {
				let input = $(this).find('input[type!="button"]'); // button이 아닌 <input> 태그들만 선택
				if(!input.val().trim()) {
					let word = $(this).find('label').text();
					let post = (word.charCodeAt(word.length-1) - ''.charCodeAt(0)) % 28 > 0 ? '' : '';
					alert(word + post + ' 입력하세요!');
					input.val('').focus();
					isValid = false; // submit()의 return 값 지정
					return false; // each() 루프 중단
				}
				
				if(input.attr('id')=='id' && idChecked==0) {
					alert('아이디 중복 체크 필수!');
					$('#id_check').focus();
					isValid = false; // submit()의 return 값 지정
					return false; // each() 루프 중단
				}
			}); // end of each
			
			return isValid;
		}); // end of submit
	});
</script>
</head>
<body>
<div class="page-main">
	<jsp:include page="/WEB-INF/views/common/header.jsp"/>
	<h2>회원 가입</h2>
	<form id="register_form" action="registerUser.do" method="post">
		<ul>
			<li>
				<label for="id">아이디</label>
				<input type="text" name="id" id="id" maxlength="12">
				<input type="button" value="아이디 중복 체크" id="id_check">
				<span id="message_id"></span>
			</li>
			<li>
				<label for="name">이름</label>
				<input type="text" name="name" id="name" maxlength="10">
			</li>
			<li>
				<label for="passwd">비밀번호</label>
				<input type="password" name="passwd" id="passwd" maxlength="12">
			</li>
			<li>
				<label for="phone">전화번호</label>
				<input type="text" name="phone" id="phone" maxlength="15">
			</li>
			<li>
				<label for="email">이메일</label>
				<input type="email" name="email" id="email" maxlength="50">
			</li>
			<li>
				<label for="zipcode">우편번호</label>
				<input type="text" name="zipcode" id="zipcode" maxlength="5">
				<input type="button" value="우편번호 찾기" onclick="sample2_execDaumPostcode()">
			</li>
			<li>
				<label for="address1">주소</label>
				<input type="text" name="address1" id="address1" maxlength="30">
			</li>
			<li>
				<label for="address2">나머지 주소</label>
				<input type="text" name="address2" id="address2" maxlength="30">
			</li>
		</ul>
		<div class="align-center">
			<input type="submit" value="등록">
			<input type="button" value="홈으로" onclick="location.href = '${pageContext.request.contextPath}/main/main.do';">
		</div>
	</form>
<!-- 우편번호 스크립트 시작 -->
<!-- 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 sample2_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>
</body>
</html>

다음으로