Java

JSP와 MVC 패턴 Todo 프로젝트

ryeonng 2024. 7. 12. 17:44

MVC 패턴

MVC (Model-View-Controller) 패턴소프트웨어 셜계 패턴으로, 애플리케이션을 세 가지 주요 구성 요소인 모델(Model), 뷰(View), 컨트롤러(Controller)로 나누어 구현하는 방식을 말한다. 이를 통해 코드의 재사용성과 유지보수성을 높이고, 역할과 책임을 명확히 분리하는데 목적이 있다.

 

소프트웨어 설계 패턴은 소프트웨어 개발에서 자주 발생하는 문제를 해결하기 위해 검증된 재사용 가능한 솔루션이다. 설계 패턴은 객체 지향 설계 원칙을 따르며, 다양한 상황에서 사용될 수 있는 일반적인 템플릿을 제공한다. (디자인패턴 이라고도 한다.)

주요 소프트웨어 설계 패턴
생성 패턴 (Creational Patterns) : 객체 생성 메커니즘을 제공하여 코드의 유연성을 높인다.
구조 패턴 (Structural Patterns) : 클래스와 객체를 조합하여 더 큰 구조를 형성한다.
행위 패턴 (Behavior Patterns) : 객체 간 상호작용과 책임 분담을 정의한다.

MVC 패턴은 무슨 패턴일까?
MVC(Model-View-Controller) 패턴은 1979년에 트리그브 렌스카우그(Trygve Reenskaug)가 제록스 팔로 알토 리서치 센터(Xerox Palo Alto Research Center, PARC)에서 개발한 Smalltalk-80 언어에서 처음 소개되었다. 렌스카우그의 원래 목표는 사용자 인터페이스를 설계하는 데 있어 데이터, 비즈니스 로직, 그리고 사용자 인터페이스를 명확히 분리하는 것이었다. 이로 인해 시스템을 더 쉽게 이해하고 유지보수할 수 있게 되었다.

MVC는 구조 패턴과 행위 패턴을 결합한 복합 패턴으로 볼 수 있다.

 

MVC 패턴은 Model, View, Controller의 앞 글자를 딴 것으로 프로그램을 구성하는 요소들을 모델, 컨트롤, 뷰 로 나누어 설계하는 아키텍처 패턴 중 하나이다.

  • Model(모델) : 애플리케이션의 데이터와 비즈니스 로직을 관리한다. 데이터베이스와의 상호작용을 처리하며, 데이터의 상태를 유지한다.
  • View(뷰) : 사용자 인터페이스를 담당한다. 모델의 데이터를 사용자에게 보여주고, 사용자의 입력을 받아들인다.
  • Controller(컨트롤러) : 사용자의 입력을 처리하고, 모델과 뷰를 연결한다. 사용자의 요청을 받아 적절한 모델을 호출하고, 결과를 뷰에 전달한다.

 

Dynamic Web Project 생성

  • MVC 패턴을 활용한 코드 설계
  • 필요 라이브러리 확인

https://mvnrepository.com/artifact/com.zaxxer/HikariCP

 

HikariCP 라이브러리를 사용하기 위해 다른 추가적인 라이브러리 설정이 필요하다.

 

https://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.21

https://mvnrepository.com/artifact/org.slf4j/slf4j-api/2.0.0-alpha5

https://mvnrepository.com/artifact/org.slf4j/slf4j-simple/2.0.0

 

Connection Pool을 사용하기 위한 DataSource 설계와 conext.xml 파일 사용
context.xml 파일에 DataSource 설정(HikariCP 사용)
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource name="jdbc/MyDB"
              auth="Container"
              type="javax.sql.DataSource"
              factory="com.zaxxer.hikari.HikariJNDIFactory"
              uniqueResourceName="MyDB"
              minimumIdle="5"
              maximumPoolSize="10"
              connectionTimeout="30000"
              idleTimeout="600000"
              maxLifetime="1800000"
              jdbcUrl="jdbc:mysql://localhost:3306/tb_todo?serverTimezone=Asia/Seoul"
              driverClassName="com.mysql.cj.jdbc.Driver"
              username="root"
              password="asd123"/>
</Context>
  • Resource name ="jdbc/MyDB" : JNDI 이름으로 애플리케이션에서 이 이름을 통해 데이터 소스를 참조한다.
  • auth="Container" : 인증 방식으로, 컨테이너에 의해 인증됨을 의미한다.
  • type="javax.sql.DataSource" : 이 리소스가 DataSource 타입임을 명시한다.
  • factory="com.zaxxer.hikari.HikariJNDIFactory" : HikariCP의 JNDI 팩토리 클래스를 사용하여 데이터 소스를 생성한다.
  • uniqueResourceName="MyDB" : HikariCP 설정에 사용되는 고유한 리소스 이름이다.
  • minimumIdle="5" : 최소 연결 수를 5로 설정한다.
  • maximumPoolSize="10" : 최대 연결 수를 10으로 설정한다.
  • connectionTimeout="30000" : 연결을 시도할 때 30초(30000 밀리초)동안 대기한다.
  • idleTimeout="600000" : 유휴 상태의 연결을 10분(600000 밀리초) 동안 유지한다.
  • maxLifetime="1800000" : 연결의 최대 수명을 30분(1800000 밀리초)으로 설정한다.
  • driverClassName="com.mysql.cj.jdbc.Driver" : MySQL JDBC 드라이버 클래스 이름이다.

 

context.xml 파일을 사용하는 이유

- 자원 공유 목적

  • 여러 웹 애플리케이션이 동일한 자원(예 : 데이터소스)을 사용할 때, 이를 중앙에서 관리하고 공유할 수 있다.
  • 각 웹 애플리케이션이 별도로 설정하지 않고도 동일한 설정을 사용할 수 있어, 설정의 중복을 피할 수 있다.

- 환경 설정 분리

  • 애플리케이션 코드와 환경 설정을 분리하여 관리할 수 있다.
  • 데이터베이스 연결 정보와 같은 환경 설정을 코드에서 분리하여 관리하면, 애플리케이션을 다른 환경(예 : 개발, 테스트, 운영)으로 이동할 때 설정만 변경하면 된다.

즉, 대규모 애플리케이션에서는 많은 설정이 필요하다. 이를 중앙에서 관리할 수 있는 방법론이 필요 하였고 관리자나 운영자가 설정을 일관되게 관리하고 변경할 수 있는 메커니즘이 제공하기 위한 기술로 발전하게 되었다.

 

 

context.xml 파일의 역할과 JNDI의 개념

context.xml

  • 서버 시작 시 톰캣 같은 서블릿 컨테이너에 의해 로드된다. 일반적으로 web.xml 파일보다 먼저 실행된다.
  • 이 파일에 정의된 리소스는 메모리에 객체로 올라가며, JNDI 네임스페이스에 이름으로 등록된다.
  • 이러한 설정은 각 애플리케이션에서 공유할 수 있도록 전역적으로 설정된다.

JNDI (Java Naming and Directory Interface)

  • 네이밍 및 디렉토리 서비스를 통해 자원(예 : 데이터소스)을 이름으로 찾고 참조하는 기술이다.
  • 애플리케이션은 JNDI 네임스페이스에 저장된 자원에 접근하여 해당 자원의 객체를 사용할 수 있다.
  • JNDI를 통해 네이밍 서비스에 의해 관리되는 객체(리소스)를 중앙에서 쉽게 관리하고 공유할 수 있다.

중요

context.xml에 기술된 리소스는 내부적으로 파일을 읽어서 객체를 WAS(Web Application Server)의 메모리에 로드하고, 해당 객체(DataSource)는 JNDI API를 활용하여 JNDI 네임스페이스에 설정된 이름으로 등록된다. 이렇게 등록된 객체는 전역적으로 다른 애플리케이션 또는 컴포넌트가 이름을 통해 접근하여 사용할 수 있는 기술이다.

... 생략 
InitialContext ctx = new InitialContext();
// JNDI 네임스페이스에서 "java:comp/env/jdbc/MyDB" 이름으로 데이터 소스를 찾음
dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");

 

JNDI 이름 규칙

java:comp/env : 표준 JNDI 컨텍스트 루트로, 애플리케이션 환경 항목이 배치되는 기본 네임스페이스이다.

Resource 이름 : 리소스 이름은 context.xml 파일에서 정의된 이름과 일치해야 한다.

 

InitialContext 객체의 역할

InitialContext 객체는 JNDI (Java Naming and Directory Interface) API의 기본 클래스 중 하나로, 애플리케이션이 네이밍 및 디렉토리 서비스를 사용할 수 있도록 해준다.

 

BasicDBUtil
package com.tenco.utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class BasicDBUtil {
	
	private static final String URL = "jdbc:mysql://localhost:3306/m_todo?serverTimezone=Asia/Seoul";
	private static final String USER = "root";
	private static final String PASSWORD = "asd123";
	
	public static Connection getConnection() throws SQLException {
		try {
			Class.forName("com.mysql.cj.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return DriverManager.getConnection(URL, USER, PASSWORD);
	}
}

 

테이블 설계
create database if not exists db_todo2;
use db_todo2;

-- 정규화 1, 2, 3 정규 테이블 설계 

-- users 테이블을 생성 
create table if not exists users(
	id int auto_increment primary key, 
    username varchar(50) not null,
    password varchar(255) not null, 
    email varchar(100) not null, 
    created_at timestamp default current_timestamp
);
desc users;
alter table users add constraint unique(username);

-- todos 테이블 생성 
create table if not exists todos(
	  id int auto_increment primary key, 
    title varchar(100) not null, 
    description text, 
    created_at timestamp default current_timestamp,
    due_date date, 
    completed boolean default false,
    user_id int not null,
    foreign key(user_id) references users(id)
);

-- 샘플 데이터 삽입
-- users 테이블에 데이터 삽입
INSERT INTO users (username, password, email) VALUES
('홍길동', 'asd123', 'hong@example.com'),
('김철수', 'asd123', 'kim@example.com'),
('이영희', 'asd123', 'lee@example.com');

-- todos 테이블에 데이터 삽입
INSERT INTO todos (user_id, title, description, due_date, completed) VALUES
(1, '할 일 1', '할 일 1에 대한 설명입니다.', '2023-12-31', FALSE),
(1, '할 일 2', '할 일 2에 대한 설명입니다.', '2024-01-15', TRUE),
(2, '할 일 3', '할 일 3에 대한 설명입니다.', '2024-02-28', FALSE),
(3, '할 일 4', '할 일 4에 대한 설명입니다.', '2024-03-10', TRUE);

select * from users;
select * from todos;
show processlist;

 

User 부분 처리

UserController
package com.tenco.controller;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

import com.tenco.model.UserDAO;
import com.tenco.model.UserDAOImpl;
import com.tenco.model.UserDTO;


@WebServlet("/user/*")
public class UserController extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private UserDAO userDAO;

	public UserController() {
		super();
	}

	@Override
	public void init() throws ServletException {
		userDAO = new UserDAOImpl();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String action = request.getPathInfo();

		switch (action) {
		case "/signIn":
			request.getRequestDispatcher("/WEB-INF/views/signIn.jsp").forward(request, response);
			break;
		case "/signUp":
			request.getRequestDispatcher("/WEB-INF/views/signUp.jsp").forward(request, response);
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String action = request.getPathInfo();
		switch (action) {
		case "/signIn":
			signIn(request, response);
			break;
		case "/signUp":
			signUp(request, response);
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}
	
	/**
	 * 로그인 처리 기능 
	 * @param request
	 * @param response
	 * @throws IOException 
	 */
	private void signIn(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		
		if(username == null || password.trim().isEmpty()) {
			response.sendRedirect("signIn?message=invalid");
			return; 
		} 
		
		UserDTO user = userDAO.getUserByUsername(username);
		if(user != null && user.getPassword().equals(password)) {
			HttpSession session = request.getSession();
			session.setAttribute("principal", user);
			response.sendRedirect("/mvc/todo/todoForm");  
		} else {
			response.sendRedirect("signIn?message=invalid");
		}
	}

	/**
	 * 회원 가입 기능 
	 * @param request
	 * @param response
	 * @throws IOException 
	 * @throws ServletException 
	 */
	private void signUp(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String email = request.getParameter("email");

		if(username == null || username.trim().isEmpty()) {
			request.setAttribute("errorMessage", "사용자 이름을 입력하시오");
			request.getRequestDispatcher("/WEB-INF/views/signUp.jsp").forward(request, response);
			return;
		}
		// 방어적 코드 작성 (password) - 생략 
		// 방어적 코드 작성 (email) - 생략 
		
		UserDTO userDTO = UserDTO.builder()
				.username(username)
				.password(password)
				.email(email)
				.build();
		
		int resultRowCount = userDAO.addUser(userDTO);
		
		if(resultRowCount == 1 ) {
			response.sendRedirect("signIn?message=success");
		} else {
			response.sendRedirect("signUp?message=error");
		}
	}
}

 

UserDTO
package com.tenco.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * DTO - 데이터를 변환하고 담는 개념 , 메서드를 사용할 수 있다.
 */

@Data 
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class UserDTO {
	
	private int id;
	private String username;
	private String email;
	private String password;
	private String createdAt;
	
	// 필요 시, 메서드 정의 가능
}

 

UserDAO
package com.tenco.model;

import java.util.List;

public interface UserDAO {
	
	int addUser(UserDTO userDTO);
	UserDTO	getUserById(int id);
	UserDTO getUserByUsername(String username);
	List<UserDTO> getAllUsers();
	int updateUser(UserDTO user, int principalId); 
	int deleteUser(int id); 
}

 

UserDAOImpl
package com.tenco.model;

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

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

public class UserDAOImpl implements UserDAO {

	private DataSource dataSource;

	public UserDAOImpl() {

		try {
			InitialContext ctx = new InitialContext(); // 예외처리
			dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB"); // 다운캐스팅
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}

	@Override
	public int addUser(UserDTO userDTO) { // DB에 넣을 부분 작성
		int resultCount = 0; // 지역변수 선언
		String sql = " INSERT INTO users(username, password, email) VALUES (?,?,?) ";
		try (Connection conn = dataSource.getConnection()) {
			// 트랜잭션 시작
			conn.setAutoCommit(false); // 수동 커밋
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				pstmt.setString(1, userDTO.getUsername());
				pstmt.setString(2, userDTO.getPassword());
				pstmt.setString(3, userDTO.getEmail());
				resultCount = pstmt.executeUpdate();
				// 트랜잭션 커밋
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			} // end of PreparedStatement
		} catch (Exception e) {
			e.printStackTrace();
		} // end of Connection
		return resultCount;
	}

	/**
	 * SELECT 구문에는 일단 트랜잭션 처리를 하지 말자.
	 * 하지만, 팬텀리드 현상(정합성을 위해 처리 하는 것도 옳은 방법이다.)
	 */
	@Override
	public UserDTO getUserById(int id) {
		String sql = " select * from users where id = ? ";
		UserDTO userDTO = null;
		try (Connection conn = dataSource.getConnection()) {
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				pstmt.setInt(1, id);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next()) {
					userDTO = new UserDTO();
					userDTO.setId(rs.getInt("id"));
					userDTO.setUsername(rs.getString("username"));
					userDTO.setPassword(rs.getString("password"));
					userDTO.setEmail(rs.getString("email"));
					userDTO.setCreatedAt(rs.getString("created_at"));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("UserDTO : " + userDTO.toString());
		return userDTO;
	}

	@Override
	public UserDTO getUserByUsername(String username) {
		String sql = " select * from users where username = ? ";
		UserDTO userDTO = null;
		try (Connection conn = dataSource.getConnection()) {
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				pstmt.setString(1, username);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next()) {
					userDTO = new UserDTO();
					userDTO.setId(rs.getInt("id"));
					userDTO.setUsername(rs.getString("username"));
					userDTO.setPassword(rs.getString("password"));
					userDTO.setEmail(rs.getString("email"));
					userDTO.setCreatedAt(rs.getString("created_at"));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			// TODO - 삭제 예정
			System.out.println("UserDTO By Username : " + userDTO.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}

		return userDTO;
	}

	@Override
	public List<UserDTO> getAllUsers() {
		String sql = " select * from users ";
		// 자료구조를 사용할 때, 일단 생성 시키자.
		List<UserDTO> list = new ArrayList<UserDTO>();
		try (Connection conn = dataSource.getConnection()) {
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				ResultSet rs = pstmt.executeQuery();
				while (rs.next()) {
					UserDTO userDTO = new UserDTO();
					userDTO.setId(rs.getInt("id"));
					userDTO.setUsername(rs.getString("username"));
					userDTO.setPassword(rs.getString("password"));
					userDTO.setEmail(rs.getString("email"));
					userDTO.setCreatedAt(rs.getString("created_at"));
					list.add(userDTO);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("UserList All  : " + list.toString());
		return list;
	}

	@Override
	public int updateUser(UserDTO user, int principalId) {
		int rowCount = 0; 
		String sql = " update users set password = ?, email = ?  where id = ? ";
		try (Connection conn = dataSource.getConnection()){
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(sql)){
				pstmt.setString(1, user.getPassword());
				pstmt.setString(2, user.getEmail());
				pstmt.setInt(3, principalId);
				rowCount = pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return rowCount;
	}

	@Override
	public int deleteUser(int id) {
		int rowCount = 0; 
		String sql = " delete from users where id = ? ";
		try (Connection conn = dataSource.getConnection()){
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(sql)){
				pstmt.setInt(1, id);
				rowCount = pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return rowCount;
	}

}

 

Todo 부분 처리

TodoController
package com.tenco.controller;

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

import com.tenco.model.TodoDAO;
import com.tenco.model.TodoDAOImpl;
import com.tenco.model.TodoDTO;
import com.tenco.model.UserDTO;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@WebServlet("/todo/*")
public class TodoController extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private TodoDAO todoDAO;

	public TodoController() {
		todoDAO = new TodoDAOImpl();
	}
	
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String action = request.getPathInfo();
		HttpSession session = request.getSession();
		UserDTO principal = (UserDTO) session.getAttribute("principal");
		if (principal == null) {
			response.sendRedirect("/mvc/user/signIn?message=invalid");
			return;
		}

		switch (action) {
		case "/todoForm":
			todoFormPage(request, response);
			break;
		case "/list":
			todoListPage(request, response, principal.getId());
			break;
		case "/detail":
			todoDetailPage(request, response, principal.getId());
			break;
		case "/delete":
			deleteTodo(request, response, principal.getId());
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}

	/**
	 * todo 삭제 기능
	 * 
	 * @param request
	 * @param response
	 * @param principalId
	 * @throws IOException
	 */
	private void deleteTodo(HttpServletRequest request, HttpServletResponse response, int principalId)
			throws IOException {
		try {
			int todoId = Integer.parseInt(request.getParameter("id"));
			todoDAO.deleteTodo(todoId, principalId);
		} catch (Exception e) {
			response.sendRedirect(request.getContextPath() + "/todo/list?error=invalid");
		}

		response.sendRedirect(request.getContextPath() + "/todo/list");
	}

	/**
	 * 상세 보기 화면
	 * 
	 * @param request
	 * @param response
	 * @throws IOException 
	 */
	private void todoDetailPage(HttpServletRequest request, HttpServletResponse response, int principalId) throws IOException {
		try {
			int todoId = Integer.parseInt(request.getParameter("id"));
			TodoDTO dto = todoDAO.getTodoById(todoId);
			if(dto.getUserId() == principalId) {
				request.setAttribute("todo", dto);
				request.getRequestDispatcher("/WEB-INF/views/todoDetail.jsp").forward(request, response);
			} else {
				 response.setContentType("text/html; charset=UTF-8");
				 PrintWriter out = response.getWriter();
				 out.println("<script> alert('권한이 없습니다'); history.back();  </script>");
			}
		} catch (Exception e) {
			response.sendRedirect(request.getContextPath() + "/todo/list?error=invalid");
		}
	}

	/**
	 * 사용자별 todo 리스트 화면 이동
	 * 
	 * @param request
	 * @param response
	 * @param principalId
	 * @throws IOException
	 * @throws ServletException
	 */
	private void todoListPage(HttpServletRequest request, HttpServletResponse response, int principalId)
			throws IOException, ServletException {
		List<TodoDTO> list = todoDAO.getTodosByUserId(principalId);
		request.setAttribute("list", list);
		request.getRequestDispatcher("/WEB-INF/views/todoList.jsp").forward(request, response);
	}

	/**
	 * todo 작성 페이지 이동
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 * @throws ServletException
	 */
	private void todoFormPage(HttpServletRequest request, HttpServletResponse response)
			throws IOException, ServletException {
		request.getRequestDispatcher("/WEB-INF/views/todoForm.jsp").forward(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		HttpSession session = request.getSession();
		UserDTO principal = (UserDTO) session.getAttribute("principal");
		if (principal == null) {
			response.sendRedirect(request.getContextPath() + "/user/signIn?error=invalid");
			return;
		}
		String action = request.getPathInfo();

		switch (action) {
		case "/add":
			addTodo(request, response, principal.getId());
			break;
		case "/update":
			updateTodo(request, response, principal.getId());
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}
	
	/**
	 * todo 수정 기능 
	 * @param request
	 * @param response
	 * @param principalId - 세션 ID 값 
	 * @throws IOException
	 */
	private void updateTodo(HttpServletRequest request, HttpServletResponse response, int principalId) throws IOException {
		
		try {
			int todoId = Integer.parseInt(request.getParameter("id"));
			String title = request.getParameter("title");
			String description = request.getParameter("description");
			String dueDate = request.getParameter("dueDate");
			boolean completed = "on".equalsIgnoreCase(request.getParameter("completed"));
			
			TodoDTO dto = TodoDTO.builder()
					.id(todoId)
					.userId(principalId)
					.title(title)
					.description(description)
					.dueDate(dueDate)
					.completed(String.valueOf(completed)).build();
			
			todoDAO.updateTodo(dto, principalId);
			
		} catch (Exception e) {
			 response.setContentType("text/html; charset=UTF-8");
			 PrintWriter out = response.getWriter();
			 out.println("<script> alert('잘못된 요청입니다'); history.back();  </script>");
		}
		// list 화면 재 요청 처리 
		response.sendRedirect(request.getContextPath() + "/todo/list");
	}

	/**
	 * 세션별 사용자 todo 등록
	 * 
	 * @param request
	 * @param response
	 * @param principalId : 세션에 담겨 있는 UserId 값
	 * @throws IOException
	 */
	private void addTodo(HttpServletRequest request, HttpServletResponse response, int principalId) throws IOException {
		String title = request.getParameter("title");
		String description = request.getParameter("description");
		String dueDate = request.getParameter("dueDate");
		boolean completed = "on".equalsIgnoreCase(request.getParameter("completed"));
		TodoDTO dto = TodoDTO.builder().userId(principalId).title(title).description(description).dueDate(dueDate)
				.completed(String.valueOf(completed)).build();
		todoDAO.addTodo(dto, principalId);
		response.sendRedirect(request.getContextPath() + "/todo/list");
	}
}

 

TodoDTO
package com.tenco.model;




import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class TodoDTO {
	private int id;
	private int userId;
	private String title;
	private String description;
	private String dueDate;
	private String completed; // "0" , "1"
	
	// completed를 데이터 변환하는 메서드를 만들어 보자.
	public String completedToString() {
		return completed.equals("1") ? "true" : "false";
	}
}

 

TodoDAO
package com.tenco.model;

import java.util.List;

public interface TodoDAO {
	void addTodo(TodoDTO dto, int principalId);
	TodoDTO getTodoById(int id);
	List<TodoDTO> getTodosByUserId(int userId);
	List<TodoDTO> getAllTodos();
	void updateTodo(TodoDTO dto, int principalId);
	void deleteTodo(int id, int principalId); 
}

 

TodoDAOImpl
package com.tenco.model;

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

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

public class TodoDAOImpl implements TodoDAO {

	private DataSource dataSource;

	public TodoDAOImpl() {
		try {
			InitialContext ctx = new InitialContext();
			dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void addTodo(TodoDTO dto, int principalId) {
		String sql = " INSERT INTO todos(user_id, title, description, due_date, completed) "
				   + " VALUES(? , ? , ? , ? , ?) ";
		try (Connection conn = dataSource.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				pstmt.setInt(1, principalId);
				pstmt.setString(2, dto.getTitle());
				pstmt.setString(3, dto.getDescription());
				pstmt.setString(4, dto.getDueDate());
				pstmt.setInt(5, dto.getCompleted() == "true" ? 1 : 0 );
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				e.printStackTrace();
				conn.rollback();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public TodoDTO getTodoById(int id) {
		String sql = " SELECT * FROM todos WHERE id = ? ";
		TodoDTO dto = null;
		try (Connection conn = dataSource.getConnection()) {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setInt(1, id);
			try (ResultSet rs = pstmt.executeQuery()) {
				if (rs.next()) {
					dto = new TodoDTO();
					dto.setId(rs.getInt("id"));
					dto.setUserId(rs.getInt("user_id"));
					dto.setTitle(rs.getString("title"));
					dto.setDescription(rs.getString("description"));
					dto.setDueDate(rs.getString("due_date"));
					dto.setCompleted(rs.getString("completed"));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return dto;
	}

	@Override
	public List<TodoDTO> getTodosByUserId(int userId) {

		String sql = " SELECT * FROM todos WHERE user_id = ? ";
		List<TodoDTO> todos = new ArrayList<>();
		try (Connection conn = dataSource.getConnection()) {
			PreparedStatement pstm = conn.prepareStatement(sql);
			pstm.setInt(1, userId);
			try (ResultSet rs = pstm.executeQuery()) {
				while (rs.next()) {
					TodoDTO dto = new TodoDTO();
					dto.setId(rs.getInt("id"));
					dto.setUserId(rs.getInt("user_id"));
					dto.setTitle(rs.getString("title"));
					dto.setDescription(rs.getString("description"));
					dto.setDueDate(rs.getString("due_date"));
					dto.setCompleted(rs.getString("completed"));
					todos.add(dto);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}

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

	@Override
	public List<TodoDTO> getAllTodos() {
		String sql = " SELECT * FROM todos ";
		List<TodoDTO> todos = new ArrayList<>();
		try (Connection conn = dataSource.getConnection()) {
			PreparedStatement pstm = conn.prepareStatement(sql);
			try (ResultSet rs = pstm.executeQuery()) {
				while (rs.next()) {
					TodoDTO dto = new TodoDTO();
					dto.setId(rs.getInt("id"));
					dto.setUserId(rs.getInt("user_id"));
					dto.setTitle(rs.getString("title"));
					dto.setDescription(rs.getString("description"));
					dto.setDueDate(rs.getString("due_date"));
					dto.setCompleted(rs.getString("completed"));
					todos.add(dto);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return todos;
	}

	@Override
	public void updateTodo(TodoDTO dto, int principalId) {
		// SELECT <-- 있지 없지 확인 과정 (필요)

		String sql = " UPDATE todos SET title = ?, description = ? " + ", due_date = ?, completed = ? "
				+ "    WHERE id = ? and user_id = ? ";
		try (Connection conn = dataSource.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
				pstmt.setString(1, dto.getTitle());
				pstmt.setString(2, dto.getDescription());
				pstmt.setString(3, dto.getDueDate());
				pstmt.setInt(4, dto.getCompleted() == "true" ? 1 : 0);
				pstmt.setInt(5, dto.getId());
				pstmt.setInt(6, dto.getUserId());
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 삭제 기능 
	 * id - Todos  Pk 
	 * principalId - 세션 ID 
	 */
	@Override
	public void deleteTodo(int id, int principalId) {
		String sql = " DELETE FROM todos "
				   + " WHERE id = ? "
			       + " 		AND user_id = ? " ;
		try (Connection conn = dataSource.getConnection()){
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(sql)){
				pstmt.setInt(1, id);
				pstmt.setInt(2, principalId);
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

 

JSP 부분 처리

wabapp/views/signUp.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<link rel="stylesheet" type="text/css" href="../css/styles.css">
</head>
<body>
	<h2>회원가입</h2>
	<!-- 에러 메세지 출력 -->
	<%
		// String errorMessage = (String)request.getAttribute("message");
		String errorMessage = (String)request.getParameter("message"); // 쿼리스트링에서는 getParameter로 뽑아낸다.
	
		if(errorMessage != null) {
			
	%>
		<p style="color:red"> <%=errorMessage%></p>
	<% } %>
	<!-- 절대경로 사용해보기 -->	
	<form action="/mvc/user/signUp" method="post">
		<label for="username">사용자 이름 : </label>
		<input type="text" id="username" name="username" value="야스오1">
		
		<label for="password">비밀번호 : </label>
		<input type="password" id="password" name="password" value="1234"> <!-- password타입은 password로 -->
		
		<label for="email">이메일 : </label>
		<input type="text" id="email" name="email" value="abc@nate.com">
		
		<button type="submit">회원가입</button>
	</form>
</body>
</html>

 

webapp/views/signIn.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
	<h2>로그인</h2>
	<!-- 회원 가입 성공 메세지 출력 -->
	<%
		// String errorMessage = (String)request.getAttribute("message");
		String success = (String)request.getParameter("message");
		if(success != null) {	
	%>
		<p style="color:blue"> <%=success%></p>	
	
	<% } %>
	<!-- 절대 경로 사용 해보기  -->
	<form action="/mvc/user/signIn" method="post">
		<label for="username">사용자 이름 : </label>
		<input type="text" id="username" name="username" value="야스오1">
		<label for="password">비밀번호: </label>
		<input type="password" id="password" name="password" value="1234">
		
		<button type="submit">로그인</button>
	</form>	
</body>
</html>

 

webapp/views/todoForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>새 할 일 추가</title>
</head>
<body>
	<h1> ToDo Page </h1>
	<%-- http://localhost:8080/mvc/todo/add --%>
	<form action="add" method="post">
		<label for="title">제목: </label>
		<input type="text" id="title" name="title" value="코딩연습 무한반복">
		<br><br>
		<label for="description">설명: </label>
		<textarea rows="30" cols="50" id="description" name="description" >
			그래야 성공하고 높은 연봉은 기본 ... 아니면 워라벨  
		</textarea>
		<br><br>
		<label for="dueDate">마감기한: </label>
		<input type="date" id="dueDate" name="dueDate" value="2024-07-11">
		<br><br>
		<label for="completed">완료 여부: </label>
		<input type="checkbox" id="completed" name="completed" ><br>
		<button type="submit">추가</button>
	</form>
	<br><br>
	<a href="list">목록으로 돌아가기</a>
	
</body>
</html>

 

webapp/views/todoList.jsp
<%@page import="java.util.Date"%>
<%@page import="com.tenco.model.TodoDTO"%>
<%@page import="java.util.ArrayList"%>
<%@page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>할 일 목록</title>
<link rel="stylesheet" type="text/css" href="../css/styles.css">
</head>
<body>
	<%
	List<TodoDTO> todoList = (List<TodoDTO>)request.getAttribute("list");
	if (todoList != null && !todoList.isEmpty()) {
	%>
	<h2>할 일 목록</h2>
	<a href="todoForm"> 새 할일 추가</a>

	<table border="1">
		<tr>
			<th>제목</th>
			<th>설명</th>
			<th>마감일</th>
			<th>완료 여부</th>
			<th>(액션-버튼)</th>
		</tr>
		<%
		for (TodoDTO todo : todoList) {
		%>
		<tr>
			<td><%=todo.getTitle()%></td>
			<td><%=todo.getDescription()%></td>
			<td><%=todo.getDueDate()%></td>
			<td><%=todo.completedToString() == "true" ? "완료" : "미완료"%></td>
			<td><a href="detail?id=<%=todo.getId()%>">상세보기</a>
				<form action="delete" method="get">
					<input type="hidden" name="id" value="<%=todo.getId()%>">
					<button type="submit">삭제</button>
				</form></td>
		</tr>
		<%
		}
		%>
	</table>
	<%
	} else {
	%>

	<hr>
	<p>등록된 할 일이 없습니다.</p>

	<%
	}
	%>
</body>
</html>

 

webapp/views/todoDetail.jsp
<%@page import="com.tenco.model.TodoDTO"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>상세보기 화면</title>
</head>
<body>
	<h2>상세 보기</h2>
	<%
		TodoDTO todo = (TodoDTO)request.getAttribute("todo");
		if(todo != null) {
	%>
			
		<p>제목 : <%=todo.getTitle() %></p><br>
		<p>설명 : <%=todo.getDescription() %></p><br>		
		<p>마감일 : <%=todo.getDueDate() %></p><br>		
		<p>완료여부 : <%=todo.completedToString() == "true" ? "완료" : "미완료" %></p><br>		
		<hr><br>
		<form action="update" method="post">
			<input type="hidden" name="id" value="<%=todo.getId() %>">
			<label for="title">제목 : </label>
			<input type="text" id="title" name="title" value="<%=todo.getTitle() %>">
			<br>
			<label for="description">설명 : </label>
			<input type="text" id="description" name="description" value="<%=todo.getDescription() %>">
			<br>
			<label for="dueDate">마감일 : </label>
			<input type="date" id="dueDate" name="dueDate" value="<%=todo.getDueDate() %>">
			<br>
			<label for="completed">완료여부 : </label>
			<input type="checkbox" id="completed" name="completed"  <%= todo.completedToString() == "true" ? "checked" : "" %> >
			<br>
			<button type="submit">수정</button>
		</form>		
			
	<%
		} else {
			out.print("<p> 정보를 불러오는데 실패 </p>");
		}
	%>
</body>
</html>

 

'Java' 카테고리의 다른 글

람다식(Lambda expression)  (2) 2024.09.13
JSTL 을 활용한 게시판 기능 만들기  (1) 2024.07.15
네트워크 프로토콜  (0) 2024.07.12
1: N 소켓 양방향 통신  (0) 2024.07.12
1:1 양방향 통신(채팅 기본 기능 구현)  (0) 2024.07.12