메인 화면
로그인
회원가입
회원가입 진행
회원가입 환영
아이디 찾기
아이디 찾기 진행
아이디 찾기 성공
비밀번호 변경
추천
어트랙션 월드컵
어트랙션 월드컵 진행
어트랙션 월드컵 결과
어트랙션 월드컵에서 어트랙션으로 이동
코스 월드컵
코스 추가
코스 삭제
MBTI별 추천
MBTI별 추천 결과
MBTI별 추천 추가
MBTI별 추천 수정
검색
검색 성공
검색 성공
검색 실패

프로젝트 개요

  • 구현 목표
    놀이공원을 방문하는 회원들을 위한 추천, 검색 등의 여러가지 기능을 제공하여 사이트 이용자의 편의성과 흥미를 높이도록 한다.
  • 활동 내용
    • 1. 로그인, 로그아웃, 회원가입, 회원가입 환영, 아이디 찾기, 비밀번호 변경 구현
    • 2. 코스 추가/삭제, MBTI별 추천 리스트 및 상세보기, MBTI별 추천 추가/수정/삭제, 추천, 어트랙션 월드컵, 코스 월드컵 구현
    • 3. 검색 기능 구현
    • 4. Daum Address API 적용
  • Category
  • Servlet/JSP 프로젝트
  • Period
  • 2023.10.30.~2023.11.17.
  • GitHub
  • https://github.com/Isaac-Seungwon/dd-studio.git

Login.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.user.model.UserDTO;
import com.ddstudio.user.repository.UserDAO;

/**
 * 사용자 로그인을 처리하는 서블릿 클래스입니다.
 * 사용자가 입력한 이메일과 비밀번호 정보를 확인하여 로그인 여부를 판단하고, 로그인에 성공하면 세션에 사용자 정보를 저장한 후 메인 페이지로 이동합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/login.do")
public class Login extends HttpServlet {

	/**
	 * 로그인 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 로그인 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/user/login.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * POST 메서드로 전송된 로그인 요청을 처리합니다.
	 * 사용자가 입력한 이메일과 비밀번호를 확인하여 로그인 여부를 판단하고, 로그인에 성공하면 세션에 사용자 정보를 저장한 후 메인 페이지로 이동합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		String email = req.getParameter("email"); // 이메일 (아이디)
		String pw = req.getParameter("pw"); // 비밀번호

		UserDAO dao = new UserDAO();
		UserDTO dto = new UserDTO();

		dto.setEmail(email);
		dto.setPw(pw);

		// 로그인
		UserDTO result = dao.login(dto);

		if (result != null) {
			// 로그인 성공
			// 인증 티켓 발급 (세션에 정보 저장)
			req.getSession().setAttribute("email", email);
			req.getSession().setAttribute("seq", result.getUser_seq());
			req.getSession().setAttribute("name", result.getName());
			req.getSession().setAttribute("lv", result.getLv());

			resp.sendRedirect("/ddstudio/index.do");

		} else {
			// 로그인 실패
			PrintWriter writer = resp.getWriter();
			writer.print("<script>alert('failed');history.back();</script>");
			writer.close();
		}
	}
}

Logout.java

package com.ddstudio.user;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 사용자 로그아웃을 처리하는 서블릿 클래스입니다.
 * 사용자의 세션에서 인증 티켓을 제거하고, 메인 페이지로 이동합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/logout.do")
public class Logout extends HttpServlet {

	/**
	 * 로그아웃을 처리하는 GET 메서드입니다.
	 * 사용자의 세션에서 인증 티켓을 제거하고, 메인 페이지로 이동합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		// 인증 티켓 제거
		req.getSession().removeAttribute("email");
		req.getSession().removeAttribute("seq");
		req.getSession().removeAttribute("name");
		req.getSession().removeAttribute("lv");
		
		resp.sendRedirect("/ddstudio/index.do");
	}

}

UserDTO.java

package com.ddstudio.user.model;

import lombok.Data;

/**
 * 사용자 정보를 담는 데이터 전송 객체(DTO)입니다.
 * 사용자의 기본 정보 및 회원 가입/로그인과 관련된 속성들을 관리합니다.
 * 
 * @author 이승원
 */

@Data
public class UserDTO {
	
	private String user_seq; // 사용자 일련번호
	private String name; // 사용자 이름
	private String email; // 사용자 이메일 (아이디로 사용)
	private String pw; // 사용자 비밀번호
	private String tel; // 사용자 연락처
	private String address; // 사용자 주소
	private String birth; // 사용자 생년월일
	private String lv; // 사용자 등급
	private String ing; // 사용자 활동 상태 (활동 여부)'
	
}

UserDAO.java

package com.ddstudio.user.repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;

import com.ddstudio.DBUtil;
import com.ddstudio.user.model.SearchDTO;
import com.ddstudio.user.model.UserDTO;

/**
 * 사용자 정보를 데이터베이스에서 조회 및 수정하는 클래스
 * 
 * @author 이승원
 */
public class UserDAO {

	private Connection conn;
	private Statement stat;
	private PreparedStatement pstat;
	private ResultSet rs;

	/**
	 * 생성자: 데이터베이스 연결
	 */
	public UserDAO() {
		this.conn = DBUtil.open();
	}

	/**
	 * 로그인
	 *
	 * @param dto 사용자 정보를 담은 UserDTO 객체
	 * @return 로그인 성공 시 UserDTO 객체, 실패 시 null
	 */
	public UserDTO login(UserDTO dto) {

		try {

			String sql = "select * from tblUser where email = ? and pw = ? and ing = 'Y'";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getEmail());
			pstat.setString(2, dto.getPw());

			rs = pstat.executeQuery();

			if (rs.next()) {

				UserDTO result = new UserDTO();

				result.setEmail(rs.getString("email"));
				result.setUser_seq(rs.getString("user_seq"));
				result.setName(rs.getString("name"));
				result.setLv(rs.getString("lv"));

				return result;
			}

		} catch (Exception e) {
			System.out.println("UserDao.login()");
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * 회원 가입
	 *
	 * @param dto 사용자 정보를 담은 UserDTO 객체
	 * @return 회원 가입 성공 시 1, 실패 시 0
	 */
	public int register(UserDTO dto) {

		try {
			String sql = "insert into tblUser (user_seq, name, email, pw, tel, address, birth, lv, ing) values (seqtblUser.nextVal, ?, ?, ?, ?, ?, TO_DATE(?, 'yyyy-mm-dd'), '1', 'Y')";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getName());
			pstat.setString(2, dto.getEmail());
			pstat.setString(3, dto.getPw());
			pstat.setString(4, dto.getTel());
			pstat.setString(5, dto.getAddress());
			pstat.setString(6, dto.getBirth());

			return pstat.executeUpdate();

		} catch (Exception e) {
			System.out.println("UserDAO.register()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	 * 이메일 중복 검사
	 *
	 * @param email 검사할 이메일
	 * @return 중복된 이메일 수
	 */
	public int check(String email) {

		try {

			String sql = "select count(*) as cnt from tblUser where email = ?";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, email);

			rs = pstat.executeQuery();

			if (rs.next()) {
				return rs.getInt("cnt");
			}

		} catch (Exception e) {
			System.out.println("UserDAO.check()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	 * 아이디 찾기
	 *
	 * @param dto 사용자 정보를 담은 UserDTO 객체
	 * @return 찾은 이메일 정보를 담은 UserDTO 객체
	 */
	public UserDTO findId(UserDTO dto) {

		try {

			String sql = "select email from tblUser where name = ? and tel = ?";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getName());
			pstat.setString(2, dto.getTel());

			rs = pstat.executeQuery();

			if (rs.next()) {

				UserDTO result = new UserDTO();

				result.setEmail(rs.getString("email"));

				return result;
			}

		} catch (Exception e) {
			System.out.println("UserDao.findId()");
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * 비밀번호 변경 전 계정 존재 여부 확인
	 *
	 * @param dto 사용자 정보를 담은 UserDTO 객체
	 * @return 계정 존재 여부를 나타내는 값 (존재 시 1, 미존재 시 0)
	 */
	public int isFindPw(UserDTO dto) {
		try {
			String sql = "select count(*) as cnt from tblUser where email = ? and tel = ?";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getEmail());
			pstat.setString(2, dto.getTel());

			rs = pstat.executeQuery();

			if (rs.next()) {
				return rs.getInt("cnt");
			}

		} catch (Exception e) {
			System.out.println("UserDao.isFindPw()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	 * 비밀번호 변경
	 *
	 * @param dto 사용자 정보를 담은 UserDTO 객체
	 * @return 비밀번호 변경 성공 시 1, 실패 시 0
	 */
	public int changePw(UserDTO dto) {

		try {

			String sql = "update tblUser set pw = ? where email = ?";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getPw());
			pstat.setString(2, dto.getEmail());

			return pstat.executeUpdate();

		} catch (Exception e) {
			System.out.println("UserDao.changePw()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	 * 검색
	 *
	 * @param searchWord 검색어
	 * @return 검색 결과를 담은 SearchDTO 객체의 리스트
	 */
	public ArrayList<SearchDTO> search(String searchWord) {
		ArrayList<SearchDTO> searchResult = new ArrayList<>();

		try {
			String sql = "SELECT * FROM vwSearch WHERE " + "ATTRACTION_NAME LIKE ? OR " + "MBTI_RESULT LIKE ? OR "
					+ "MBTI_MBTI LIKE ? OR " + "COURSE_NAME LIKE ? OR " + "HASHTAG_NAME LIKE ? OR "
					+ "RESTAURANT_NAME LIKE ? OR " + "RESTAURANT_MENU LIKE ? OR " + "CATEGORY_NAME LIKE ? OR "
					+ "SHOP_NAME LIKE ? OR " + "SHOP_INFO LIKE ? OR " + "ITEM_NAME LIKE ? OR " + "ITEM_INFO LIKE ? OR "
					+ "CONVENIENT_NAME LIKE ? OR " + "FESTIVAL_NAME LIKE ? OR " + "FESTIVAL_INFO LIKE ? OR "
					+ "THEATER_NAME LIKE ? OR " + "MOVIE_NAME LIKE ? OR " + "NOTICE_SUBJECT LIKE ? OR "
					+ "NOTICE_CONTENT LIKE ? OR " + "BENEFIT_NAME LIKE ? OR " + "BENEFIT_TYPE LIKE ? OR "
					+ "FAQ_CATEGORY LIKE ? OR " + "FAQ_QUESTION LIKE ? OR " + "FAQ_ANSWER LIKE ?";

			pstat = conn.prepareStatement(sql);

			// 모든 컬럼에 대해 검색
			for (int i = 1; i <= 24; i++) {
				pstat.setString(i, "%" + searchWord + "%");
			}

			rs = pstat.executeQuery();

			while (rs.next()) {
				SearchDTO result = new SearchDTO();

				result.setAttractionName(rs.getString("ATTRACTION_NAME"));
				result.setMbtiResult(rs.getString("MBTI_RESULT"));
				result.setMbtiMbti(rs.getString("MBTI_MBTI"));
				result.setCourseName(rs.getString("COURSE_NAME"));
				result.setHashtagName(rs.getString("HASHTAG_NAME"));
				result.setRestaurantName(rs.getString("RESTAURANT_NAME"));
				result.setRestaurantMenu(rs.getString("RESTAURANT_MENU"));
				result.setCategoryName(rs.getString("CATEGORY_NAME"));
				result.setShopName(rs.getString("SHOP_NAME"));
				result.setShopInfo(rs.getString("SHOP_INFO"));
				result.setItemName(rs.getString("ITEM_NAME"));
				result.setItemInfo(rs.getString("ITEM_INFO"));
				result.setConvenientName(rs.getString("CONVENIENT_NAME"));
				result.setFestivalName(rs.getString("FESTIVAL_NAME"));
				result.setFestivalInfo(rs.getString("FESTIVAL_INFO"));
				result.setTheaterName(rs.getString("THEATER_NAME"));
				result.setMovieName(rs.getString("MOVIE_NAME"));
				result.setNoticeSubject(rs.getString("NOTICE_SUBJECT"));
				result.setNoticeContent(rs.getString("NOTICE_CONTENT"));
				result.setBenefitName(rs.getString("BENEFIT_NAME"));
				result.setBenefitType(rs.getString("BENEFIT_TYPE"));
				result.setFaqCategory(rs.getString("FAQ_CATEGORY"));
				result.setFaqQuestion(rs.getString("FAQ_QUESTION"));
				result.setFaqAnswer(rs.getString("FAQ_ANSWER"));

				// System.out.println("ATTRACTION_NAME: " + rs.getString("ATTRACTION_NAME"));

				searchResult.add(result);
			}
		} catch (Exception e) {
			System.out.println("UserDAO.search()");
			e.printStackTrace();
		}

		return searchResult;
	}

	/**
	 * 해시태그 목록 조회
	 *
	 * @return 해시태그 목록을 담은 문자열의 리스트
	 */
	public ArrayList<String> getHashtagList() {
		ArrayList<String> hashtagList = new ArrayList<>();

		try {
			String query = "SELECT NAME FROM tblHashtag";
			stat = conn.createStatement();
			rs = stat.executeQuery(query);

			while (rs.next()) {
				String hashtagName = rs.getString("NAME");

				hashtagList.add(hashtagName);
			}
		} catch (Exception e) {
			System.out.println("UserDAO.getHashtagList()");
			e.printStackTrace();
		}

		return hashtagList;
	}

}

login.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#auto-login {
	display: flex;
	justify-content: space-between;
}

#auto-login .button {
	width: 100px;
}

h2 small {
	font-size: 80%;
}

.button-container {
	margin-top: 0;
    display: flex;
    justify-content: space-between;
}

.button {
	width: 183px;
	margin-left: 0;
	font-size: 16px;
	border: 1px solid #ccc;
    border-radius: 5px;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
}
 
.round-button {
    background: #007bff;
    background: linear-gradient(to right, #6bC2ff, #007bff);
    color: #fff;
    border: none;
    font-size: 16px;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
    transition: background 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}

.round-button:hover {
    /* background: #0056b3; */
    color: #fff;
    box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
}

#main {
	margin-top: 100px;
}

#main > .sub-title {
	border-top: 0;
	margin-top: 0;
}

.sub-title > p {
	margin-bottom: 0;    
	font-size: 20px;
    font-weight: 800;
}

#title {
	margin-top: 123px;
	background-image: url('/ddstudio/asset/image/background-8.jpg');
}

#title > h2 {
	color: white;
}
</style>
</head>
<body>
	<!-- login.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">

		<div id="title" title="작가 wirestock 출처 Freepik">
			<h2>로그인</h2>
		</div>

		<div class="sub-title">
			<p>회원 이메일과 비밀번호로 로그인하세요.</p>
		</div>

		<div id="content">
			<div class="wide-item">

				<form method="POST" action="/ddstudio/user/login.do">
					<table>
						<tr>
							<th><input type="text" name="email" id="email" required class="middle-high" placeholder="이메일"></th>
						</tr>
						<tr>
							<th><input type="password" name="pw" id="pw" required class="middle-high" placeholder="비밀번호"></th>
							<td><button class="login round-button" id="login check" onclick="location.href='/ddstudio/user/login.do';">로그인</button></td>
						</tr>
						<tr>
							<td>
								<div class="button-container">
									<button type="button" class="button" onclick="location.href='/ddstudio/user/findid.do';">아이디 찾기</button>
									<button type="button" class="button" onclick="location.href='/ddstudio/user/changepw.do';">비밀번호 변경</button>
								</div>
							</td>
						</tr>
					</table>
				</form>
			</div>
			
			<!-- 자동 로그인 시작 -->
			<!-- 
			<hr>
			
			<h2>자동 로그인 <small>(관리자용)</small></h2>
			<div id="auto-login">
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="admin@naver.com">
					<input type="hidden" name="pw" value="admin1111!">
					<button type="submit" class="login button">관리자</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="park@naver.com">
					<input type="hidden" name="pw" value="park1111!">
					<button type="submit" class="login button">박나래</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="hwnag@kakao.com">
					<input type="hidden" name="pw" value="hwang1111!">
					<button type="submit" class="login button">황주원</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="kim@kakao.com">
					<input type="hidden" name="pw" value="kim1111!">
					<button type="submit" class="login button">김형우</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="cha@daum.net">
					<input type="hidden" name="pw" value="cha1111!">
					<button type="submit" class="login button">차민재</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="lee@kakao.com">
					<input type="hidden" name="pw" value="lee1111!">
					<button type="submit" class="login button">이정은</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="cha@msn.com">
					<input type="hidden" name="pw" value="cha1111!">
					<button type="submit" class="login button">차수민</button>
				</form>
				<form method="POST" action="/ddstudio/user/login.do">
					<input type="hidden" name="email" value="lee@naver.com">
					<input type="hidden" name="pw" value="lee1111!">
					<button type="submit" class="login button">이승원</button>
				</form>
			</div>
			-->
			<!-- 자동 로그인 끝 -->
		</div>
	</main>
	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
</body>
</html>

Register.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.user.model.UserDTO;
import com.ddstudio.user.repository.UserDAO;

/**
 * 회원가입을 처리하는 서블릿 클래스입니다.
 * 회원 정보를 입력받아 등록하고, 성공 시 회원가입 환영 페이지로 이동합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/register.do")
public class Register extends HttpServlet {

	/**
	 * 회원가입 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 회원가입 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/user/register.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * 회원 정보를 입력받아 등록하는 POST 메서드입니다.
	 * 회원 등록 성공 시 회원가입 환영 페이지로 이동하고, 실패 시 에러 알림창을 띄웁니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		try {
			// 회원 정보
			String email = req.getParameter("email");
			String pw = req.getParameter("pw");
			String name = req.getParameter("name");
			String birth = req.getParameter("birth");
			String tel = req.getParameter("tel");
			String address;
	        
	        // 주소 처리
	        if (req.getParameter("post-code").trim().isEmpty()) {
		        address = "default"; //주소를 입력하지 않은 경우 (미기재)
	        } else {
		        String postCode = req.getParameter("post-code").trim();
		        String addressBasis = req.getParameter("address-basis").trim();
		        String addressDetail = req.getParameter("address-detail").trim();
		        address = postCode + " " + addressBasis + " " + addressDetail;
	        }
	        
			UserDTO dto = new UserDTO();

			dto.setEmail(email);
			dto.setPw(pw);
			dto.setName(name);
			dto.setBirth(birth);
			dto.setTel(tel);
			dto.setAddress(address);
			
			UserDAO dao = new UserDAO();

			// 회원 등록
			int result = dao.register(dto);

			// 회원 등록 성공 시 환영 페이지로 이동
			if (result == 1) {
				resp.sendRedirect("/ddstudio/user/registerwelcome.do");
				return;
			}

		} catch (Exception e) {
			System.out.println("Register.doPost()");
			e.printStackTrace();
		}

		// 0 또는 에러
		PrintWriter writer = resp.getWriter();
		writer.print("<script>alert('failed');history.back();</script>");
		writer.close();
	}
}

register.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#cancel {
	margin-right: 8px;
}

#duplicate-check {
	padding: 0;
}

.sub-title > p {
	font-size: 20px;
    font-weight: 800;
}

.button {
    border: none;
    border-radius: 5px;
    font-size: 16px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
    transition: background 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}

table th {
	padding: 0 10px;
}

td > div > input {
	background: transparent;
    border-bottom: 1.5px solid #cecece;
}
</style>
</head>
<body>
	<!-- register.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>회원가입</h1>

		<div class="sub-title">
			<p>회원정보입력</p>
		</div>

		<div id="content">
			<div class="wide-item">
				<form method="POST" action="/ddstudio/user/register.do">
					<table>
						<!-- 이메일 필드와 에러 메시지 -->
						<tr>
							<th class="required">이메일 (아이디)</th>
							<td>
								<div class="email">
									<input type="text" name="email" id="email" required class="middle-flat">
									<input type="button" id="duplicate-check" class="button check" disabled value="중복검사"></input>
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="email-error" class="error-message" style="display:none;"></div>
						        <span id="duplicate-check-message" class="error-message" style="display:none;"></span>
						    </td>
						</tr>
						<!-- 비밀번호 필드와 에러 메시지 -->
						<tr>
							<th class="required">비밀번호</th>
							<td>
								<div>
									<input type="password" name="pw" id="pw" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="pw-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						<!-- 비밀번호 확인 필드와 에러 메시지 -->
						<tr>
							<th class="required">비밀번호 확인</th>
							<td>
								<div>
									<input type="password" name="pwok" id="pwok" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="pw-confirm-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						<!-- 이름 필드와 에러 메시지 -->
						<tr>
							<th class="required">이름</th>
							<td>
								<div>
									<input type="text" name="name" id="name" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="name-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						<!-- 생년월일 필드와 에러 메시지 -->
						<tr>
							<th class="required">생년월일</th>
							<td>
								<div>
									<input type="date" name="birth" id="birth" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="birth-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						 <!-- 연락처 필드와 에러 메시지 -->
						<tr>
							<th class="required">연락처</th>
							<td>
								<div>
									<input type="text" name="tel" id="tel" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="tel-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						<!-- 주소 필드 -->
						<tr>
							<th>주소</th>
							<td>
								<div class="address">
									<input type="text" name="post-code" id="post-code" class="middle-flat" placeholder="우편번호">
									<button type="button" class="button check" onclick="execDaumPostcode()">우편번호 검색</button>
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="address">
									<input type="text" name="address-basis" id="address-basis" class="middle-flat" placeholder="기본주소">
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="address">
									<input type="text" name="address-detail" id="address-detail" class="middle-flat" placeholder="상세주소">
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<!-- validateAndSubmit 함수로 가입 버튼 클릭 시 유효성 검사 -->
									<!-- <div id="ok-message"></div> -->
									<button type="submit" id="join" class="check button" disabled>가입</button>
									<button type="button" id="cancel" class="button" onclick="location.href='/ddstudio/index.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>
			</div>
		</div>
	</main>
	
	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
		
	<script>
		let isValid = [false, false, false, false, false, false, false]; // 검사 결과 저장
		
		/* 이메일 (아이디) 중복 검사 */
		$('#duplicate-check').click(function(){
			$.ajax({
				type: 'POST',
				url: '/ddstudio/user/duplicatecheck.do',
				data: {
					email: $('#email').val()
				},
				dataType: 'json',
				success: function(result) {
					//alert(result.message); //사용 가능(0) 사용중(1)
					
					if (result.message == 0) {
						//console.log("true");
					    $('#duplicate-check-message').css('display', 'block');
						$('#duplicate-check-message').text('사용 가능한 아이디입니다.');
						$('#join').prop('disalbed', false);
						
						isValid[1] = true;
					}
					else {
						//console.log("false");
					    $('#duplicate-check-message').css('display', 'block');
						$('#duplicate-check-message').text('이미 사용중인 아이디입니다.');
						$('#join').prop('disalbed', true);
						
						isValid[1] = false;
					}
				},
				errors: function(a,b,c) {
					console.log(a,b,c);
				}
			});
		});
	
		// 이메일(아이디)를 수정하면 가입 불가능
		$('#email').change(function() {
			$('#join').prop('disabled', true);
		});
		
		document.addEventListener("DOMContentLoaded", function () {
			
			const joinButton = document.getElementById("join");
	
			function updateButtonStatus() {
		        const isAllValid = isValid.every((value) => value);
	
		        if (isAllValid) {
		            joinButton.removeAttribute("disabled");
		            //document.getElementById("ok-message").textContent = "true";
		        } else {
		            joinButton.setAttribute("disabled", "disabled");
		            //document.getElementById("ok-message").textContent = "false";
		        }
		    }
		    
		    /* 이메일 유효성 검사 */
		    const emailField = document.getElementById("email");
		    const emailErrorDiv = document.getElementById("email-error");
		    const emailRegex = /^[a-z0-9._%+-]{1,20}\@[a-z0-9.-]{1,8}\.[a-z]{1,5}$/;

		    const duplicateCheck = document.getElementById("duplicate-check");
	        
			emailField.addEventListener("input", function () {
		    	isValid[0] = emailRegex.test(emailField.value);
		    	isValid[1] = false;
		    	
		        emailErrorDiv.textContent = isValid[0] ? "" : "올바른 이메일 형식을 입력하세요. (예: park@email.com)";
		        emailErrorDiv.style.display = isValid[0] ? "none" : "block";

		        $('#duplicate-check-message').css('display', 'none');
		        
		        if (emailField.value.length === 0) {
		        	emailErrorDiv.textContent = "";
		        	emailErrorDiv.style.display = "none";
		        }
	
		        if (isValid[0]) {
		        	duplicateCheck.removeAttribute("disabled");
		        } else {
		        	duplicateCheck.setAttribute("disabled", "disabled");
		        }
	            
		        updateButtonStatus();
		    });
	        
		    /* 비밀번호 유효성 검사 */
		    const passwordField = document.getElementById("pw");
		    const passwordErrorDiv = document.getElementById("pw-error");
		    const passwordConfirmField = document.getElementById("pwok");
		    const passwordConfirmErrorDiv = document.getElementById("pw-confirm-error");
		    const passwordRegex = /^(?=.*[0-9])(?=.*[A-Za-z])(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,15}$/
	        
		    passwordField.addEventListener("input", function () {
		    	isValid[2] = passwordRegex.test(passwordField.value);
		    	isValid[3] = passwordConfirmField.value === passwordField.value;
	
		        passwordErrorDiv.textContent = isValid[2] ? "" : "8-15자의 영문/숫자/특수문자를 함께 입력하세요.";
		        passwordErrorDiv.style.display = isValid[2] ? "none" : "block";
		        
		        passwordConfirmErrorDiv.textContent = isValid[3] ? "" : "비밀번호가 동일하지 않습니다.";
		        passwordConfirmErrorDiv.style.display = isValid[3] ? "none" : "block";
	
		        if (passwordField.value.length === 0) {
		            passwordErrorDiv.textContent = "";
		            passwordErrorDiv.style.display = "none";
		        }
	
		        if (passwordConfirmField.value.length === 0) {
		            passwordConfirmErrorDiv.textContent = "";
		            passwordConfirmErrorDiv.style.display = "none";
		        }
	
		        updateButtonStatus();
		    });
	
		    /* 비밀번호 확인 유효성 검사 */
		    passwordConfirmField.addEventListener("input", function () {
		    	isValid[3] = passwordConfirmField.value === passwordField.value;
		        
		        passwordConfirmErrorDiv.textContent = isValid[3] ? "" : "비밀번호가 동일하지 않습니다.";
		        passwordConfirmErrorDiv.style.display = isValid[3] ? "none" : "block";
		        
		        if (passwordConfirmField.value.length === 0) {
		            passwordConfirmErrorDiv.textContent = "";
		            passwordConfirmErrorDiv.style.display = "none";
		        }
	
		        updateButtonStatus();
		    });
		    
		    /* 이름 유효성 검사 */
		    const nameField = document.getElementById("name");
		    const nameErrorDiv = document.getElementById("name-error");
		    const nameRegex = /^[가-힣]{2,6}$/; // 2글자에서 6글자의 한글 이름만 허용
	
		    nameField.addEventListener("input", function () {
		    	isValid[4] = nameRegex.test(nameField.value);
		        
		        nameErrorDiv.textContent = isValid[4] ? "" : "2-6자의 한글 이름을 입력하세요.";
		        nameErrorDiv.style.display = isValid[4] ? "none" : "block";
	
		        if (nameField.value.length === 0) {
		            nameErrorDiv.textContent = "";
		            nameErrorDiv.style.display = "none";
		        }
	
		        updateButtonStatus();
		    });
	        
		    /* 생년월일 유효성 검사 */
		    const birthField = document.getElementById("birth");
		    const birthErrorDiv = document.getElementById("birth-error");
		    const birthRegex = /^(19|20)\d\d-[0-1]\d-[0-3]\d$/; // YYYY-MM-DD 형식
	
		    birthField.addEventListener("input", function () {
		    	isValid[5] = birthRegex.test(birthField.value);
	
		        birthErrorDiv.textContent = isValid[5] ? "" : "올바른 생년월일 형식을 입력하세요. (예: YYYY-MM-DD)";
		        birthErrorDiv.style.display = isValid[5] ? "none" : "block";

		        if (birthField.value.length === 0) {
		            birthErrorDiv.textContent = "";
		            birthErrorDiv.style.display = "none";
		        }
	
		        updateButtonStatus();
		    });
		    
		    /* 연락처 유효성 검사 */
		    const telField = document.getElementById("tel");
		    const telErrorDiv = document.getElementById("tel-error");
		    const telRegex = /^010-[0-9]{4}-[0-9]{4}$/; // 010-XXXX-XXXX 형식의 전화번호
	
		    telField.addEventListener("input", function () {
		    	isValid[6] = telRegex.test(telField.value);
	
		        telErrorDiv.textContent = isValid[6] ? "" : "올바른 전화번호 형식을 입력하세요. (예: 010-XXXX-XXXX)";
		        telErrorDiv.style.display = isValid[6] ? "none" : "block";
	
		        if (telField.value.length === 0) {
		            telErrorDiv.textContent = "";
		            telErrorDiv.style.display = "none";
		        }
		        
		        updateButtonStatus();
		    });
	        
		    const cancelButton = document.getElementById("cancel");
	
		    const postCodeField = document.getElementById("post-code");
		    const addressBasisField = document.getElementById("address-basis");
		    const addressDetailField = document.getElementById("address-detail");
		    
		    cancelButton.addEventListener("click", function () {
		        emailField.value = "";
		        passwordField.value = "";
		        passwordConfirmField.value = "";
		        nameField.value = "";
		        birthField.value = "";
		        telField.value = "";
		        postCodeField.value = "";
		        addressBasisField.value = "";
		        addressDetailField.value = "";
		        $('#duplicate-check-message').css('display', 'none');
	
		        emailErrorDiv.style.display = "none";
		        passwordErrorDiv.style.display = "none";
		        passwordConfirmErrorDiv.style.display = "none";
		        nameErrorDiv.style.display = "none";
		        birthErrorDiv.style.display = "none";
		        telErrorDiv.style.display = "none";
		        
		        isValid = [false, false, false, false, false, false, false];
		        updateButtonStatus();
		    });
		});
	</script>
	
	<!-- 주소 입력 폼 -->
	<script>
		function execDaumPostcode() {
			new daum.Postcode({
				oncomplete : function(data) {
					var addr = '';
					var extraAddr = '';

					if (data.userSelectedType === 'R') { // 도로명 주소 선택
						addr = data.roadAddress;
					} else { // 지번 주소 선택
						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 + ')';
						}
					} else {
						document.getElementById("address-basis").value = '';
					}

					// 우편번호와 주소 정보를 input 박스에 삽입
					document.getElementById("post-code").value = data.zonecode;
					document.getElementById("address-basis").value = addr;
					document.getElementById("address-basis").value += extraAddr;
					document.getElementById("address-detail").focus(); // 상세주소로 포커스 이동
				}
			}).open();
		}
	</script>
</body>
</html>

FindId.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONObject;

import com.ddstudio.user.model.UserDTO;
import com.ddstudio.user.repository.UserDAO;

/**
 * 아이디 찾기 기능을 담당하는 서블릿 클래스입니다.
 * 사용자가 입력한 이름과 전화번호 정보를 처리하여 해당 회원의 이메일을 찾아서 전달합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/findid.do")
public class FindId extends HttpServlet {

	/**
	 * 아이디 찾기 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 아이디 찾기 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/user/find-id.jsp");
		dispatcher.forward(req, resp);
	}
	
	/**
	 * POST 메서드로 전송된 요청을 처리하여 아이디를 찾아서 전달합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		req.setCharacterEncoding("UTF-8");
		
		String name = req.getParameter("name"); // 이름
		String tel = req.getParameter("tel"); // 전화번호

		UserDAO dao = new UserDAO();
		UserDTO dto = new UserDTO();
		
		dto.setName(name);
		dto.setTel(tel);
		
		// 입력한 정보로 아이디 찾기
		UserDTO result = dao.findId(dto);

		resp.setContentType("application/json");
		PrintWriter writer = resp.getWriter();

		// 회원 정보가 있을 경우 아이디 전달
		JSONObject obj = new JSONObject();
		if (result != null) {
			obj.put("email", result.getEmail());
		} else {
			obj.put("email", null);
		}

		writer.write(obj.toString());
		writer.close();
	}
}

ChangePw.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.user.model.UserDTO;
import com.ddstudio.user.repository.UserDAO;

/**
 * 비밀번호 변경 기능을 담당하는 서블릿 클래스입니다.
 * 사용자가 입력한 이메일, 연락처, 비밀번호 정보를 처리하여 인증번호 확인 및 비밀번호 변경을 수행합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/changepw.do")
public class ChangePw extends HttpServlet {

	/**
	 * 비밀번호 변경 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 비밀번호 변경 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/user/change-pw.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * POST 메서드로 전송된 요청을 처리하여 인증번호 확인 및 비밀번호 변경을 수행합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		req.setCharacterEncoding("UTF-8");
		resp.setContentType("application/json");
		resp.setCharacterEncoding("UTF-8");
		
		PrintWriter writer = resp.getWriter();
		
		String email = req.getParameter("email"); // 아이디 (이메일)
		String tel = req.getParameter("tel"); // 연락처
		String pw = req.getParameter("pw"); // 비밀번호

		UserDAO dao = new UserDAO();
		UserDTO dto = new UserDTO();
		
		if (pw == null || pw.trim().isEmpty()) {
			// 인증번호 확인
			dto.setEmail(email);
			dto.setTel(tel);
			
			int cnt = dao.isFindPw(dto);
		
			writer.printf("{ \"cnt\": %d }", cnt);
		} else {
			// 비밀번호 변경
			dto.setEmail(email);
			dto.setPw(pw);
			
			int message = dao.changePw(dto);
			
			writer.printf("{ \"message\": %d }", message);
		}
		
		writer.close();
	}
	
}

DuplicateCheck.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.user.repository.UserDAO;

/**
 * 이메일 중복 검사 기능을 수행하는 서블릿 클래스입니다.
 * 사용자가 입력한 이메일이 이미 등록되어 있는지 확인합니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/duplicatecheck.do")
public class DuplicateCheck extends HttpServlet {

	/**
	 * POST 메서드로 전송된 요청을 처리하여 이메일 중복 여부를 검사합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		String email = req.getParameter("email"); // 아이디 (이메일)
		
		UserDAO dao = new UserDAO();
		
		// 이메일 중복 검사
		int message = dao.check(email);
		
		resp.setContentType("application/json");
		resp.setCharacterEncoding("UTF-8");
		
		PrintWriter writer = resp.getWriter();
	
		// 중복 여부 메시지
		writer.printf("{ \"message\": %d }", message);
		writer.close();
	}
}

find-id.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#check #myid {
	margin-right: 0 !important;
}

#cancel {
	margin-right: 40px;
}

#acceptok {
	margin-right: 25px;
}

#valid > tbody > tr:nth-child(5) > td > div > input {
	margin-left: -12px;
}

.sub-title > p {
	font-size: 20px;
    font-weight: 800;
}

.button {
    border: none;
    border-radius: 5px;
    font-size: 16px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
    transition: background 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}

/*
table {
	border-left: 2px solid #d1d1d1;
    border-right: 2px solid #d1d1d1;
    border-radius: 20px;
    border-collapse: separate;
	background: transparent;
}
*/

table th {
	padding: 0 10px;
}

td > div > input {
	background: transparent;
    border-bottom: 1.5px solid #cecece;
}
</style>
</head>
<body>
	<!-- find-id.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>아이디 찾기</h1>

		<div class="sub-title">
			<p>회원정보입력</p>
		</div>

		<div id="content">
			<div class="wide-item">
				<form method="POST" action="/ddstudio/user/findid.do">
					<table id="valid">
						<!-- 이름 필드와 에러 메시지 -->
						<tr>
							<th class="required">이름</th>
							<td>
								<div>
									<input type="text" name="name" id="name" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div id="name-error" class="error-message" style="display: none;"></div>
							</td>
						</tr>
						<!-- 연락처 필드와 에러 메시지 -->
						<tr>
							<th class="required">연락처</th>
							<td>
								<div>
									<input type="text" name="tel" id="tel" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div id="tel-error" class="error-message" style="display: none;"></div>
							</td>
						</tr>

						<tr>
							<th class="required">인증번호</th>
							<td>
								<div class="certification">
									<input type="text" name="authcode" placeholder="6자리 숫자" class="certificationNumber" autocomplete="off">
									<span class="certificationTime">03:00</span>
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="acceptreq" class="acceptreq check button" disabled>요청</button>
									<button type="button" id="acceptok" class="check button" disabled>확인</button>
								</div>
							</td>
						</tr>
					</table>

					<div class="sub-title">
						<p>본인 이메일 (아이디) 확인</p>
					</div>
					<table id="check">
						<!-- 본인 이메일 확인 필드 -->
						<tr>
							<th>이메일</th>
							<td>
								<div>
									<input type="text" name="myid" id="myid" class="middle-flat" style="pointer-events: none;">
								</div>
							</td>
						</tr>

						<tr>
							<td colspan="2">
								<div class="button-container">
									<button type="button" id="cancel" class="button" onclick="location.href='/ddstudio/user/login.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>
			</div>
		</div>
	</main>

	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
	<script>
	    let countTime = 0; //타이머 초기값
	    let intervalCall; //타이머 식별
	    let authCode; //인증 코드
	    let popupLayer; //팝업 레이어
	
	    //타이머 시작
	    $.time = function (time) {
	        countTime = time; //시간 초기화
	        clearInterval(intervalCall); // 이전 타이머 중단
	        intervalCall = setInterval(alertFunc, 1000);
	    }
	
	    //타이머 중단
	    $.closeTime = function () {
	        clearInterval(intervalCall);
	    }
	
	    //타이머 호출
	    function alertFunc() {
	        let min = Math.floor(countTime / 60);
	        let sec = countTime - (60 * min);
	        
	        //타이머 출력
	        if (sec > 9) {
	            $('.certificationTime').text(min + ':' + sec + '');
	        } else {
	            $('.certificationTime').text(min + ':0' + sec + '');
	        }
	        
	        /* 인증번호 시간이 만료되면 재요청 필요 */
	        if (countTime <= 0) {
	            clearInterval(intervalCall); //타이머 중단
	            $('#acceptok').attr('disabled', 'disabled'); //확인 버튼 비활성화
	        }
	        
	        countTime--;
	    };
	
	    //인증번호 요청
	    $('.acceptreq').on("click", function () {
	        authCode = Math.floor(100000 + Math.random() * 900000); //랜덤한 6자리

	        // 기존에 열린 팝업이 있다면 닫기
	        if (popupLayer) {
	            popupLayer.remove();
	        }
	        
	        showPopup(authCode); //팝업으로 생성된 코드 출력
	
	        $('input[name="authcode"]').val(authCode); //생성된 코드를 인증번호 입력란에 입력
	        $('#acceptok').removeAttr('disabled'); //확인 버튼 활성화
	        $.time(179); //179초 타이머 시작
	    });
	
	    function showPopup(authCode) {
	        popupLayer = $('<div class="popup-layer"</div>');
	        let popupBox = $('<div class="popup-box"><p>인증번호: ' + authCode + '</p><button class="close-popup">Close</button></div>');
	
	        popupLayer.append(popupBox);
	
	        $('body').append(popupLayer);
	        $('.close-popup').on('click', function () {
	            popupLayer.remove();
	            popupLayer = null; // 팝업이 닫힐 때 변수 초기화
	        });
	    }
	
	    /* 인증번호 확인 */
	    $('#acceptok').on('click', function () {
	        let enteredCode = $('input[name="authcode"]').val();
	        $('#myid').val("");
	
	        if (authCode !== null && enteredCode === authCode.toString()) {
	            //alert('일치');
        		$.ajax({
        			type: 'POST',
        			url: '/ddstudio/user/findid.do',
        			data: {
        				name: $('#name').val(),
        				tel: $('#tel').val()
        			},
        			dataType: 'json',
        			success: function(result) {
        				//alert(result.email);

        				if (result.email != null) {
        					$('#myid').val(result.email);
        				} else {
        					$('#myid').val('해당 정보로 가입한 회원이 없습니다.');
        				}
        			},
        			errors: function(a,b,c) {
        				console.log(a,b,c);
        			}
        		});
	        } else {
	            alert('인증번호가 일치하지 않습니다.');
	            //toastr.error('인증번호가 일치하지 않습니다.', '인증 오류');
       			/* Swal.fire({
		            icon: 'error',
		            title: '인증번호 불일치',
		            text: '인증번호가 일치하지 않습니다.',
		        }); */
	        }
	    });
	</script>
	<script>
		let isValid = [false, false]; // 검사 결과 저장
		
		document.addEventListener("DOMContentLoaded", function () {
			
		    const acceptReqButton = document.getElementById("acceptreq");
	
		    function updateButtonStatus() {
		        const isAllValid = isValid.every((value) => value);
	
		        if (isAllValid) {
		        	acceptReqButton.removeAttribute("disabled");
		            //document.getElementById("ok-message").textContent = "true";
		        } else {
		        	acceptReqButton.setAttribute("disabled", "disabled");
		            //document.getElementById("ok-message").textContent = "false";
		        }
		    }
		    
		    /* 이름 유효성 검사 */
		    const nameField = document.getElementById("name");
		    const nameErrorDiv = document.getElementById("name-error");
		    const nameRegex = /^[가-힣]{2,6}$/; // 2글자에서 6글자의 한글 이름만 허용
	
		    nameField.addEventListener("input", function () {
		    	isValid[0] = nameRegex.test(nameField.value);
		        
		        nameErrorDiv.textContent = isValid[0] ? "" : "2-6자의 한글 이름을 입력하세요.";
		        nameErrorDiv.style.display = isValid[0] ? "none" : "block";
	
		        if (nameField.value.length === 0) {
		            nameErrorDiv.textContent = "";
		            nameErrorDiv.style.display = "none";
		        }

		        $('#acceptok').attr('disabled', 'disabled');
		        
		        updateButtonStatus();
		    });
	        
		    /* 연락처 유효성 검사 */
		    const telField = document.getElementById("tel");
		    const telErrorDiv = document.getElementById("tel-error");
		    const telRegex = /^010-[0-9]{4}-[0-9]{4}$/; // 010-XXXX-XXXX 형식의 전화번호
	
		    telField.addEventListener("input", function () {
		    	isValid[1] = telRegex.test(telField.value);
	
		        telErrorDiv.textContent = isValid[1] ? "" : "올바른 전화번호 형식을 입력하세요. (예: 010-XXXX-XXXX)";
		        telErrorDiv.style.display = isValid[1] ? "none" : "block";
	
		        if (telField.value.length === 0) {
		            telErrorDiv.textContent = "";
		            telErrorDiv.style.display = "none";
		        }

		        $('#acceptok').attr('disabled', 'disabled');
		        
		        updateButtonStatus();
		    });
	        
		    const cancelButton = document.getElementById("cancel");
		    
		    cancelButton.addEventListener("click", function () {
		        nameField.value = "";
		        telField.value = "";
		        nameErrorDiv.style.display = "none";
		        telErrorDiv.style.display = "none";
		        $('#acceptok').attr('disabled', 'disabled');
		        $('input[name="authcode"]').val("");
		        $('#myid').val("");
		        
		        isValid = [false, false];
		        updateButtonStatus();

		        // 타이머 정지
			    clearInterval(intervalCall);
		        
		        // 팝업 레이어 닫기
		        if (popupLayer) {
		            popupLayer.remove();
		            popupLayer = null;
		        }
		    });
		});
	</script>
</body>
</html>

change-pw.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#email {
	width: 400px;
}

#cancel, #acceptok {
	margin-right: 10px;
}

#valid > tbody > tr:nth-child(5) > td > div > input {
	margin-left: -10px;
}

.certification {
	margin-left: 30px;
}

td > div > input {
	width: 80%;
}

.sub-title > p {
	font-size: 20px;
    font-weight: 800;
}

.button {
    border: none;
    border-radius: 5px;
    font-size: 16px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
    transition: background 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}

/*
table {
	border-left: 2px solid #d1d1d1;
    border-right: 2px solid #d1d1d1;
    border-radius: 20px;
    border-collapse: separate;
    background: transparent;
}
*/

table th {
	padding: 0 0 0 10px;
}

td > div > input {
	background: transparent;
    border-bottom: 1.5px solid #cecece;
}
</style>
</head>
<body>
	<!-- change-pw.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>비밀번호 변경</h1>

		<div class="sub-title">
			<p>회원정보입력</p>
		</div>

		<div id="content">
			<div class="wide-item">
				<form method="POST" action="/ddstudio/user/changepw.do">
					<table id="valid">
						<!-- 이메일 필드와 에러 메시지 -->
						<tr>
							<th class="required">이메일 (아이디)</th>
							<td>
								<div class="email">
									<input type="text" name="email" id="email" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="email-error" class="error-message" style="display:none;"></div>
						        <span id="duplicate-check-message" class="error-message" style="display:none;"></span>
						    </td>
						</tr>
						 <!-- 연락처 필드와 에러 메시지 -->
						<tr>
							<th class="required">연락처</th>
							<td>
								<div>
									<input type="text" name="tel" id="tel" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="tel-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						
						<tr>
							<th class="required">인증번호</th>
							<td>
								<div class="certification">
									<input type="text" name="authcode" placeholder="6자리 숫자" class="certificationNumber" autocomplete="off">
									<span class="certificationTime">03:00</span>
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="acceptreq" class="acceptreq check button" disabled>요청</button>
									<button type="button" id="acceptok" class="check button" disabled>확인</button>
								</div>
							</td>
						</tr>
					</table>
					
					<div class="sub-title">
						<p>새로운 비밀번호 입력</p>
					</div>
					<table id="check">
						<!-- 비밀번호 필드와 에러 메시지 -->
						<tr>
							<th class="required">새로운 비밀번호</th>
							<td>
								<div>
									<input type="password" name="pw" id="pw" required class="middle-flat" disabled>
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="pw-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						<!-- 비밀번호 확인 필드와 에러 메시지 -->
						<tr>
							<th class="required">비밀번호 확인</th>
							<td>
								<div>
									<input type="password" name="pwok" id="pwok" required class="middle-flat" disabled>
								</div>
							</td>
						</tr>
						<tr>
						    <th></th>
						    <td>
						        <div id="pw-confirm-error" class="error-message" style="display:none;"></div>
						    </td>
						</tr>
						
						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="change" class="check button" disabled>변경</button>
									<button type="button" id="cancel" class="button" onclick="location.href='/ddstudio/user/login.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>
			</div>
		</div>
	</main>
	
	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
	<script>
	    let countTime = 0; //타이머 초기값
	    let intervalCall; //타이머 식별
	    let authCode; //인증 코드
	    let popupLayer; //팝업 레이어
	
	    //타이머 시작
	    $.time = function (time) {
	        countTime = time; //시간 초기화
	        clearInterval(intervalCall); // 이전 타이머 중단
	        intervalCall = setInterval(alertFunc, 1000);
	    }
	
	    //타이머 중단
	    $.closeTime = function () {
	        clearInterval(intervalCall);
	    }
	
	    //타이머 호출
	    function alertFunc() {
	        let min = Math.floor(countTime / 60);
	        let sec = countTime - (60 * min);
	        
	        //타이머 출력
	        if (sec > 9) {
	            $('.certificationTime').text(min + ':' + sec + '');
	        } else {
	            $('.certificationTime').text(min + ':0' + sec + '');
	        }
	        
	        /* 인증번호 시간이 만료되면 재요청 필요 */
	        if (countTime <= 0) {
	            clearInterval(intervalCall); //타이머 중단
	            $('#acceptok').attr('disabled', 'disabled'); //확인 버튼 비활성화
	        }
	        
	        countTime--;
	    };
	
	    //인증번호 요청
	    $('.acceptreq').on("click", function () {
	        authCode = Math.floor(100000 + Math.random() * 900000); //랜덤한 6자리

	        // 기존에 열린 팝업이 있다면 닫기
	        if (popupLayer) {
	            popupLayer.remove();
	        }
	        
	        showPopup(authCode); //팝업으로 생성된 코드 출력
	
	        $('input[name="authcode"]').val(authCode); //생성된 코드를 인증번호 입력란에 입력
	        $('#acceptok').removeAttr('disabled'); //확인 버튼 활성화
	        $.time(179); //179초 타이머 시작
	    });
	
	    function showPopup(authCode) {
	        popupLayer = $('<div class="popup-layer"</div>');
	        let popupBox = $('<div class="popup-box"><p>인증번호: ' + authCode + '</p><button class="close-popup">Close</button></div>');
	
	        popupLayer.append(popupBox);
	
	        $('body').append(popupLayer);
	        $('.close-popup').on('click', function () {
	            popupLayer.remove();
	            popupLayer = null; // 팝업이 닫힐 때 변수 초기화
	        });
	    }
		
		$('#acceptok').on('click', function () {
	        let enteredCode = $('input[name="authcode"]').val();
	        $('#pw').val("");
	        $('#pwok').val("");
            $('#pw').prop('disabled', true);
            $('#pwok').prop('disabled', true);
	
	        if (authCode !== null && enteredCode === authCode.toString()) {
	            //alert('일치');
        	    $.ajax({
        	        type: 'POST',
        	        url: '/ddstudio/user/changepw.do',
        	        data: {
        	            email: $('#email').val(),
        	            tel: $('#tel').val(),
           	            pw: null
        	        },
        	        dataType: 'json',
        	        success: function (result) {
        	            if (result.cnt == 1) {
        	                alert("계정을 확인했습니다.");
        	                $('#pw').prop('disabled', false);
        	                $('#pwok').prop('disabled', false);
        	            } else {
        	                alert("해당 정보로 가입한 회원이 없습니다.");
        	                $('#pw').prop('disabled', true);
        	                $('#pwok').prop('disabled', true);
        	            }
        	        },
        	        errors: function(a,b,c) {
        				console.log(a,b,c);
        			}
        	    });
	        } else {
	            alert('인증번호가 일치하지 않습니다.');
	        }
	    });
		

		$('#change').on('click', function () {

       	    $.ajax({
       	        type: 'POST',
       	        url: '/ddstudio/user/changepw.do',
       	        data: {
    	            email: $('#email').val(),
    	            tel: $('#tel').val(),
       	            pw: $('#pw').val()
       	        },
       	        dataType: 'json',
       	        success: function (result) {
       	            if (result.message == 1) {
       	                alert("비밀번호를 변경했습니다.");
       	                window.location.replace('/ddstudio/index.do');
       	            } else {
       	                alert("비밀번호 변경에 실패했습니다.");
       	            }
       	        },
       	        errors: function(a,b,c) {
       				console.log(a,b,c);
       			}
       	    });
	    });
	</script>
	<script>
		let isValid = [false, false]; // 검사 결과 저장
		let isPasswordValid = [false, false]; // 비밀번호 검사 결과 저장
		
		document.addEventListener("DOMContentLoaded", function () {
			
			const acceptReqButton = document.getElementById("acceptreq");
			const changeButton = document.getElementById("change");
	
		    function updateButtonStatus() {
		        const isAllValid = isValid.every((value) => value);
	
		        if (isAllValid) {
		        	acceptReqButton.removeAttribute("disabled");
		            //document.getElementById("ok-message").textContent = "true";
		        } else {
		        	acceptReqButton.setAttribute("disabled", "disabled");
		            //document.getElementById("ok-message").textContent = "false";
		        }
		    }
		    
		    function updateChangeButtonStatus() {
		        const isAllPasswordValid = isPasswordValid.every((value) => value);
	
		        if (isAllPasswordValid) {
		            changeButton.removeAttribute("disabled");
		            //document.getElementById("ok-message").textContent = "true";
		        } else {
		        	changeButton.setAttribute("disabled", "disabled");
		            //document.getElementById("ok-message").textContent = "false";
		        }
		    }
		    
		    /* 이메일 유효성 검사 */
		    const emailField = document.getElementById("email");
		    const emailErrorDiv = document.getElementById("email-error");
		    const emailRegex = /^[a-z0-9._%+-]{1,20}\@[a-z0-9.-]{1,8}\.[a-z]{1,5}$/;
	        
		    emailField.addEventListener("input", function () {
		    	isValid[0] = emailRegex.test(emailField.value);
		    	
		        emailErrorDiv.textContent = isValid[0] ? "" : "올바른 이메일 형식을 입력하세요. (예: park@email.com)";
		        emailErrorDiv.style.display = isValid[0] ? "none" : "block";
		        
		        if (emailField.value.length === 0) {
		        	emailErrorDiv.textContent = "";
		        	emailErrorDiv.style.display = "none";
		        }

	            $('#pw').prop('disabled', true);
	            $('#pwok').prop('disabled', true);
	            $('#acceptok').attr('disabled', 'disabled');
	            
		        updateButtonStatus();
		        updateChangeButtonStatus();
		    });
	        
		    /* 연락처 유효성 검사 */
		    const telField = document.getElementById("tel");
		    const telErrorDiv = document.getElementById("tel-error");
		    const telRegex = /^010-[0-9]{4}-[0-9]{4}$/; // 010-XXXX-XXXX 형식의 전화번호
	
		    telField.addEventListener("input", function () {
		    	isValid[1] = telRegex.test(telField.value);
	
		        telErrorDiv.textContent = isValid[1] ? "" : "올바른 전화번호 형식을 입력하세요. (예: 010-XXXX-XXXX)";
		        telErrorDiv.style.display = isValid[1] ? "none" : "block";
	
		        if (telField.value.length === 0) {
		            telErrorDiv.textContent = "";
		            telErrorDiv.style.display = "none";
		        }

	            $('#pw').prop('disabled', true);
	            $('#pwok').prop('disabled', true);
	            $('#acceptok').attr('disabled', 'disabled');
	            
		        updateButtonStatus();
		        updateChangeButtonStatus();
		    });

		    /* 비밀번호 유효성 검사 */
		    const passwordField = document.getElementById("pw");
		    const passwordErrorDiv = document.getElementById("pw-error");
		    const passwordConfirmField = document.getElementById("pwok");
		    const passwordConfirmErrorDiv = document.getElementById("pw-confirm-error");
		    const passwordRegex = /^(?=.*[0-9])(?=.*[A-Za-z])(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,15}$/
	        
		    passwordField.addEventListener("input", function () {
		    	isPasswordValid[0] = passwordRegex.test(passwordField.value);
		    	isPasswordValid[1] = passwordConfirmField.value === passwordField.value;
	
		        passwordErrorDiv.textContent = isPasswordValid[0] ? "" : "8-15자의 영문/숫자/특수문자를 함께 입력하세요.";
		        passwordErrorDiv.style.display = isPasswordValid[0] ? "none" : "block";
		        
		        passwordConfirmErrorDiv.textContent = isPasswordValid[1] ? "" : "비밀번호가 동일하지 않습니다.";
		        passwordConfirmErrorDiv.style.display = isPasswordValid[1] ? "none" : "block";
	
		        if (passwordField.value.length === 0) {
		            passwordErrorDiv.textContent = "";
		            passwordErrorDiv.style.display = "none";
		        }
	
		        if (passwordConfirmField.value.length === 0) {
		            passwordConfirmErrorDiv.textContent = "";
		            passwordConfirmErrorDiv.style.display = "none";
		        }
		        
		        updateChangeButtonStatus();
		    });
	
		    /* 비밀번호 확인 유효성 검사 */
		    passwordConfirmField.addEventListener("input", function () {
		    	isPasswordValid[1] = passwordConfirmField.value === passwordField.value;
		        
		        passwordConfirmErrorDiv.textContent = isPasswordValid[1] ? "" : "비밀번호가 동일하지 않습니다.";
		        passwordConfirmErrorDiv.style.display = isPasswordValid[1] ? "none" : "block";
		        
		        if (passwordConfirmField.value.length === 0) {
		            passwordConfirmErrorDiv.textContent = "";
		            passwordConfirmErrorDiv.style.display = "none";
		        }
		        
		        updateChangeButtonStatus();
		    });
		    
		    const cancelButton = document.getElementById("cancel");
	
		    cancelButton.addEventListener("click", function () {
		        emailField.value = "";
		        passwordField.value = "";
		        passwordConfirmField.value = "";
		        telField.value = "";
	
		        emailErrorDiv.style.display = "none";
		        passwordErrorDiv.style.display = "none";
		        passwordConfirmErrorDiv.style.display = "none";
		        telErrorDiv.style.display = "none";
		        
		        $('input[name="authcode"]').val("");
		        $('#pw').val("");
		        $('#pwok').val("");
	            $('#pw').prop('disabled', true);
	            $('#pwok').prop('disabled', true);
	            $('#acceptok').attr('disabled', 'disabled');
		        
		        isValid = [false, false];
		        isPasswordValid = [false, false];
		        updateButtonStatus();
		        updateChangeButtonStatus();
		        
		        // 타이머 정지
			    clearInterval(intervalCall);

		        // 팝업 레이어 닫기
		        if (popupLayer) {
		            popupLayer.remove();
		            popupLayer = null;
		        }
		    });
		});
	</script>
</body>
</html>

MBTI.java

package com.ddstudio.test;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * MBTI별 추천 페이지로 이동하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbti.do")
public class MBTI extends HttpServlet {

	/**
	 * MBTI별 추천 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// MBTI별 추천 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/list.jsp");
		dispatcher.forward(req, resp);
	}

}

MBTIList.java

package com.ddstudio.test;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.ddstudio.test.model.MBTIDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI 정보를 가져와 JSON 형식으로 응답하는 서블릿 클래스입니다.
 * MBTI 테이블에 들어 있는 정보만을 가져옵니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbti/list.do")
public class MBTIList extends HttpServlet {

	/**
	 * MBTI 정보를 가져와 JSON 형식으로 응답하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		TestDAO dao = new TestDAO();
		
		// MBTI 정보 가져오기
		ArrayList clist = dao.listMBTI();

		JSONArray arr = new JSONArray();
		
		// MBTI 정보를 JSON 형식으로 배열에 추가
		for (MBTIDTO dto : clist) {
		    JSONObject obj = new JSONObject();
		    obj.put("mbti", dto.getMbti());
		    arr.add(obj);
		}

		resp.setContentType("application/json");
		resp.setCharacterEncoding("UTF-8");

		// JSON 데이터 전송
		PrintWriter writer = resp.getWriter();
		writer.write(arr.toString());
		writer.close();
		
	}
}

MBTIListLoad.java

package com.ddstudio.test;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.ddstudio.test.model.MBTIDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI 정보를 가져와 JSON 형식으로 응답하는 서블릿 클래스입니다.
 * MBTI와 연관이 있는 어트랙션과 코스 데이터를 함께 가져옵니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtilistload.do")
public class MBTIListLoad extends HttpServlet {

	/**
	 * MBTI 정보를 가져와 JSON 형식으로 응답하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setCharacterEncoding("UTF-8");
		resp.setContentType("application/json");
		
		PrintWriter writer = resp.getWriter();
		
		try {
			TestDAO dao = new TestDAO();
			
			// MBTI 정보 가져오기
			ArrayList mbtiList = dao.getAllMBTI();
			
			JSONArray jsonArray = new JSONArray();
			
			// MBTI 정보를 JSON 형식으로 배열에 추가
			for (MBTIDTO mbti : mbtiList) {
				JSONObject jsonObject = new JSONObject();
				jsonObject.put("mbti_seq", mbti.getMbti_seq());
				jsonObject.put("result", mbti.getResult());
				jsonObject.put("mbti", mbti.getMbti());
				jsonObject.put("course_seq", mbti.getCourse_seq());
				jsonObject.put("course_name", mbti.getCourse_name());
				jsonObject.put("course_img", mbti.getCourse_img());
				jsonObject.put("attraction_seq", mbti.getAttraction_seq());
				jsonObject.put("attraction_name", mbti.getAttraction_name());
				jsonObject.put("attraction_img", mbti.getAttraction_img());
				
				jsonArray.add(jsonObject);
			}
			
			// JSON 배열을 문자열로 변환하여 전송
			writer.print(jsonArray.toJSONString());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writer.close();
		}
	}
}

MBTIObjectLoad.java

package com.ddstudio.test;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.ddstudio.activity.model.AttractionDTO;
import com.ddstudio.test.model.CourseDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI와 연관된 코스 및 어트랙션 정보로 MBTI별 추천 정보를 JSON 형식으로 응답하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtiobjectload.do")
public class MBTIObjectLoad extends HttpServlet {

	/**
	 * MBTI와 연관된 코스 및 어트랙션 정보를 가져와 JSON 형식으로 응답하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		TestDAO dao = new TestDAO();

		// CourseDTO 및 AttractionDTO 리스트 가져오기
		ArrayList clist = dao.listCourse();
		ArrayList alist = dao.listAttraction();

		JSONArray arr = new JSONArray();

		// course
		// CourseDTO를 JSON으로 변환하여 배열에 추가
		for (CourseDTO dto : clist) {
			JSONObject obj = new JSONObject();
			obj.put("type", "course");
			obj.put("course_seq", dto.getCourse_seq());
			obj.put("course_name", dto.getName());
			arr.add(obj);
		}

		// attraction
		// AttractionDTO를 JSON으로 변환하여 배열에 추가
		for (AttractionDTO dto : alist) {
			JSONObject obj = new JSONObject();
			obj.put("type", "attraction");
			obj.put("attraction_seq", dto.getAttraction_seq());
			obj.put("attraction_name", dto.getName());
			arr.add(obj);
		}

		resp.setContentType("application/json");
		resp.setCharacterEncoding("UTF-8");

		// JSON 배열 전송
		PrintWriter writer = resp.getWriter();
		writer.write(arr.toString());
		writer.close();
	}
}

MBTIDetail.java

package com.ddstudio.test;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONObject;

import com.ddstudio.test.model.MBTIDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI별 추천 상세 정보를 처리하는 서블릿 클래스입니다.
 * 사용자가 선택한 MBTI에 대한 상세 정보를 JSON 형식으로 응답합니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtidetail.do")
public class MBTIDetail extends HttpServlet {

	/**
	 * MBTI별 추천 상세 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// MBTI별 추천 상세 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/detail.jsp");
		dispatcher.forward(req, resp);
	}
	
	/**
	 * 선택한 MBTI에 대한 상세 정보를 응답하는 POST 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		String mbti = req.getParameter("mbti");

		TestDAO dao = new TestDAO();
		MBTIDTO mbtiDetails = dao.get(mbti);

		JSONObject mbtiResult = new JSONObject();

		if (mbtiDetails != null) {
			mbtiResult.put("mbti_seq", mbtiDetails.getMbti_seq());
			mbtiResult.put("result", mbtiDetails.getResult());
			mbtiResult.put("mbti", mbtiDetails.getMbti());
			mbtiResult.put("course_seq", mbtiDetails.getCourse_seq());
			mbtiResult.put("course_name", mbtiDetails.getCourse_name());
			mbtiResult.put("course_img", mbtiDetails.getCourse_img());
			mbtiResult.put("attraction_seq", mbtiDetails.getAttraction_seq());
			mbtiResult.put("attraction_name", mbtiDetails.getAttraction_name());
			mbtiResult.put("attraction_img", mbtiDetails.getAttraction_img());
		}

		resp.setContentType("application/json");
		resp.setCharacterEncoding("UTF-8");

		//System.out.println(mbtiResult);
		
		try (PrintWriter writer = resp.getWriter()) {
			writer.write(mbtiResult.toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

MBTIAdd.java

package com.ddstudio.test;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.test.model.MBTIDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI별 추천 정보를 추가하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtiadd.do")
public class MBTIAdd extends HttpServlet {

	/**
	 * MBTI별 추천 추가 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// MBTI별 추천 추가 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/add.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * MBTI별 추천 정보를 추가하는 POST 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		try {
			
			resp.setContentType("application/json");
			resp.setCharacterEncoding("UTF-8");
			
			// MBTI별 추천 추가할 데이터
			String mbti = req.getParameter("mbti");
			String resultMessage = req.getParameter("result");
			String courseSeq = req.getParameter("course-name");
			String attractionSeq = req.getParameter("attraction-name");

			MBTIDTO dto = new MBTIDTO();
			dto.setMbti(mbti);
			dto.setResult(resultMessage);
			dto.setCourse_seq(courseSeq);
			dto.setAttraction_seq(attractionSeq);

			TestDAO dao = new TestDAO();

			// MBTI별 추천 정보 추가
			int result = dao.mbtiAdd(dto);

			// System.out.println(result);

			// 응답 데이터를 JSON 형식으로 생성
			String jsonResponse = "{\"result\": " + result + "}";
			resp.getWriter().write(jsonResponse);

		} catch (Exception e) {
			// 에러 발생
			e.printStackTrace();
			resp.getWriter().write("{\"result\": 0}");
		}

	}

}

MBTIEdit.java

package com.ddstudio.test;

import java.io.IOException;
import java.util.ArrayList;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.test.model.MBTIDTO;
import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI별 추천 정보를 수정하는 기능을 담당하는 서블릿 클래스입니다.
 * 사용자에게 MBTI 정보를 전달하고 수정 페이지로 이동합니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtiedit.do")
public class MBTIEdit extends HttpServlet {

	/**
	 * MBTI별 추천 수정 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// MBTI별 추천 수정 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/edit.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * MBTI 정보를 가져와 수정하는 POST 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setCharacterEncoding("UTF-8");
		resp.setContentType("text/html;charset=UTF-8");

		try {
			
			TestDAO dao = new TestDAO();
			
			// MBTI별 추천 정보 가져오기
			ArrayList mbtiList = dao.getAllMBTI();
			
			// request 객체에 MBTI 정보를 attribute로 설정
			req.setAttribute("mbtiList", mbtiList);

			RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/edit.jsp");
			dispatcher.forward(req, resp);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

MBTIDel.java

package com.ddstudio.test;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.test.repository.TestDAO;

/**
 * MBTI별 추천 정보를 삭제하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/mbtidel.do")
public class MBTIDel extends HttpServlet {

	/**
	 * MBTI별 추천 삭제 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// MBTI별 추천 삭제 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/mbti/del.jsp");
		dispatcher.forward(req, resp);
	}

	/**
	 * MBTI별 추천 정보를 삭제하는 POST 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
        String mbti_seq = req.getParameter("mbti_seq"); // 삭제할 MBTI별 추천 정보 번호

        TestDAO dao = new TestDAO();
        
        // MBTI별 추천 정보 삭제
        int result = dao.MBTIDel(mbti_seq);

        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().print("{\"result\":" + result + "}");
    }

}

MBTIDTO.java

package com.ddstudio.test.model;

/**
 * MBTI 정보를 담는 데이터 전송 객체(DTO)입니다.
 * MBTI의 일련번호, 결과, MBTI 유형, 코스 일련번호 및 정보, 어트랙션 일련번호 및 정보를 관리합니다.
 * 
 * @author 이승원
 */
public class MBTIDTO {

	private String mbti_seq; // MBTI 일련번호
	private String result; // MBTI 결과
	private String mbti; // MBTI 유형
	private String course_seq; // 코스 일련번호
	private String course_name; // 코스 이름
	private String course_img; // 코스 이미지 경로
	private String attraction_seq; // 어트랙션 일련번호
	private String attraction_name; // 어트랙션 이름
	private String attraction_img; // 어트랙션 이미지 경로
	
	public String getMbti_seq() {
		return mbti_seq;
	}
	
	public void setMbti_seq(String mbti_seq) {
		this.mbti_seq = mbti_seq;
	}
	
	public String getResult() {
		return result;
	}
	
	public void setResult(String result) {
		this.result = result;
	}
	
	public String getMbti() {
		return mbti;
	}
	
	public void setMbti(String mbti) {
		this.mbti = mbti;
	}
	
	public String getCourse_seq() {
		return course_seq;
	}
	
	public void setCourse_seq(String course_seq) {
		this.course_seq = course_seq;
	}
	
	public String getCourse_name() {
		return course_name;
	}
	
	public void setCourse_name(String course_name) {
		this.course_name = course_name;
	}
	
	public String getCourse_img() {
		return course_img;
	}
	
	public void setCourse_img(String course_img) {
		this.course_img = course_img;
	}
	
	public String getAttraction_seq() {
		return attraction_seq;
	}
	
	public void setAttraction_seq(String attraction_seq) {
		this.attraction_seq = attraction_seq;
	}
	
	public String getAttraction_name() {
		return attraction_name;
	}
	
	public void setAttraction_name(String attraction_name) {
		this.attraction_name = attraction_name;
	}
	
	public String getAttraction_img() {
		return attraction_img;
	}
	
	public void setAttraction_img(String attraction_img) {
		this.attraction_img = attraction_img;
	}
	
}

mbti > list.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<style>
.button-container {
	display: flex;
	flex-wrap: wrap;
}

.item {
	height: 100px;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	color: white;
	background-color: #007bff;
	border-radius: 8px;
	margin: 10px;
	padding: 20px;
	border: 0px;
	box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
	transition: all 0.3s ease-in-out;
	cursor: pointer;
	font-size: 40px;
	font-weight: 600;
}

.item:hover {
	transform: scale(1.05);
	background-color: #0056b3;
}

.item h3 {
	margin: 0;
	font-size: 18px;
	color: #333;
}

#title {
	margin-top: 123px;
	background-image: url('/ddstudio/asset/image/background-7.jpg');
}

#content {
	margin-top: 50px !important;
}

#overlay-div {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 400px;
	background-color: black;
	opacity: 0.45; /* 투명도 조절 */
	z-index: 1; /* 다른 요소들보다 위에 위치하도록 설정 */
}

#title h2 {
	color: #EEE;
	z-index: 2;
}
</style>
</head>
<body>
	<!-- /mbti/list.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<div id="title" title="senivpetro 출처 Freepik">
		<div id="overlay-div"></div>
			<h2>MBTI별 추천</h2>
		</div>

		<!-- 관리자 -->
		<c:if test="${lv == 2}">
			<div id="admin-btn">
				<button type="button" id="add-btn"
					onclick="location.href='/ddstudio/test/mbtiadd.do'">
					<i class="fa-solid fa-plus"></i>추가
				</button>
				<button type="button" id="del-btn"
					onclick="location.href='/ddstudio/test/mbtidel.do'">
					<i class="fa-solid fa-trash"></i>삭제
				</button>
				<button type="button" id="edit-btn"
					onclick="location.href='/ddstudio/test/mbtiedit.do'">
					<i class="fa-solid fa-pen-to-square"></i>수정
				</button>
			</div>
		</c:if>

		<div id="content">

			<form id="mbtiForm" method="GET"
				action="/ddstudio/test/mbtidetail.do">
				<div class="button-container">
					<!--
					<div class="item" name="ISTJ" id="ISTJ"
						onclick="handleButtonClick('ISTJ')">ISTJ</div>
					<div class="item" name="ISFJ" id="ISFJ"
						onclick="handleButtonClick('ISFJ')">ISFJ</div>
					<div class="item" name="INFJ" id="INFJ"
						onclick="handleButtonClick('INFJ')">INFJ</div>
					<div class="item" name="INTJ" id="INTJ"
						onclick="handleButtonClick('INTJ')">INTJ</div>
					<div class="item" name="ISTP" id="ISTP"
						onclick="handleButtonClick('ISTP')">ISTP</div>
					<div class="item" name="ISFP" id="ISFP"
						onclick="handleButtonClick('ISFP')">ISFP</div>
					<div class="item" name="INFP" id="INFP"
						onclick="handleButtonClick('INFP')">INFP</div>
					<div class="item" name="INTP" id="INTP"
						onclick="handleButtonClick('INTP')">INTP</div>
					<div class="item" name="ESTP" id="ESTP"
						onclick="handleButtonClick('ESTP')">ESTP</div>
					<div class="item" name="ESFP" id="ESFP"
						onclick="handleButtonClick('ESFP')">ESFP</div>
					<div class="item" name="ENFP" id="ENFP"
						onclick="handleButtonClick('ENFP')">ENFP</div>
					<div class="item" name="ENTP" id="ENTP"
						onclick="handleButtonClick('ENTP')">ENTP</div>
					<div class="item" name="ESTJ" id="ESTJ"
						onclick="handleButtonClick('ESTJ')">ESTJ</div>
					<div class="item" name="ESFJ" id="ESFJ"
						onclick="handleButtonClick('ESFJ')">ESFJ</div>
					<div class="item" name="ENFJ" id="ENFJ"
						onclick="handleButtonClick('ENFJ')">ENFJ</div>
					<div class="item" name="ENTJ" id="ENTJ"
						onclick="handleButtonClick('ENTJ')">ENTJ</div>
					-->
				</div>
			</form>
		</div>

	</main>
	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->

	<script>
		$(document).ready(function() {
			load();
		});

		function load() {
			$.ajax({
				type : 'GET',
				url : '/ddstudio/test/mbti/list.do',
				dataType : 'json',
				success : function(result) {
					var container = $('.button-container');
					container.empty();

					$(result).each(function(index, item) {
					    var div = $('<div>').addClass('item').attr({
					        'name' : item.mbti,
					        'id' : item.mbti,
					        'onclick' : 'handleButtonClick(\'' + item.mbti + '\')'
					    }).text(item.mbti);

					    // console.log(div);
					    container.append(div);
					});

					updateImage(result);
				},
				error : function(a, b, c) {
					console.log(a, b, c);
				}
			});
		}
		
		function handleButtonClick(mbti) {
		    console.log('Button clicked:', mbti);
		    var form = $('#mbtiForm');
		    form.find('input[name="mbti"]').remove(); //이전의 mbti 태그 삭제
		    
		    $('<input>').attr({
		        type: 'hidden',
		        name: 'mbti',
		        value: mbti
		    }).appendTo(form);

		    form.submit();
		}

	</script>
</body>
</html>

mbti > detail.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<style>
.wide-content-container {
    width: 1000px;
    border: 0;
    align-items: center;
    justify-content: center;
    text-align: center;
    margin: auto;
}

.sub-title {
	font-weight: 800;
    font-size: 24px;
    margin-top: 30px;
}

#content .wide-content-container {
    margin-top: -140px;
}

.wide-item>div {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	border: 0 !important;
}

.wide-item img {
	width: 90%;
	height: auto;
	border-radius: 8px;
	margin-bottom: 10px;
}

#title {
	margin-top: 123px;
}

.wide-item>div:nth-child(1) {
	margin-top: 250px;
}

.wide-item>div:nth-child(2) {
    justify-content: end;
    margin-top: 10px;
    height: 150px;
    font-weight: 800;
    font-size: 19px;
}

.wide-item img {
	height: 270px;
}

#title {
	margin-top: 123px;
	background-image: url('/ddstudio/asset/image/background-7.jpg');
}

#overlay-div {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 400px;
	background-color: black;
	opacity: 0.45; /* 투명도 조절 */
	z-index: 1; /* 다른 요소들보다 위에 위치하도록 설정 */
}

#title h2 {
	color: #EEE;
	z-index: 2;
}

#title p {
	z-index: 2;
}
</style>
</head>
<body>
	<!-- /mbti/list.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">

		<div id="title" title="작가 senivpetro 출처 Freepik">
		<div id="overlay-div"></div>
			<h2 id="mbtiTitle">ISTJ</h2>
			<br>
			<p>MBTI별 추천 결과</p>
		</div>
		
		<div class="sub-title">
			<p id="result">유형</p>
		</div>

		<div id="content">

			<div class="wide-content-container">
				<div class="wide-item">
					<div id="attraction-img">놀이기구 사진</div>
					<div id="attraction-name">놀이기구 이름</div>
				</div>
				<div class="wide-item">
					<div id="course-img">코스 사진</div>
					<div id="course-name">코스 이름</div>
				</div>
			</div>
		</div>

	</main>
	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->

	<script>
		function loadMBTIDetail() {
			var mbti = getParameterByName('mbti'); // mbti 매개변수 가져오기
			var xhr = new XMLHttpRequest();

			xhr.onreadystatechange = function() {
				if (xhr.readyState == 4 && xhr.status == 200) {
					// 서버 응답 처리 함수 호출
					handleMBTIDetailResponse(xhr.responseText);
				}
			};

			// 요청을 POST로 변경하고 MBTI 매개변수를 요청 본문에 포함
			xhr.open("POST", "/ddstudio/test/mbtidetail.do", true);
			xhr.setRequestHeader("Content-Type",
					"application/x-www-form-urlencoded");
			var params = "mbti=" + encodeURIComponent(mbti);
			xhr.send(params);
		}

		// AJAX POST 응답을 처리하는 함수
		function handleMBTIDetailResponse(responseText) {
			var data = JSON.parse(responseText);

			// 어트랙션 업데이트
			var result = document.getElementById('result');
			if (result) {
				result.innerText = data.result;
			}
			
			// 어트랙션 업데이트
			var attractionImg = document.getElementById('attraction-img');
			if (attractionImg) {
				attractionImg.innerHTML = '<img src="/ddstudio/asset/image/attraction/' + data.attraction_img + '" alt="' + data.attraction_name + '">';
			}

			var attractionName = document.getElementById('attraction-name');
			if (attractionName) {
				attractionName.innerText = data.attraction_name;
			}

			// 코스 업데이트
			var courseImg = document.getElementById('course-img');
			if (courseImg) {
				courseImg.innerHTML = '<img src="/ddstudio/asset/image/course/' + data.course_img + '" alt="' + data.course_name + '">';
			}

			var courseName = document.getElementById('course-name');
			if (courseName) {
				courseName.innerText = data.course_name;
			}
		}

		// URL에서 매개변수 추출 함수
		function getParameterByName(name, url) {
			if (!url)
				url = window.location.href;
			name = name.replace(/[\[\]]/g, "\\$&");
			var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex
					.exec(url);
			if (!results)
				return null;
			if (!results[2])
				return '';
			return decodeURIComponent(results[2].replace(/\+/g, " "));
		}

		// 페이지 로드 시 호출
		window.onload = loadMBTIDetail;

		// URL에서 mbti 매개변수 가져오기
		var mbti = getParameterByName('mbti');

		// 가져온 매개변수를 h2에 동적으로 설정
		var mbtiTitleElement = document.getElementById('mbtiTitle');
		if (mbtiTitleElement && mbti) {
			mbtiTitleElement.innerText = mbti;
		}
	</script>
</body>
</html>

mbti > add.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#cancel {
	margin-right: 25px;
}

.button {
	font-size: 16px;
	border: 1px solid #ccc;
    border-radius: 5px;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
}

select {
	width: 85%;
	padding: 10px;
	font-size: 16px;
	border: 1px solid #ccc;
	border-radius: 4px;
	box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
	appearance: none;
	-webkit-appearance: none;
	background-position: right 10px center;
	background-repeat: no-repeat;
	margin-left: 20px;
}

select option:checked {
	background-color: #ddd;
}

#courseImage {
	width: 375px;
	justify-content: center;
	align-items: center;
	text-align: center;
	align-items: center;
}
</style>
</head>
<body>
	<!-- /test/mbti/add.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>MBTI별 추천 추가</h1>

		<div class="sub-title"></div>

		<div id="content">
			<div class="wide-item">

				<form method="POST" action="/ddstudio/test/mbtiadd.do" id="mbtiForm"
					enctype="multipart/form-data">
					<table>
						<tr>
							<th class="required">MBTI명</th>
							<td>
								<div>
									<input name="mbti" id="mbti" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">결과</th>
							<td>
								<div>
									<input name="result" id="result" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">코스명</th>
							<td>
								<div>
									<select name="course-name" id="course-name" required class="middle-flat">
									</select>
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">어트랙션명</th>
							<td>
								<div>
									<select name="attraction-name" id="attraction-name" required class="middle-flat">
									</select>
								</div>
							</td>
						</tr>

						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="addButton" class="check button" onclick="addMBTI()">추가</button>
									<button type="button" id="cancel" class="button" onclick="location.href='/ddstudio/test/mbti.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>

			</div>
		</div>
	</main>

	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
	<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
	<script>
		// MBTI 추가
		function addMBTI() {
			$.ajax({
			    type: 'POST',
			    url: '/ddstudio/test/mbtiadd.do',
			    data: {
			        mbti: $('#mbti').val(),
			        result: $('#result').val(),
			        'course-name': $('#course-name').val(),
			        'attraction-name': $('#attraction-name').val()
			    },
			    success: function (response) {
		            // console.log(response);
		            var result = response.result;

		            if (result == 1) {
						//alert("추가 되었습니다.")
		                window.location.href = '/ddstudio/test/mbti.do';
		            } else {
		                alert("failed");
		            }
		        },
			    error: function (a, b, c) {
			        console.log(a, b, c);
			    }
			});
		}

		// 데이터 로드
		$(document).ready(function() {
			load();
		});

		function load() {
		    $.ajax({
		        type: 'GET',
		        url: '/ddstudio/test/mbtiobjectload.do',
		        dataType: 'json',
		        success: function (result) {
		            var courseSelect = $('#course-name');
		            var attractionSelect = $('#attraction-name');

		            courseSelect.empty();
		            attractionSelect.empty();

		            $(result).each(function (index, item) {
		                var option;

		                if (item.type === 'course') {
		                    option = $('<option>').val(item.course_seq).text(item.course_name);
		                    courseSelect.append(option);
		                } else if (item.type === 'attraction') {
		                     option = $('<option>').val(item.attraction_seq).text(item.attraction_name);
		                    attractionSelect.append(option);
		                }
		            });
		        },
		        error: function (a, b, c) {
		            console.log(a, b, c);
		        }
		    });
		}
	</script>

</body>
</html>

mbti > edit.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#cancel {
	margin-right: 25px;
}

select {
	width: 85%;
	padding: 10px;
	font-size: 16px;
	border: 1px solid #ccc;
	border-radius: 4px;
	box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
	appearance: none;
	-webkit-appearance: none;
	background-position: right 10px center;
	background-repeat: no-repeat;
	margin-left: 20px;
}

.button {
	font-size: 16px;
	border: 1px solid #ccc;
    border-radius: 5px;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
}

#mbti-container {
	display: flex;
	margin-left: 25px;
}

#selected-mbti {
	width: 190px;
}

#mbti {
	width: 198px;
	height: 43px;
}

select option:checked {
	background-color: #ddd;
}

#courseImage {
	width: 375px;
	justify-content: center;
	align-items: center;
	text-align: center;
	align-items: center;
}
</style>
</head>
<body>
	<!-- /test/mbti/edit.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>MBTI별 추천 수정</h1>

		<div class="sub-title"></div>

		<div id="content">
			<div class="wide-item">

				<form method="POST" action="/ddstudio/test/mbtiedit.do"
					id="mbtiForm" enctype="multipart/form-data">
					<table>
						<tr>
							<th class="required">MBTI명</th>
							<td>
								<div id="mbti-container">
									<div>
										<select name="selected-mbti" id="selected-mbti" required
											class="middle-flat">
										</select>
									</div>
									<div>
										<input name="mbti" id="mbti" required class="middle-flat">
									</div>
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">결과</th>
							<td>
								<div>
									<input name="result" id="result" required class="middle-flat">
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">코스명</th>
							<td>
								<div>
									<select name="course-name" id="course-name" required
										class="middle-flat">
									</select>
								</div>
							</td>
						</tr>
						<tr>
							<th class="required">어트랙션명</th>
							<td>
								<div>
									<select name="attraction-name" id="attraction-name" required
										class="middle-flat">
									</select>
								</div>
							</td>
						</tr>

						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="editButton" class="check button"
										onclick="editMBTI()">수정</button>
									<button type="button" id="cancel" class="button"
										onclick="location.href='/ddstudio/test/mbti.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>

			</div>
		</div>
	</main>

	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
	<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
	<script>
		// 데이터 로드
		$(document).ready(function() {
			load();
		});

		function load() {
			$.ajax({
				type : 'GET',
				url : '/ddstudio/test/mbtilistload.do',
				dataType : 'json',
				success : function(result) {
					var mbtiSelect = $('#selected-mbti');
					mbtiSelect.empty();

					$.each(result, function(index, item) {
						var option = $('<option>').val(item.mbti_seq).text(
								item.mbti);
						mbtiSelect.append(option);
					});

					// 선택된 MBTI 정보 표시 함수 호출
					showSelectedMBTI();
				},
				error : function(a, b, c) {
					console.log(a, b, c);
				}
			});

			$.ajax({
				type : 'GET',
				url : '/ddstudio/test/mbtiobjectload.do',
				dataType : 'json',
				success : function(result) {
					var courseSelect = $('#course-name');
					var attractionSelect = $('#attraction-name');

					courseSelect.empty();
					attractionSelect.empty();

					$(result).each(
							function(index, item) {
								var option;

								if (item.type === 'course') {
									option = $('<option>').val(item.course_seq)
											.text(item.course_name);
									courseSelect.append(option);
								} else if (item.type === 'attraction') {
									option = $('<option>').val(
											item.attraction_seq).text(
											item.attraction_name);
									attractionSelect.append(option);
								}
							});
				},
				error : function(a, b, c) {
					console.log(a, b, c);
				}
			});
		}

		// MBTI 추가 및 삭제
	    function editMBTI() {
	        var mbti_seq = $('#selected-mbti').val();

	        // 삭제 수행
	        $.ajax({
	            type: 'POST',
	            url: '/ddstudio/test/mbtidel.do',
	            data: {
	                mbti_seq: mbti_seq
	            },
	            success: function (response) {
	                var result = response.result;

	                if (result == 1) {
	                    // 삭제가 성공하면 추가 수행
	                    addMBTI();
	                } else {
	                    alert("failed");
	                }
	            },
	            error: function (a, b, c) {
	                console.log(a, b, c);
	            }
	        });
	    }

	    // MBTI 추가
	    function addMBTI() {
	        $.ajax({
	            type: 'POST',
	            url: '/ddstudio/test/mbtiadd.do',
	            data: {
	                mbti: $('#mbti').val(),
	                result: $('#result').val(),
	                'course-name': $('#course-name').val(),
	                'attraction-name': $('#attraction-name').val()
	            },
	            success: function (response) {
	                var result = response.result;

	                if (result == 1) {
						//alert("수정 되었습니다.")
	                    window.location.href = '/ddstudio/test/mbti.do';
	                } else {
	                    alert("failed");
	                }
	            },
	            error: function (a, b, c) {
	                console.log(a, b, c);
	            }
	        });
	    }
	</script>

</body>
</html>

mbti > del.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<link rel="stylesheet" href="/ddstudio/asset/css/user.css">
<style>
#cancel {
	margin-right: 35px;
}

select {
	padding: 10px;
	font-size: 16px;
	border: 1px solid #ccc;
	border-radius: 4px;
	box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
	appearance: none;
	-webkit-appearance: none;
	background-position: right 10px center;
	background-repeat: no-repeat;
	margin-left: 20px;
}

.button {
	font-size: 16px;
	border: 1px solid #ccc;
    border-radius: 5px;
    background-color: white;
    box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.1);
}

#mbti-container {
	display: flex;
	margin-left: 25px;
}

select option:checked {
	background-color: #ddd;
}

#selected-mbti {
	width: 400px;
	margin-left: 47px;
}
</style>
</head>
<body>
	<!-- /test/mbti/del.jsp -->
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<h1>MBTI별 추천 삭제</h1>

		<div class="sub-title"></div>

		<div id="content">
			<div class="wide-item">

				<form method="POST" action="/ddstudio/test/mbtidel.do" id="mbtiForm"
					enctype="multipart/form-data">
					<table>
						<tr>
							<th class="required">MBTI명</th>
							<td>
								<div id="mbti-container">
									<div>
										<select name="selected-mbti" id="selected-mbti" required class="middle-flat">
										</select>
									</div>
								</div>
							</td>
						</tr>
						<tr>
							<th></th>
							<td>
								<div class="button-container">
									<button type="button" id="delButton" class="check button" onclick="delMBTI()">삭제</button>
									<button type="button" id="cancel" class="button" onclick="location.href='/ddstudio/test/mbti.do';">취소</button>
								</div>
							</td>
						</tr>
					</table>
				</form>

			</div>
		</div>
	</main>

	<%@ include file="/WEB-INF/views/inc/footer.jsp"%><!-- Footer -->
	<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
	<script>
		// 데이터 로드
		$(document).ready(function() {
			load();
		});

		function load() {
			$.ajax({
				type : 'GET',
				url : '/ddstudio/test/mbtilistload.do',
				dataType : 'json',
				success : function(result) {
					var mbtiSelect = $('#selected-mbti');
					mbtiSelect.empty();

					$.each(result, function(index, item) {
						var option = $('<option>').val(item.mbti_seq).text(
								item.mbti);
						mbtiSelect.append(option);
					});
				},
				error : function(a, b, c) {
					console.log(a, b, c);
				}
			});
		}

		// MBTI 삭제
		function delMBTI() {
			var mbti_seq = $('#selected-mbti').val();

			$.ajax({
				type : 'POST',
				url : '/ddstudio/test/mbtidel.do',
				data : {
					mbti_seq : mbti_seq
				},
				success : function(response) {
					var result = response.result;

					if (result == 1) {
						//alert("삭제 되었습니다.")
						window.location.href = '/ddstudio/test/mbti.do';
					} else {
						alert("failed");
					}
				},
				error : function(a, b, c) {
					console.log(a, b, c);
				}
			});
		}
	</script>

</body>
</html>

Worldcup.java

package com.ddstudio.test;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * DD 월드컵 페이지로 이동하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/worldcup.do")
public class Worldcup extends HttpServlet {

	/**
	 * DD 월드컵 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// DD 월드컵 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/worldcup/list.jsp");
		dispatcher.forward(req, resp);
	}
}

WorldcupAttraction.java

package com.ddstudio.test;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * DD 월드컵 페이지로 이동하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/test/worldcup.do")
public class Worldcup extends HttpServlet {

	/**
	 * DD 월드컵 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req  HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// DD 월드컵 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/test/worldcup/list.jsp");
		dispatcher.forward(req, resp);
	}
}

TestDAO.java

package com.ddstudio.test.repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Random;

import com.ddstudio.DBUtil;
import com.ddstudio.activity.model.AttractionDTO;
import com.ddstudio.test.model.CourseDTO;
import com.ddstudio.test.model.MBTIDTO;

/**
 * 추천(테스트) 관련 데이터베이스 작업을 수행하는 클래스
 * 
 * @author 이승원
 */
public class TestDAO {

	private Connection conn;
	private Statement stat;
	private PreparedStatement pstat;
	private ResultSet rs;

	public TestDAO() {
		this.conn = DBUtil.open();
	}

	/**
	* 코스 추가
	*
	* @param dto 추가할 코스 정보를 담은 CourseDTO 객체
	* @return 데이터베이스에 추가된 행의 수
	*/
	public int courseAdd(CourseDTO dto) {

		try {

			String sql = "INSERT INTO tblCourse (course_seq, name, img) VALUES (seqtblCourse.nextVal, ?, ?)";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getName());
			pstat.setString(2, dto.getImg());

			return pstat.executeUpdate();

		} catch (Exception e) {
			System.out.println("TestDAO.courseAdd()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	* 코스 목록 조회
	*
	* @return 코스 목록을 담은 ArrayList 객체
	*/
	public ArrayList<CourseDTO> listCourse() {

		try {

			String sql = "select * from tblCourse order by course_seq";

			stat = conn.createStatement();
			rs = stat.executeQuery(sql);

			ArrayList<CourseDTO> list = new ArrayList<CourseDTO>();

			while (rs.next()) {

				CourseDTO dto = new CourseDTO();

				dto.setCourse_seq(rs.getString("course_seq"));
				dto.setName(rs.getString("name"));
				dto.setImg(rs.getString("img"));

				list.add(dto);
			}

			return list;

		} catch (Exception e) {
			System.out.println("TestDAO.listCourse()");
			e.printStackTrace();
		}

		return null;
	}

	/**
	* 코스 삭제
	*
	* @param courseSeq 삭제할 코스의 일련번호
	* @return 데이터베이스에 삭제된 행의 수
	*/
	public int deleteCourse(String courseSeq) {
		try {
			// tblMBTI에서 레코드 삭제
			String deleteMbtiSql = "delete from tblMBTI where course_seq = ?";
			pstat = conn.prepareStatement(deleteMbtiSql);
			pstat.setString(1, courseSeq);
			pstat.executeUpdate();

			// tblCourse에서 레코드 삭제
			String deleteCourseSql = "delete from tblCourse where course_seq = ?";
			pstat = conn.prepareStatement(deleteCourseSql);
			pstat.setString(1, courseSeq);

			// tblCourse 삭제 실행 및 결과 반환
			return pstat.executeUpdate();
		} catch (Exception e) {
			System.out.println("TestDAO.deleteCourse()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	* MBTI 상세 정보 조회
	*
	* @param mbti 조회할 MBTI
	* @return 조회된 MBTI 정보를 담은 MBTIDTO 객체
	*/
	public MBTIDTO get(String mbti) {
		try {
			String sql = "select * from vwMBTIDetail where mbti = ?";
			pstat = conn.prepareStatement(sql);
			pstat.setString(1, mbti);

			rs = pstat.executeQuery();

			if (rs.next()) {
				MBTIDTO result = new MBTIDTO();

				result.setMbti_seq(rs.getString("mbti_seq"));
				result.setResult(rs.getString("result"));
				result.setMbti(rs.getString("mbti"));
				result.setCourse_seq(rs.getString("course_seq"));
				result.setCourse_name(rs.getString("course_name"));
				result.setCourse_img(rs.getString("course_img"));
				result.setAttraction_seq(rs.getString("attraction_seq"));
				result.setAttraction_name(rs.getString("attraction_name"));
				result.setAttraction_img(rs.getString("attraction_img"));

				return result;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	/**
	* MBTI 목록 조회
	*
	* @return MBTI 목록을 담은 ArrayList 객체
	*/
	public ArrayList<MBTIDTO> listMBTI() {

		try {

			String sql = "select * from tblMBTI order by mbti_seq";

			stat = conn.createStatement();
			rs = stat.executeQuery(sql);

			ArrayList<MBTIDTO> list = new ArrayList<MBTIDTO>();

			while (rs.next()) {

				MBTIDTO dto = new MBTIDTO();

				dto.setMbti_seq(rs.getString("mbti_seq"));
				dto.setResult(rs.getString("result"));
				dto.setMbti(rs.getString("mbti"));
				dto.setCourse_seq(rs.getString("course_seq"));
				dto.setAttraction_seq(rs.getString("attraction_seq"));

				list.add(dto);
			}

			return list;

		} catch (Exception e) {
			System.out.println("TestDAO.listMBTI()");
			e.printStackTrace();
		}

		return null;
	}

	/**
	* 어트랙션 목록 조회
	*
	* @return 어트랙션 목록을 담은 ArrayList 객체
	*/
	public ArrayList<AttractionDTO> listAttraction() {

		try {

			String sql = "select * from tblAttraction where name not like '%(운영종료)%'";

			stat = conn.createStatement();
			rs = stat.executeQuery(sql);

			ArrayList<AttractionDTO> list = new ArrayList<AttractionDTO>();

			while (rs.next()) {

				AttractionDTO dto = new AttractionDTO();

				dto.setAttraction_seq(rs.getString("attraction_seq"));
				dto.setName(rs.getString("name"));

				list.add(dto);
			}

			return list;

		} catch (Exception e) {
			System.out.println("TestDAO.listAttraction()");
			e.printStackTrace();
		}

		return null;
	}

	/**
	* MBTI별 추천 추가
	*
	* @param dto MBTI 정보를 담은 MBTIDTO 객체
	* @return 데이터베이스에 추가된 행의 수
	*/
	public int mbtiAdd(MBTIDTO dto) {
		try {
			String sql = "INSERT INTO tblMBTI (mbti_seq, result, mbti, course_seq, attraction_seq) VALUES (seqtblMBTI.nextVal, ?, ?, ?, ?)";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, dto.getResult());
			pstat.setString(2, dto.getMbti());
			pstat.setString(3, dto.getCourse_seq());
			pstat.setString(4, dto.getAttraction_seq());

			int result = pstat.executeUpdate();

			return result;
		} catch (Exception e) {
			System.out.println("TestDAO.mbtiAdd()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	* MBTI 전체 조회 (관련 정보 포함)
	*
	* @return MBTI 목록을 담은 ArrayList 객체
	*/
	public ArrayList<MBTIDTO> getAllMBTI() {
		ArrayList<MBTIDTO> mbtiList = new ArrayList<>();

		String sql = "SELECT * FROM vwMBTIDetail ORDER BY mbti_seq";

		try (PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) {

			while (rs.next()) {
				MBTIDTO mbti = new MBTIDTO();
				mbti.setMbti_seq(rs.getString("mbti_seq"));
				mbti.setResult(rs.getString("result"));
				mbti.setMbti(rs.getString("mbti"));
				mbti.setCourse_seq(rs.getString("course_seq"));
				mbti.setCourse_name(rs.getString("course_name"));
				mbti.setCourse_img(rs.getString("course_img"));
				mbti.setAttraction_seq(rs.getString("attraction_seq"));
				mbti.setAttraction_name(rs.getString("attraction_name"));
				mbti.setAttraction_img(rs.getString("attraction_img"));

				mbtiList.add(mbti);
			}
		} catch (Exception e) {
			System.out.println("TestDAO.getAllMBTI()");
			e.printStackTrace();
		}

		return mbtiList;
	}

	/**
	* MBTI 삭제
	*
	* @param mbti_seq 삭제할 MBTI의 일련번호
	* @return 데이터베이스에 삭제된 행의 수
	*/
	public int MBTIDel(String mbti_seq) {

		try {

			String sql = "delete from tblMBTI where mbti_seq = ?";

			pstat = conn.prepareStatement(sql);
			pstat.setString(1, mbti_seq);

			return pstat.executeUpdate();

		} catch (Exception e) {
			System.out.println("TestDAO.MBTIDel()");
			e.printStackTrace();
		}

		return 0;
	}

	/**
	* 어트랙션 목록 조회
	*
	* @return 어트랙션 목록을 담은 ArrayList 객체
	*/
	public ArrayList<AttractionDTO> getAttractionList() {
		
		ArrayList<AttractionDTO> list = new ArrayList<AttractionDTO>();
		
		try {
			String sql = "SELECT a.*, (SELECT img FROM tblAttractionImg WHERE attraction_seq = a.attraction_seq AND rownum = 1) AS img FROM tblAttraction a WHERE a.name NOT LIKE '%(운영종료)%'";

			stat = conn.createStatement();

			// 쿼리 실행 및 결과 처리
			rs = stat.executeQuery(sql);

			while (rs.next()) {
				AttractionDTO dto = new AttractionDTO();
				dto.setAttraction_seq(rs.getString("attraction_seq"));
				dto.setName(rs.getString("name"));
				dto.setImg(rs.getString("img"));

				list.add(dto);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return list;
	}

	/**
	* 코스 목록 조회
	*
	* @return 코스 목록을 담은 ArrayList 객체
	*/
	public ArrayList<CourseDTO> getCourseList() {
		
		ArrayList<CourseDTO> list = new ArrayList<CourseDTO>();

		try {
			String sql = "select * from tblCourse";

			stat = conn.createStatement();

			// 쿼리 실행 및 결과 처리
			rs = stat.executeQuery(sql);

			while (rs.next()) {
				CourseDTO dto = new CourseDTO();
				dto.setCourse_seq(rs.getString("course_seq"));
				dto.setName(rs.getString("name"));
				dto.setImg(rs.getString("img"));

				list.add(dto);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return list;
	}
}

attraction > detail.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>
<head>
<meta charset="UTF-8">
<%@ include file="/WEB-INF/views/inc/asset.jsp"%>
<link rel="stylesheet" href="/ddstudio/asset/css/main.css">
<style>
.button-container {
	display: flex;
}

body {
	overflow-x: hidden;
}

.item {
	width: 50%;
	height: 600px;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	background-color: transparent;
	border-radius: 8px;
	margin: 10px;
	padding: 20px;
	box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
	transition: all 0.35s ease;
	transform-origin: center bottom;
	cursor: pointer;
	font-size: 40px;
	font-weight: 600;
	color: #333;
	position: relative;
	overflow: hidden;
}

.item>div:nth-child(1) {
	background-color: transparent;
}

.item>div:nth-child(2) {
	background-color: transparent;
}

.item div.img-container {
	width: 100%;
	height: 100%;
	background-size: cover;
	background-position: center;
}

#item1:hover {
    transform: rotate(-10deg) scale(0.9) translateX(-10px) translateY(5px);
}

#item2:hover {
    transform: rotate(10deg) scale(0.9) translateX(10px) translateY(5px);
}

#item1:hover, #item2:hover {
    opacity: 0.25;
}

.item:hover .img-container i {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(4);
    opacity: 1;
    color: white;
    transition: opacity 0.3s ease;
}

#attinfo {
	font-size : 18px;
	text-align: center;
	color: #444;
	font-weight: bold;
	margin-bottom: 3px;
}

#result-message {
	margin-top: 40px;
	text-align: center;
	color: #3498db;
	font-weight: bold;
	font-size: 30px;
}

.item h3 {
	margin: 0;
	font-size: 18px;
	color: #333;
	padding: 15px;
}

#title {
	margin-top: 123px;
}

#content {
	margin-top: 50px !important;
	padding: 0;
}

#worldcup-container {
	width: 100%;
	display: flex;
	justify-content: center;
}

#title {
	margin-top: 123px;
	background-image: url('/ddstudio/asset/image/background-7.jpg');
}

#overlay-div {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 400px;
	background-color: black;
	opacity: 0.45; /* 투명도 조절 */
	z-index: 1; /* 다른 요소들보다 위에 위치하도록 설정 */
}

#title h2 {
	color: #EEE;
	z-index: 2;
}
</style>
</head>
<body>
	<%-- /test/worldcup/attraction/detail.jsp --%>
	<%@ include file="/WEB-INF/views/inc/header.jsp"%><!-- Header -->

	<main id="main">
		<div id="title" title="작가 senivpetro 출처 Freepik">
		<div id="overlay-div"></div>
			<h2>어트랙션 월드컵</h2>
		</div>

		<div id="result-info"></div>
		<div id="worldcup-container" class="button-container">
			<!-- 어트랙션을 출력 -->

			<c:forEach var="attraction" items="${selectedTwoAttractions}"
				varStatus="loop">
				<div class="item" id="item${loop.index + 1}"
					onclick="selectAttraction('${attraction.attraction_seq}')">
					<div class="img-container"
						style="background-image: url('/ddstudio/asset/image/attraction/${attraction.img}');"></div>
					<h3>${attraction.name}</h3>
				</div>
			</c:forEach>
		</div>
	</main>

	<%@ include file="/WEB-INF/views/inc/footer.jsp"%>

	<script>
		let selectedTwoAttractions;

		$(document).ready(function() {
			// 페이지가 로드될 때 세션 초기화
			initializeSession();
		});

		function initializeSession() {
			$.ajax({
				type : 'POST',
				url : '/ddstudio/test/worldcup/attraction/detail.do',
				data : {
					'isNewSession' : true
				},
				success : function(data) {
					// console.log('세션 초기화', data);
				},
				error : function(a, b, c) {
					console.error(a, b, c);
				}
			});
		}

		function selectAttraction(attractionSeq) {
			$.ajax({
				type : 'POST',
				url : '/ddstudio/test/worldcup/attraction/detail.do',
				data : {
					'attractionSeq' : attractionSeq
				},
				success : function(data) {
					//console.log('선택한 어트랙션 정보:', data.selectedTwoAttractions);
					//console.log('남은 어트랙션:', data.remainingAttractionSeqs);

					// 전역 변수에 할당
					selectedTwoAttractions = data.selectedTwoAttractions;

					// 어트랙션 정보에 따라 화면 갱신
					if (selectedTwoAttractions.length > 1) {
						refreshScreen();
					} else {
						resultScreen(selectedTwoAttractions[0]);
					}
				},
				error : function(a, b, c) {
					console.error(a, b, c);
				}
			});
		}

		function refreshScreen() {
			//console.log('refreshScreen 함수 호출');

			// 모든 어트랙션을 화면에 갱신
			$('#worldcup-container').empty();
	
			if (selectedTwoAttractions.length == 2) {
				for (let i = 0; i < selectedTwoAttractions.length; i++) {
					const attraction = selectedTwoAttractions[i];
					const imgUrl = attraction.img ? '/ddstudio/asset/image/attraction/' + attraction.img : '쌍용열차.jpg';
	
					// 동적으로 id 생성
					const itemId = 'item' + (i + 1);
	
					const item = $(
							'<div class="item" id="' + itemId
									+ '" onclick="selectAttraction('
									+ attraction.attraction_seq + ')">').append(
							'<div class="img-container" style="background-image: url(\''
									+ imgUrl + '\');"></div>').append(
							'<h3>' + attraction.name + '</h3>');
					$('#worldcup-container').append(item);
				}
			} else {
				const attraction = selectedTwoAttractions[0];
				const imgUrl = attraction.img ? '/ddstudio/asset/image/attraction/' + attraction.img : '쌍용열차.jpg';

				// 동적으로 id 생성
				const itemId = 'item3';

				const item = $(
						'<div class="item" id="' + itemId
								+ '" onclick="selectAttraction('
								+ attraction.attraction_seq + ')">').append(
						'<div class="img-container" style="background-image: url(\''
								+ imgUrl + '\');"></div>').append(
						'<h3>' + attraction.name + '</h3>');
				$('#worldcup-container').append(item);
			}
		}

		function resultScreen(selectedAttraction) {
		    // 어트랙션을 화면에 갱신
		    $('#worldcup-container').empty();

		    const resultContainer = $('<div class="item result-container" id="item3">');
		    const imgContainer = $('<div class="img-container" style="background-image: url(\'/ddstudio/asset/image/attraction/' + selectedAttraction.img + '\');"></div>');
		    const infoname = $('<h3>' + selectedAttraction.name + '</h3>');
		    const message = $('<p id="result-message">최고의 어트랙션이죠!<br>[' + selectedAttraction.name + ']</p>' + '<p id="attinfo">클릭시 해당 어트랙션 페이지로 이동합니다.</p>');

		    // 메시지
		    $('#result-info').append(message);

		    // 최종 선택 어트랙션
		    resultContainer.append(imgContainer).append(infoname);

		    // 클릭 이벤트 처리
		    resultContainer.click(function() {
		        // 어트랙션 상세 페이지로 이동
		        window.location.href = '/ddstudio/activity/attractiondetail.do?seq=' + selectedAttraction.attraction_seq;
		    });

		    // #worldcup-container에 추가
		    $('#worldcup-container').append(resultContainer);
		}
		
		// 이미지 중앙에 우는 아이콘 추가
		$('#worldcup-container').on('mouseenter', '#item1', function () {
		    // 호버 시작
		    const sadFaceIcon = $('<i class="fa-regular fa-face-sad-cry"></i>');
		    $(this).find('.img-container').append(sadFaceIcon);
		});
		
		$('#worldcup-container').on('mouseenter', '#item2', function () {
		    // 호버 시작
		    const sadFaceIcon = $('<i class="fa-regular fa-face-sad-cry"></i>');
		    $(this).find('.img-container').append(sadFaceIcon);
		});

		$('#worldcup-container').on('mouseleave', '#item1', function () {
		    // 호버 종료
		    $(this).find('.img-container .fa-face-sad-cry').remove();
		});

		$('#worldcup-container').on('mouseleave', '#item2', function () {
		    // 호버 종료
		    $(this).find('.img-container .fa-face-sad-cry').remove();
		});
	</script>
</body>
</html>

Search.java

package com.ddstudio.user;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.ddstudio.user.model.SearchDTO;
import com.ddstudio.user.repository.UserDAO;

/**
 * 검색 기능을 제공하는 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/search.do")
public class Search extends HttpServlet {

	/**
	 * 검색 페이지로 이동하는 GET 메서드입니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 검색 페이지 이동
		RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/user/search.jsp");
		dispatcher.forward(req, resp);
	}
	
	/**
	 * 검색 기능을 수행하는 POST 메서드입니다.
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setCharacterEncoding("UTF-8");
		resp.setContentType("application/json");

		PrintWriter out = resp.getWriter();

		try {
			String searchWord = req.getParameter("word"); // 검색어
			
			UserDAO dao = new UserDAO();
			
			// 검색 결과
			ArrayList searchResults = dao.search(searchWord);
			
			JSONArray jsonArray = new JSONArray();

			// 검색 결과를 JSON 객체로 변환하여 배열에 추가
			for (SearchDTO result : searchResults) {
				JSONObject jsonObject = new JSONObject();
				
				jsonObject.put("attractionName", result.getAttractionName());
				jsonObject.put("mbtiResult", result.getMbtiResult());
				jsonObject.put("mbtiMbti", result.getMbtiMbti());
				jsonObject.put("courseName", result.getCourseName());
				jsonObject.put("hashtagName", result.getHashtagName());
				jsonObject.put("restaurantName", result.getRestaurantName());
				jsonObject.put("restaurantMenu", result.getRestaurantMenu());
				jsonObject.put("categoryName", result.getCategoryName());
				jsonObject.put("shopName", result.getShopName());
				jsonObject.put("shopInfo", result.getShopInfo());
				jsonObject.put("itemName", result.getItemName());
				jsonObject.put("itemInfo", result.getItemInfo());
				jsonObject.put("convenientName", result.getConvenientName());
				jsonObject.put("festivalName", result.getFestivalName());
				jsonObject.put("festivalInfo", result.getFestivalInfo());
				jsonObject.put("theaterName", result.getTheaterName());
				jsonObject.put("movieName", result.getMovieName());
				jsonObject.put("noticeSubject", result.getNoticeSubject());
				jsonObject.put("noticeContent", result.getNoticeContent());
				jsonObject.put("benefitName", result.getBenefitName());
				jsonObject.put("benefitType", result.getBenefitType());
				jsonObject.put("faqCategory", result.getFaqCategory());
				jsonObject.put("faqQuestion", result.getFaqQuestion());
				jsonObject.put("faqAnswer", result.getFaqAnswer());

				jsonArray.add(jsonObject);
				// System.out.println(jsonObject);
			}

			// JSON 배열을 응답
			out.print(jsonArray.toJSONString());
		} catch (Exception e) {
			System.out.println("Search.doPost()");
			e.printStackTrace();
		}
	}
}

SearchHashtag.java

package com.ddstudio.user;

import java.io.IOException;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ddstudio.user.repository.UserDAO;

/**
 * 해시태그 검색을 위한 서블릿 클래스입니다.
 * 
 * @author 이승원
 */
@WebServlet("/user/searchhashtag.do")
public class SearchHashtag extends HttpServlet {

	/**
	 * GET 메서드로 요청이 들어올 때 호출되는 메서드입니다.
	 * 해시태그 목록을 클라이언트에 전송합니다.
	 * 
	 * @param req HttpServletRequest 객체
	 * @param resp HttpServletResponse 객체
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		try {
			UserDAO userDAO = new UserDAO();

			// 해시태그 목록
			ArrayList<String> hashtagList = userDAO.getHashtagList();

			// StringBuilder를 사용하여 JSON 형식의 문자열을 생성
			// JSON 배열 형태인 "["로 시작하여 각 해시태그를 쌍따옴표로 묶어 문자열에 추가
			// 해시태그가 목록의 마지막이 아니라면 쉼표(,) 추가
			StringBuilder hashtagJson = new StringBuilder("[");
			for (int i = 0; i < hashtagList.size(); i++) {
				hashtagJson.append("\"").append(hashtagList.get(i)).append("\"");
				if (i < hashtagList.size() - 1) {
					hashtagJson.append(",");
				}
			}
			hashtagJson.append("]");

			// System.out.println(hashtagJson);

			// 클라이언트에 전송
			resp.setContentType("application/json");
			resp.setCharacterEncoding("UTF-8");
			resp.getWriter().write(hashtagJson.toString());

		} catch (Exception e) {
			e.printStackTrace();
			resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}
}

SearchDTO.java

package com.ddstudio.user.model;

/**
 * 검색 조건을 담는 데이터 전송 객체(DTO)입니다.
 * 각 속성은 검색할 대상에 대한 조건을 나타냅니다. 값이 없거나 공백일 경우 " "로 초기화됩니다.
 * 
 * @author 이승원
 */
public class SearchDTO {
	
	private String attractionName; // 어트랙션 이름
	private String mbtiResult; // MBTI 결과
	private String mbtiMbti; // MBTI 유형
	private String courseName; // 코스 이름
	private String hashtagName; // 해시태그 이름
	private String restaurantName; // 음식점 이름
	private String restaurantMenu; // 음식점 대표 메뉴
	private String categoryName; // 카테고리 이름
	private String shopName; // 상점 이름
	private String shopInfo; // 상점 정보
	private String itemName; // 상품 이름
	private String itemInfo; // 상품 정보
	private String convenientName; // 편의시설 이름
	private String festivalName; // 페스티벌 이름
	private String festivalInfo; // 페스티벌정보
	private String theaterName; // 영화관 이름
	private String movieName; // 영화 이름
	private String noticeSubject; // 공지사항 제목
	private String noticeContent; // 공지사항 내용
	private String benefitName; // 혜택 이름
	private String benefitType; // 혜택 유형
	private String faqCategory; // FAQ 카테고리
	private String faqQuestion; // FAQ 질문
	private String faqAnswer; // FAQ 답변

	public String getAttractionName() {
		return (attractionName != null) ? attractionName : " ";
	}

	public void setAttractionName(String attractionName) {
		this.attractionName = (attractionName != null && !attractionName.trim().isEmpty()) ? attractionName : " ";
	}

	public String getMbtiResult() {
		return (mbtiResult != null) ? mbtiResult : " ";
	}

	public void setMbtiResult(String mbtiResult) {
		this.mbtiResult = (mbtiResult != null && !mbtiResult.trim().isEmpty()) ? mbtiResult : " ";
	}

	public String getMbtiMbti() {
		return (mbtiMbti != null) ? mbtiMbti : " ";
	}

	public void setMbtiMbti(String mbtiMbti) {
		this.mbtiMbti = (mbtiMbti != null && !mbtiMbti.trim().isEmpty()) ? mbtiMbti : " ";
	}

	public String getCourseName() {
		return (courseName != null) ? courseName : " ";
	}

	public void setCourseName(String courseName) {
		this.courseName = (courseName != null && !courseName.trim().isEmpty()) ? courseName : " ";
	}

	public String getHashtagName() {
		return (hashtagName != null) ? hashtagName : " ";
	}

	public void setHashtagName(String hashtagName) {
		this.hashtagName = (hashtagName != null && !hashtagName.trim().isEmpty()) ? hashtagName : " ";
	}

	public String getRestaurantName() {
		return (restaurantName != null) ? restaurantName : " ";
	}

	public void setRestaurantName(String restaurantName) {
		this.restaurantName = (restaurantName != null && !restaurantName.trim().isEmpty()) ? restaurantName : " ";
	}

	public String getRestaurantMenu() {
		return (restaurantMenu != null) ? restaurantMenu : " ";
	}

	public void setRestaurantMenu(String restaurantMenu) {
		this.restaurantMenu = (restaurantMenu != null && !restaurantMenu.trim().isEmpty()) ? restaurantMenu : " ";
	}

	public String getCategoryName() {
		return (categoryName != null) ? categoryName : " ";
	}

	public void setCategoryName(String categoryName) {
		this.categoryName = (categoryName != null && !categoryName.trim().isEmpty()) ? categoryName : " ";
	}

	public String getShopName() {
		return (shopName != null) ? shopName : " ";
	}

	public void setShopName(String shopName) {
		this.shopName = (shopName != null && !shopName.trim().isEmpty()) ? shopName : " ";
	}

	public String getShopInfo() {
		return (shopInfo != null) ? shopInfo : " ";
	}

	public void setShopInfo(String shopInfo) {
		this.shopInfo = (shopInfo != null && !shopInfo.trim().isEmpty()) ? shopInfo : " ";
	}

	public String getItemName() {
		return (itemName != null) ? itemName : " ";
	}

	public void setItemName(String itemName) {
		this.itemName = (itemName != null && !itemName.trim().isEmpty()) ? itemName : " ";
	}

	public String getItemInfo() {
		return (itemInfo != null) ? itemInfo : " ";
	}

	public void setItemInfo(String itemInfo) {
		this.itemInfo = (itemInfo != null && !itemInfo.trim().isEmpty()) ? itemInfo : " ";
	}

	public String getConvenientName() {
		return (convenientName != null) ? convenientName : " ";
	}

	public void setConvenientName(String convenientName) {
		this.convenientName = (convenientName != null && !convenientName.trim().isEmpty()) ? convenientName : " ";
	}

	public String getFestivalName() {
		return (festivalName != null) ? festivalName : " ";
	}

	public void setFestivalName(String festivalName) {
		this.festivalName = (festivalName != null && !festivalName.trim().isEmpty()) ? festivalName : " ";
	}

	public String getFestivalInfo() {
		return (festivalInfo != null) ? festivalInfo : " ";
	}

	public void setFestivalInfo(String festivalInfo) {
		this.festivalInfo = (festivalInfo != null && !festivalInfo.trim().isEmpty()) ? festivalInfo : " ";
	}

	public String getTheaterName() {
		return (theaterName != null) ? theaterName : " ";
	}

	public void setTheaterName(String theaterName) {
		this.theaterName = (theaterName != null && !theaterName.trim().isEmpty()) ? theaterName : " ";
	}

	public String getMovieName() {
		return (movieName != null) ? movieName : " ";
	}

	public void setMovieName(String movieName) {
		this.movieName = (movieName != null && !movieName.trim().isEmpty()) ? movieName : " ";
	}

	public String getNoticeSubject() {
		return (noticeSubject != null) ? noticeSubject : " ";
	}

	public void setNoticeSubject(String noticeSubject) {
		this.noticeSubject = (noticeSubject != null && !noticeSubject.trim().isEmpty()) ? noticeSubject : " ";
	}

	public String getNoticeContent() {
		return (noticeContent != null) ? noticeContent : " ";
	}

	public void setNoticeContent(String noticeContent) {
		this.noticeContent = (noticeContent != null && !noticeContent.trim().isEmpty()) ? noticeContent : " ";
	}

	public String getBenefitName() {
		return (benefitName != null) ? benefitName : " ";
	}

	public void setBenefitName(String benefitName) {
		this.benefitName = (benefitName != null && !benefitName.trim().isEmpty()) ? benefitName : " ";
	}

	public String getBenefitType() {
		return (benefitType != null) ? benefitType : " ";
	}

	public void setBenefitType(String benefitType) {
		this.benefitType = (benefitType != null && !benefitType.trim().isEmpty()) ? benefitType : " ";
	}

	public String getFaqCategory() {
		return (faqCategory != null) ? faqCategory : " ";
	}

	public void setFaqCategory(String faqCategory) {
		this.faqCategory = (faqCategory != null && !faqCategory.trim().isEmpty()) ? faqCategory : " ";
	}

	public String getFaqQuestion() {
		return (faqQuestion != null) ? faqQuestion : " ";
	}

	public void setFaqQuestion(String faqQuestion) {
		this.faqQuestion = (faqQuestion != null && !faqQuestion.trim().isEmpty()) ? faqQuestion : " ";
	}

	public String getFaqAnswer() {
		return (faqAnswer != null) ? faqAnswer : " ";
	}

	public void setFaqAnswer(String faqAnswer) {
		this.faqAnswer = (faqAnswer != null && !faqAnswer.trim().isEmpty()) ? faqAnswer : " ";
	}

}

search.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>
<head>
<meta charset="UTF-8">
<%@include file="/WEB-INF/views/inc/asset.jsp"%>
<style>
.nav-icon {
	font-size: 50px !important;
}

#main {
	text-align: center;
	margin-top: 150px;
}

#search-container {
	width: 80%;
	text-align: center;
	border-top: 2px solid black;
	margin: 50px auto 0;
}

#search-form {
	margin-top: 50px;
}

/*
#hashtag-container {
    display: flex;
    text-align: center;
    margin-top: 13px;
    font-size: 18px;
    justify-content: center;
}

.hashtag {
    width: 100px;
    height: 40px;
    margin-right: 10px;
    cursor: pointer;
}

.hashtag:not(:last-child) {
    margin-right: 0;
}
 */
 
#result-container {
	text-align: left;
	margin-top: 20px;
}

.result-title {
    font-size: 30px;
    font-weight: bold;
    color: #333;
    margin-bottom: 10px;
    position: relative;
}

.result-title::before {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 60.7 28.7 32 64 32h96c123.7 0 224 100.3 224 224s-100.3 224-224 224H64c-35.3 0-64-28.7-64-64V96zm160 0H64V416h96c88.4 0 160-71.6 160-160s-71.6-160-160-160z"/></svg>');
    margin-right: 10px;
}

.result-title + .result-title {
    margin-top: 30px;
}

.result-content {
    font-size: 18px;
    color: #555;
    margin-bottom: 30px;
    padding-bottom: 10px;
    border-bottom: 1px solid #EEE;
}

.result-content:last-child {
    border-bottom: none;
}

#result-container > p::before {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>');
   	margin-right: 5px;
    vertical-align: text-top;
}

#result-container > p {
    text-align: center;
    font-size: 18px;
    color: #555;
}

#category {
	width: 100px;
	height: 40px;
	margin-right: 5px;
}

#search-field {
	height: 80px;
	width: 60%;
	padding: 30px;
	font-size: 20px;
	border-radius: 40px;
}

#search-button {
	background-color: transparent;
	border: none;
	outline: none;
}

#search-button span {
	font-size: 40px;
	position: relative;
	top: 13px;
}
</style>
</head>
<body>
	<!-- search.jsp -->
	<%@include file="/WEB-INF/views/inc/header.jsp"%>

	<main id="main">
		<h1>검색</h1>

		<div id="search-container">
			<form method="POST" action="/ddstudio/user/search.do"
				id="search-form">
				<input type="text" name="word" id="search-field" pattern=".{2,}"
					placeholder="두 글자 이상의 단어를 입력하세요." required>

				<button type="submit" id="search-button">
					<span class="material-symbols-outlined">search</span>
				</button>
			</form>
			<!-- 
				<div id="hashtag-container">
					<div id="hashtag">#hashtag</div>
					<div id="hashtag">#hashtag</div>
					<div id="hashtag">#hashtag</div>
				</div>
 			-->
			<div id="result-container"></div>
		</div>
	</main>
	<%@include file="/WEB-INF/views/inc/footer.jsp"%>
	
	<script>
	/*
	    // 페이지 로드 시 해시태그를 랜덤으로 추가
	    function addRandomHashtags(hashtagList) {
	        $('#hashtag-container').empty();
	
	        // 랜덤으로 3개의 해시태그 선택
	        var randomHashtags = getRandomElements(hashtagList, 3);
	
	        // 해시태그를 동적으로 추가
	        $.each(randomHashtags, function (index, hashtag) {
	            var hashtagElement = '<div class="hashtag">#' + hashtag + '</div>';
	            $('#hashtag-container').append(hashtagElement);
	        });
	    }
	
	    // 배열에서 랜덤으로 요소를 선택
	    function getRandomElements(array, numElements) {
	        var shuffledArray = array.slice(); // 복제본을 만들어 셔플링
	        
	        for (var i = shuffledArray.length - 1; i > 0; i--) {
	            var j = Math.floor(Math.random() * (i + 1));
	            // 배열 요소 교환
	            [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
	        }
	        
	        // 처음부터 numElements만큼의 요소 반환
	        return shuffledArray.slice(0, numElements);
	    }
	
	    // 페이지 로드 시 실행
	    $(document).ready(function () {
	        $.ajax({
	            type: 'GET',
	            url: '/ddstudio/user/searchhashtag.do',
	            success: function (data) {
	                // console.log('Hashtag data:', data);
	
	                // 해시태그 추가
	                addRandomHashtags(data);
	            },
	            error: function (a, b, c) {
	                console.error(a, b, c);
	            }
	        });
	    });
	    
	 	// 해시태그 클릭 이벤트 추가
	    $(document).on('click', '#hashtag-container .hashtag', function () {
	        // 클릭된 해시태그의 클래스를 토글
	        $(this).toggleClass('hashtag-clicked');

	        // 클릭된 해시태그가 있으면 검색어 필드에 해당 해시태그 값을 넣고 검색 실행
	        if ($('#hashtag-container .hashtag-clicked').length > 0) {
	            var clickedHashtags = $('#hashtag-container .hashtag-clicked').map(function () {
	                return $(this).text().substring(1); // '#' 제외한 부분만 추출
	            }).get();

	            // 검색어 필드에 클릭된 해시태그 값을 넣음
	            $('#search-field').val(clickedHashtags.join(' '));

	            // 검색 실행
	            $('#search-form').submit();
	        }
	    });
		*/
		
	    // 검색 실행
	    $('#search-form').submit(function(e) {
	        e.preventDefault();
	
	        // 검색어
	        var searchTerm = $('#search-field').val().toLowerCase();
	
	        /*
		    // 해시태그를 클릭한 경우
	        if ($('#hashtag-container .hashtag').hasClass('hashtag-clicked')) {
	            console.log("해시태그 클릭")
	            
	            $('#hashtag-container .hashtag-clicked').removeClass('hashtag-clicked');
	        } else {
	            console.log("해시태그 안 클릭")
	            
	            $('#hashtag-container .hashtag-clicked').removeClass('hashtag-clicked');
	        }
	        */
		     
	        $.ajax({
	            type: 'POST',
	            url: '/ddstudio/user/search.do',
	            data: $('#search-form').serialize(),
	            success: function(data) {
	                // console.log('data:', data);
	                var searchResults = data;
	
	                // 검색 결과를 담을 객체 초기화
	                var resultLists = {};
	
	                // 검색 결과 태그 초기화
	                $('#result-container').empty();
	
	                // 검색 결과 처리 함수
	                var processResults = function (title, list) {
	                    if (list.length > 0) {
	                        $('#result-container').append('<div class="result-title">' + title + '</div>');
	                        var content = '<div class="result-content">';
	                        $.each(list, function (index, item) {
	                            content += item + ', ';
	                        });
	                        content = content.slice(0, -2);
	                        content += '</div>';
	                        $('#result-container').append(content);
	                    }
	                };
	
	                // 검색 결과 반복 처리
	                $.each(searchResults, function(index, result) {
	                    var containsSearchTerm = function(attribute) {
	                        return attribute.toLowerCase().indexOf(searchTerm) !== -1;
	                    };
	
	                    // 각 속성에 대한 처리
	                    $.each(result, function (key, value) {
	                        if (containsSearchTerm(value) && value.trim() !== " ") {
	                            // 결과 배열이 없으면 초기화
	                            if (!resultLists[key]) {
	                                resultLists[key] = [];
	                            }
	                            
	                            // 중복 체크 후 추가
	                            if (!resultLists[key].includes(value)) {
	                                resultLists[key].push(value);
	                            }
	                        }
	                    });
	                });
	
	                // 결과 출력을 위한 한글 설명 매핑
	                var titleMapping = {
	                    attractionName: '어트랙션명',
	                    mbtiResult: 'MBTI 결과',
	                    mbtiMbti: 'MBTI',
	                    courseName: '코스명',
	                    hashtagName: '해시태그',
	                    restaurantName: '식당명',
	                    restaurantMenu: '식당 메뉴',
	                    categoryName: '카테고리명',
	                    shopName: '가게명',
	                    shopInfo: '가게 정보',
	                    itemName: '아이템명',
	                    itemInfo: '아이템 정보',
	                    convenientName: '편의시설명',
	                    festivalName: '축제명',
	                    festivalInfo: '축제 정보',
	                    theaterName: '극장명',
	                    movieName: '영화명',
	                    noticeSubject: '공지 제목',
	                    noticeContent: '공지 내용',
	                    benefitName: '혜택명',
	                    benefitType: '혜택 유형',
	                    faqCategory: 'FAQ 카테고리',
	                    faqQuestion: 'FAQ 질문',
	                    faqAnswer: 'FAQ 답변'
	                };
	
	                // 결과 출력
	                $.each(resultLists, function (key, list) {
	                    processResults(titleMapping[key] || key, list);
	                });
	
	                // 검색 결과가 없는 경우 메시지 출력
	                if ($.isEmptyObject(resultLists)) {
	                    $('#result-container').append('<p>검색 결과가 없습니다.</p>');
	                }
	            },
	            error: function(a, b, c) {
	                console.error(a, b, c);
	            }
	        });
	    });
	</script>

</body>
</html>

어트랙션 리스트

-- 어트랙션 리스트 (seq, name, img)
CREATE OR REPLACE VIEW vwAttractionList
AS
select
    a.attraction_seq,
    a.name as attraction_name,
    ai.img as attraction_img
from tblAttraction a
left join tblAttractionImg ai
on a.attraction_seq = ai.attraction_seq;

MBTI 상세정보

-- MBTI 상세정보 (course, attraction 추가)
CREATE OR REPLACE VIEW vwMBTIDetail
AS
SELECT
    m.mbti_seq,
    m.result,
    m.mbti,
    c.course_seq,
    c.name as course_name,
    c.img as course_img,
    a.attraction_seq,
    a.name as attraction_name,
    ai.img as attraction_img
FROM tblMBTI m
LEFT JOIN tblCourse c
ON m.course_seq = c.course_seq
LEFT JOIN tblAttraction a
ON m.attraction_seq = a.attraction_seq
LEFT JOIN tblAttractionImg ai
ON a.attraction_seq = ai.attraction_seq;

검색

-- 검색
drop view vwSearchAttraction;
select * from vwSearchLocation;

-- 검색 (어트랙션, 영화관, 기프트숍 등 위치정보)
CREATE OR REPLACE VIEW vwSearchLocation
AS
SELECT
	a.name as attraction_name,
	mb.result as mbti_result,
	mb.mbti as mbti_mbti,
	c.name as course_name,
	h.name as hashtag_name,
	r.name as restaurant_name,
	r.menu as restaurant_menu,
	ct.name as category_name,
	s.name as shop_name,
	s.info as shop_info,
	i.name as item_name,
	i.info as item_info,
	co.name as convenient_name,
	f.name as festival_name,
	f.info as festival_info,
	t.name as theater_name,
	m.name as movie_name
FROM tblLocation l
LEFT JOIN tblAttraction a
ON l.location_seq = a.location_seq
LEFT join tblMBTI mb
ON a.attraction_seq = mb.attraction_seq
LEFT JOIN tblCourse c
ON mb.course_seq = c.course_seq
LEFT JOIN tblAttractionHashtag ah
ON a.attraction_seq = ah.attraction_seq
LEFT JOIN tblHashtag h
ON h.hashtag_seq = ah.hashtag_seq
LEFT JOIN tblRestaurant r
ON l.location_seq = r.location_seq
LEFT JOIN tblCategory ct
ON r.category_seq = ct.category_seq
LEFT JOIN tblShop s
ON l.location_seq = s.location_seq
LEFT JOIN tblItem i
ON s.shop_seq = i.shop_seq
LEFT JOIN tblConvenient co
ON l.location_seq = co.location_seq
LEFT JOIN tblFestival f
ON l.location_seq = f.location_seq
LEFT JOIN tblFestivalHashtag fh
ON f.festival_seq = fh.festival_seq
LEFT JOIN tblHashtag h
ON h.hashtag_seq = fh.hashtag_seq
LEFT JOIN tblTheater t
ON l.location_seq = t.location_seq
LEFT JOIN tblMoviePlay mp
ON t.theater_seq = mp.theater_seq
LEFT JOIN tblMovie m
ON mp.movie_seq = m.movie_seq
LEFT JOIN tblMovieHashtag mh
ON m.movie_seq = mh.movie_seq
LEFT JOIN tblHashtag h
on h.hashtag_seq = mh.hashtag_seq;

-- 검색 (공지사항, FAQ, 헤택 등 정보)
CREATE OR REPLACE VIEW vwSearchInfo AS
SELECT 
    subject AS notice_subject, content AS notice_content, null as benefit_name, null AS benefit_type, NULL AS faq_category, NULL AS faq_question, NULL AS faq_answer
FROM tblNotice
UNION ALL
SELECT 
    NULL, NULL, NULL, NULL, type, question, answer
FROM tblFAQ
UNION ALL
SELECT 
    NULL, null, NULL, NULL, NULL, NULL, NULL
FROM tblTicket
UNION ALL
SELECT 
    NULL, null, name, type, NULL, NULL, NULL
FROM tblBenefit;

-- 전체 검색
drop view vwSearch;

CREATE OR REPLACE VIEW vwSearch AS
SELECT
    attraction_name, mbti_result, mbti_mbti, course_name, hashtag_name,
    restaurant_name, restaurant_menu, category_name, shop_name, shop_info, item_name,
    item_info, convenient_name, festival_name, festival_info, theater_name, movie_name,
    null notice_subject, null notice_content, null benefit_name, null benefit_type,
    null faq_category, null faq_question, null faq_answer
FROM vwSearchLocation
UNION ALL
SELECT
    null, null, null, null, null, null, null, null, null, null, null, null,
    null, null, null, null, null,
    notice_subject, notice_content, benefit_name, benefit_type, faq_category,
    faq_question, faq_answer
FROM vwSearchInfo;

프로젝트 측면

프로젝트를 통해 다양한 기능을 구현하면서 HTML, CSS, JavaScript 코드 작성에 대한 실무 경험을 쌓을 수 있었습니다. 이 과정에서 웹 개발에서 기초가 되는 프론트엔드 기술들을 다루면서 사용자 인터페이스를 구현하는 방법을 구체화하였습니다. 더불어 Ajax, JSTL, Open API, Regex와 같은 학습한 기술과 도구를 프로젝트에 적용하면서, 이들의 실제 활용 방법을 이해하고 익힐 수 있었습니다.

팀원들과 협의하여 로그인 관련 기능과 월드컵 및 검색 기능 구현을 맡게 되었습니다. 처음 해보는 업무를 구현해야 한다는 건 어려웠지만, 흥미롭기도 했습니다. 하루 목표를 세우면서 열심히 기능 구현을 위해 노력하였고, 프로젝트 후반에는 MVC 패턴에 능숙해지고, 기능을 구현하는 속도가 빨라진 것을 느낄 수 있었습니다. 업무를 수행하면서 특히, Ajax를 활용한 비동기 통신, JSON 데이터 처리, 그리고 정규식을 활용한 문자열 처리와 같은 다양한 기술을 습득할 수 있었습니다.


보완할 점

놀이공원 사이트에 추가적인 기능이나 보완사항이 있지만, 시간 부족으로 인해 구현하지 못한 부분이 있어 아쉬움이 남았습니다. 검색 내용을 포함하는 데이터를 출력하는데, 이미지 또는 지도 데이터를 출력하거나 링크로 연결하는 등의 기능을 구현하지 못했습니다.

웹 프로젝트에서 팀원들과 문제를 해결하고 다양한 기능을 구현하면서, 개발하는 재미를 느낄 수 있었습니다. 아직 개발자로서 보완해야 할 점이 많지만, 팀원들과의 협업 및 소통 능력을 키우고, 다양한 기술 스택을 습득하여 문제 해결 능력을 갖춘 '프로젝트에 꼭 필요한 개발자'로 성장하고 싶다는 생각이 들었습니다.