JSP
detail.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:set var="pageTitle" value="Article Detail" />
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<div class="card bg-base-100 shadow">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h2 class="card-title text-2xl">${article.title}</h2>
<div class="badge badge-outline">#${article.id}</div>
</div>
<p class="text-sm text-base-content/70">등록: ${article.regDate} · 수정: ${article.updateDate}</p>
<div class="prose max-w-none whitespace-pre-wrap">${article.body}</div>
<div class="divider"></div>
<div class="flex gap-2 justify-end">
<c:if test="${article.userCanModify}">
<a href="/usr/article/modify?id=${article.id}" class="btn btn-warning">수정</a>
<button class="btn btn-error"
onclick="onDelete(${article.id})">삭제</button>
</c:if>
<a href="/usr/article/list" class="btn">목록</a>
</div>
</div>
</div>
<script>
async function onDelete(id){
const ok = await confirmAsync("정말 삭제하시겠어요?");
if(!ok) return;
location.href = "/usr/article/doDelete?id=" + id;
}
</script>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
list.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:set var="pageTitle" value="Article List" />
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<div class="card bg-base-100 shadow">
<div class="card-body gap-4">
<div class="flex flex-col md:flex-row md:items-center gap-3 justify-between">
<div class="join">
<input id="searchInput" type="text" class="input input-bordered join-item" placeholder="제목 검색…" />
<button id="searchBtn" class="btn btn-primary join-item">검색</button>
</div>
<c:if test="${rq.isLogined() }">
<a href="/usr/article/write" class="btn btn-secondary">새 글 작성</a>
</c:if>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>ID</th>
<th>등록일</th>
<th>제목</th>
<th>작성자ID</th>
</tr>
</thead>
<tbody id="articleTbody">
<c:forEach var="article" items="${articles}">
<tr class="hover cursor-pointer"
onclick="location.href='/usr/article/detail?id=${article.id}'">
<td>${article.id}</td>
<td>${article.regDate}</td>
<td class="text-primary">${article.title}</td>
<td>${article.memberId}</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
<div class="join self-end">
<button class="btn join-item">«</button>
<button class="btn join-item btn-active">1</button>
<button class="btn join-item">2</button>
<button class="btn join-item">»</button>
</div>
</div>
</div>
<script>
$("#searchBtn").on("click", function(){
const q = $("#searchInput").val().trim();
if(!q) return showToast("검색어를 입력하세요", "warning");
location.href = "/usr/article/list?searchKeyword=" + encodeURIComponent(q);
});
</script>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
modify.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<c:set var="pageTitle" value="Modify Article" />
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<form action="/usr/article/doModify" method="post" data-safe-submit>
<input type="hidden" name="id" value="${article.id}" />
<div class="form-control mb-4">
<label class="label"><span class="label-text">제목</span></label>
<input name="title" type="text" class="input input-bordered" value="${article.title}" required />
</div>
<div class="form-control mb-6">
<label class="label"><span class="label-text">내용</span></label>
<textarea name="body" class="textarea textarea-bordered h-40" required>${article.body}</textarea>
</div>
<div class="flex gap-2 justify-end">
<a href="/usr/article/detail?id=${article.id}" class="btn">취소</a>
<button type="submit" class="btn btn-primary">수정</button>
</div>
</form>
</div>
</div>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
write.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<c:set var="pageTitle" value="Write Article" />
<div class="card bg-base-100 shadow">
<div class="card-body">
<form action="/usr/article/doWrite" method="post" data-safe-submit>
<div class="form-control mb-4">
<label class="label"><span class="label-text">제목</span></label>
<input name="title" type="text" class="input input-bordered" placeholder="제목을 입력하세요" required />
</div>
<div class="form-control mb-6">
<label class="label"><span class="label-text">내용</span></label>
<textarea name="body" class="textarea textarea-bordered h-40" placeholder="내용을 입력하세요" required></textarea>
</div>
<div class="flex gap-2 justify-end">
<a href="/usr/article/list" class="btn">취소</a>
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
</div>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
</main>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div id="toast" class="toast toast-end hidden z-50"></div>
<dialog id="confirmModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">확인</h3>
<p id="confirmMessage" class="py-4">이 작업을 진행할까요?</p>
<div class="modal-action">
<form method="dialog" class="flex gap-2">
<button class="btn btn-outline">취소</button>
<button id="confirmOkBtn" class="btn btn-primary">확인</button>
</form>
</div>
</div>
</dialog>
</body>
</html>
head.jspf
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${pageTitle != null ? pageTitle : 'App'}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="/resource/common.css" />
<script defer src="/resource/common.js"></script>
</head>
<body class="min-h-screen bg-base-200 text-base-content">
<header class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<a href="/home/main" class="btn btn-ghost text-xl">MyApp</a>
</div>
<div class="flex-none gap-2">
<a href="/usr/article/list" class="btn btn-ghost">Articles</a>
<a href="/usr/member/join" class="btn btn-ghost">Join</a>
<c:if test="${!rq.isLogined() }">
<a href="/usr/member/login" class="btn btn-primary">Login</a>
</c:if>
<c:if test="${rq.isLogined() }">
<a href="/usr/member/doLogout" class="btn btn-primary">Logout</a>
</c:if>
<label class="swap swap-rotate ml-2">
<input id="themeToggle" type="checkbox" />
<svg class="swap-on fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.64 17l-.71.71a9 9 0 1012.72-12.72l-.71.71A7 7 0 115.64 17z"/></svg>
<svg class="swap-off fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6.76 4.84l-1.8-1.79-1.42 1.41 1.79 1.8 1.43-1.42zM1 13h3v-2H1v2zm10-9h2V1h-2v3zM4.96 19.78l1.8-1.79-1.42-1.41-1.79 1.8 1.41 1.4zM20 13h3v-2h-3v2zm-7 10h2v-3h-2v3zm6.24-4.22l1.41-1.4-1.79-1.8-1.41 1.41 1.79 1.79zM12 6a6 6 0 100 12A6 6 0 0012 6z"/></svg>
</label>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<h1 class="text-2xl md:text-3xl font-bold mb-6">${pageTitle}</h1>
main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:set var="pageTitle" value="MAIN PAGE"></c:set>
<%@ include file="../common/head.jspf"%>
<h1>Spring Boot + JSP 정상 작동!</h1>
<div><div>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nobis in eaque iure ut accusantium soluta pariatur dolorem consequatur ipsam ab. Optio et error aperiam quos eos possimus commodi aspernatur nemo.</div>
<div>Doloremque fugit eos veritatis fugiat explicabo minus dolore necessitatibus et eum voluptatibus autem ipsum assumenda atque numquam aspernatur quibusdam neque qui aut sit eligendi. Non magnam voluptate enim odit sapiente.</div>
</div>
<%@ include file="../common/foot.jspf"%>
join.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<c:set var="pageTitle" value="Join" />
<div class="card bg-base-100 shadow max-w-2xl mx-auto">
<div class="card-body">
<form action="/usr/member/doJoin" method="post" id="joinForm" data-safe-submit class="grid md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">아이디</span></label>
<input name="loginId" class="input input-bordered" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">비밀번호</span></label>
<input name="loginPw" type="text" class="input input-bordered" required/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">이름</span></label>
<input name="name" class="input input-bordered" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">닉네임</span></label>
<input name="nickname" class="input input-bordered" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">휴대폰</span></label>
<input name="cellphoneNum" type="text" class="input input-bordered"/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">이메일</span></label>
<input name="email" type="text" class="input input-bordered"/>
</div>
<div class="md:col-span-2 flex justify-end gap-2 mt-4">
<a href="/home/main" class="btn">취소</a>
<button class="btn btn-primary" type="submit">회원가입</button>
</div>
</form>
</div>
</div>
<script>
$("#joinForm").on("submit", function(e){
const email = $("input[name=email]").val().trim();
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){
e.preventDefault();
showToast("이메일 형식을 확인해주세요.", "warning");
}
});
</script>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
login.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ include file="/WEB-INF/jsp/usr/common/head.jspf"%>
<c:set var="pageTitle" value="Login" />
<div class="hero min-h-[60vh]">
<div class="hero-content w-full max-w-md">
<div class="card bg-base-100 w-full shadow">
<div class="card-body">
<form action="/usr/member/doLogin" method="post" data-safe-submit>
<div class="form-control">
<label class="label"><span class="label-text">아이디</span></label>
<input name="loginId" class="input input-bordered" required />
</div>
<div class="form-control mt-3">
<label class="label"><span class="label-text">비밀번호</span></label>
<input name="loginPw" type="password" class="input input-bordered" required />
</div>
<div class="form-control mt-6">
<button class="btn btn-primary" type="submit">로그인</button>
</div>
<div class="text-sm text-right mt-2">
<a href="/usr/member/join" class="link">회원가입</a>
</div>
</form>
</div>
</div>
</div>
</div>
<%@ include file="/WEB-INF/jsp/usr/common/foot.jspf"%>
common.css
@charset "UTF-8";
:root {
--app-max-width: 1100px;
}
.container {
max-width: var(--app-max-width);
}
.table td, .table th {
white-space: nowrap;
}
common.js
$(function () {
const $toggle = $("#themeToggle");
const initial = localStorage.getItem("theme") || "light";
setTheme(initial);
$toggle.prop("checked", initial !== "light");
$toggle.on("change", function () {
setTheme(this.checked ? "dark" : "light");
});
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}
});
window.showToast = function (message, type = "info", ms = 2500) {
const $toast = $("#toast");
const color = {
info: "alert-info",
success: "alert-success",
warning: "alert-warning",
error: "alert-error",
}[type] || "alert-info";
const html = `<div class="alert ${color} shadow">${message}</div>`;
$toast.removeClass("hidden").append(html);
setTimeout(() => $toast.addClass("hidden").empty(), ms);
};
window.confirmAsync = function (message = "진행할까요?") {
return new Promise((resolve) => {
const modal = document.getElementById("confirmModal");
$("#confirmMessage").text(message);
modal.showModal();
$("#confirmOkBtn").one("click", function () {
resolve(true);
});
$(modal).one("close", function () {
resolve(false);
});
});
};
$(document).on("submit", "form[data-safe-submit]", function () {
const $btn = $(this).find("button[type=submit], input[type=submit]");
$btn.prop("disabled", true).addClass("loading");
setTimeout(() => $btn.prop("disabled", false).removeClass("loading"), 3000);
});