728x90
※ 외부 라이브러리는 footer에 작성하는 이유? 성능과 로딩 최적화 때문
- 페이지 로딩 우선순위
head에 스크립트를 넣으면 페이지 로딩이 완료되기 전에 자바스크립트가 먼저 로드되고 실행될 수 있음. 이는 스크립트가 다른 DOM 요소의 렌더링을 차단할 수 있기 때문에 페이지 로딩 속도를 늦출 수 있음. footer에 스크립트를 넣으면 HTML 문서의 주요 콘텐츠가 먼저 로드되고 표시된 후, 그 다음에 자바스크립트가 로드되기 때문에 사용자에게 빠른 콘텐츠 표시를 제공할 수 있음 - 비동기적 로딩
footer에 스크립트를 추가하면서 async나 defer 속성을 사용하면, 스크립트가 비동기적으로 로드되거나 DOM이 완전히 로드된 후 실행될 수 있음. 이로 인해 페이지 로딩 시간이 개선 - 최소한의 차단적 렌더링
페이지 번호 이벤트 처리
- 페이지 번호의 링크는 페이지 번호만 가지도록 하고 별도의 form 태그와 이벤트 처리를 통해서 이동
- 검색 조건이 많이 붙을 수 있는데, 관련된 모든 링크를 다 수정해줘야하기 때문에 form태그를 사용
단점 → 자바스크립트가 정상하지 않으면 동작하지 않음
<form action="actionForm" action="/board/list" method="get">
<input type="hidden" name="pageNum" value="${page.criteria.pageNum}">
<input type="hidden" name="amount" value="${page.criteria.amount}">
</form>
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@include file="../include/header.jsp" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Tables</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
DataTables Advanced Tables
<button id="regBtn" type="button" class="btn btn-xs pull-right">Register New Board</button>
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<table width="100%" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>#번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>수정일</th>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="boardVO">
<tr class="odd gradeX">
<td><c:out value="${boardVO.bno}" /></td>
<td><a href="/board/get?bno=<c:out value='${boardVO.bno}' />"><c:out value="${boardVO.title}" /></a></td>
<td><c:out value="${boardVO.writer}" /></td>
<td><fmt:formatDate value="${boardVO.regdate }" pattern="yyyy-MM-dd"/> </td>
<td><fmt:formatDate value="${boardVO.updateDate }" pattern="yyyy-MM-dd" /></td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- /.table-responsive -->
<h4>${page}</h4>
<div class="pull-right">
<ul class="pagination">
<c:if test="${page.prev}">
<li class="page-item"><a class="page-link" href="${page.startPage - 1}" tabindex="-1">Previous</a></li>
</c:if>
<c:forEach begin="${page.startPage}" end="${page.endPage}" var="num">
<li class="page-item ${page.criteria.pageNum == num? 'active' :''}"><a class="page-link" href="${num}">${num}</a></li>
</c:forEach>
<c:if test="${page.next}">
<li class="page-item"><a class="page-link" href="${page.endPage + 1}">Next</a></li>
</c:if>
</ul>
</div>
<form id="actionForm" action="/board/list" method="get">
<input type="hidden" name="pageNum" value="${page.criteria.pageNum}">
<input type="hidden" name="amount" value="${page.criteria.amount}">
</form>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<div id="myModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Save changes</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function(){
var result = '<c:out value="${result}"/>';
checkModal(result);
history.replaceState({},null,null); // 히스토리 클리어
function checkModal(result){
if(result ==='' || history.state){
return;
}
if(result === 'success'){
$('.modal-body').html("정상적으로 처리됨.");
} else if(parseInt(result)>0){
$('.modal-body').html("게시글 " + parseInt(result) + " 번이 등록됨.");
}
$("#myModal").modal("show");
}
$("#regBtn").click(function(){
self.location = "/board/register";
});
var actionForm = $("#actionForm");
$(".page-link").on("click", function(e){
e.preventDefault(); // a태그는 기능은 이동 → 이동이 되지 않음(기본 동작 제어)
console.log('click');
//form 태그의 내용 변경 후 submit
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
actionForm.submit();
});
});
</script>
<%@include file="../include/footer.jsp" %>
3페이지에서 1376376의 글을 조회 → List 버튼을 누르면 list의 1페이지로 넘어감
list.jsp
<c:forEach items="${list}" var="boardVO">
<tr class="odd gradeX">
<td><c:out value="${boardVO.bno}" /></td>
<td><a href="/board/get?bno=<c:out value='${boardVO.bno}' />"><c:out value="${boardVO.title}" /></a></td>
<td><c:out value="${boardVO.writer}" /></td>
<td><fmt:formatDate value="${boardVO.regdate }" pattern="yyyy-MM-dd"/> </td>
<td><fmt:formatDate value="${boardVO.updateDate }" pattern="yyyy-MM-dd" /></td>
</tr>
</c:forEach>
<c:forEach items="${list}" var="boardVO">
<tr class="odd gradeX">
<td><c:out value="${boardVO.bno}" /></td>
<td><a href="<c:out value='${boardVO.bno}' />"><c:out value="${boardVO.title}" /></a></td>
<td><c:out value="${boardVO.writer}" /></td>
<td><fmt:formatDate value="${boardVO.regdate }" pattern="yyyy-MM-dd"/> </td>
<td><fmt:formatDate value="${boardVO.updateDate }" pattern="yyyy-MM-dd" /></td>
</tr>
</c:forEach>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@include file="../include/header.jsp" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Tables</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
DataTables Advanced Tables
<button id="regBtn" type="button" class="btn btn-xs pull-right">Register New Board</button>
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<table width="100%" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>#번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>수정일</th>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="boardVO">
<tr class="odd gradeX">
<td><c:out value="${boardVO.bno}" /></td>
<td><a class="move" href="<c:out value='${boardVO.bno}' />"><c:out value="${boardVO.title}" /></a></td>
<td><c:out value="${boardVO.writer}" /></td>
<td><fmt:formatDate value="${boardVO.regdate }" pattern="yyyy-MM-dd"/> </td>
<td><fmt:formatDate value="${boardVO.updateDate }" pattern="yyyy-MM-dd" /></td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- /.table-responsive -->
<h4>${page}</h4>
<div class="pull-right">
<ul class="pagination">
<c:if test="${page.prev}">
<li class="page-item"><a class="page-link" href="${page.startPage - 1}" tabindex="-1">Previous</a></li>
</c:if>
<c:forEach begin="${page.startPage}" end="${page.endPage}" var="num">
<li class="page-item ${page.criteria.pageNum == num? 'active' :''}"><a class="page-link" href="${num}">${num}</a></li>
</c:forEach>
<c:if test="${page.next}">
<li class="page-item"><a class="page-link" href="${page.endPage + 1}">Next</a></li>
</c:if>
</ul>
</div>
<form id="actionForm" action="/board/list" method="get">
<input type="hidden" name="pageNum" value="${page.criteria.pageNum}">
<input type="hidden" name="amount" value="${page.criteria.amount}">
</form>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<div id="myModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Save changes</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function(){
var result = '<c:out value="${result}"/>';
checkModal(result);
history.replaceState({},null,null); // 히스토리 클리어
function checkModal(result){
if(result ==='' || history.state){
return;
}
if(result === 'success'){
$('.modal-body').html("정상적으로 처리됨.");
} else if(parseInt(result)>0){
$('.modal-body').html("게시글 " + parseInt(result) + " 번이 등록됨.");
}
$("#myModal").modal("show");
}
$("#regBtn").click(function(){
self.location = "/board/register";
});
var actionForm = $("#actionForm");
$(".page-link").on("click", function(e){
e.preventDefault(); // a태그는 기능은 이동 → 이동이 되지 않음(기본 동작 제어)
console.log('click');
//form 태그의 내용 변경 후 submit
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
actionForm.submit();
});
$(".move").on("click", function(e){
e.preventDefault();
console.log($(this).attr("href"));
actionForm.append("<input type='hidden' name='bno' value='"+$(this).attr("href")+"'>");
actionForm.attr("action","/board/get").submit();
})
});
</script>
<%@include file="../include/footer.jsp" %>
주소줄에 bno도 추가됨 추가적으로 수정 삭제 조회에도 이런 작업이 필요함
package org.zerock.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;
import org.zerock.domain.PageDTO;
import org.zerock.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
@Controller
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Log4j
public class BoardController {
private final BoardService boardService;
(...생략...)
@GetMapping({"/get", "/modify"})
public void get(@RequestParam("bno") Long bno,Model model, Criteria criteria) {
model.addAttribute("boardVO", boardService.get(bno));
}
}
get.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@include file="../include/header.jsp" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Board Read</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
Board Read
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<div class="form-group">
<label>BNO</label> <input name="title" class="form-control" readonly="readonly" value="<c:out value='${boardVO.bno}'/>">
</div>
<div class="form-group">
<label>Title</label> <input name="title" class="form-control" readonly="readonly" value="<c:out value='${boardVO.title}'/>">
</div>
<div class="form-group">
<label>Content</label> <textarea rows="5" cols="50" class="form-control" name="content"><c:out value='${boardVO.content}'/></textarea>
</div>
<div class="form-group">
<label>Writer</label> <input name="writer" class="form-control" value="<c:out value='${boardVO.writer}'/>">
</div>
<form id="actionForm" action="/board/list" method="get">
<input type="hidden" name="pageNum" value="${criteria.pageNum}">
<input type="hidden" name="amount" value="${criteria.amount}">
<input type="hidden" name="bno" value="${boardVO.bno}">
</form>
<button type="button" class="btn btn-default listBtn"><a href="/board/list">List</a></button>
<button type="button" class="btn btn-default modiBtn"><a href="/board/modify?bno=<c:out value='${boardVO.bno }'/>">Modify</a></button>
<script>
var actionForm = $("#actionForm");
$('.listBtn').click(function(e){
e.preventDefault();
actionForm.find("input[name='bno']").remove();
actionForm.submit();
});
$('.modiBtn').click(function(e){
e.preventDefault();
actionForm.attr("action","/board/modify");
actionForm.submit();
});
</script>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<%@include file="../include/footer.jsp" %>
수정
BoardController
@PostMapping("/modify")
public String modify(BoardVO boardVO, RedirectAttributes redirectAttributes, Criteria criteria) {
log.info("BoardController modify()......................");
int count = boardService.modify(boardVO);
if(count == 1) {
redirectAttributes.addFlashAttribute("result", "success");
}
redirectAttributes.addAttribute("pageNum",criteria.getPageNum());
redirectAttributes.addAttribute("amount",criteria.getAmount());
return "redirect:/board/list";
}
modify.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@include file="../include/header.jsp" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Board Modify/Delete</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
Board Modify/Delete
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<form>
<input type="hidden" name="pageNum" value="${criteria.pageNum}">
<input type="hidden" name="amount" value="${criteria.amount}">
<div class="form-group">
<label>BNO</label> <input name="bno" class="form-control" readonly="readonly" value="<c:out value='${boardVO.bno}'/>">
</div>
<div class="form-group">
<label>Title</label> <input name="title" class="form-control" value="<c:out value='${boardVO.title}'/>">
</div>
<div class="form-group">
<label>Content</label> <textarea rows="5" cols="50" class="form-control" name="content"><c:out value='${boardVO.content}'/></textarea>
</div>
<div class="form-group">
<label>Writer</label> <input name="writer" class="form-control" value="<c:out value='${boardVO.writer}'/>">
</div>
<button class="btn btn-default" data-oper='modify'>Modify</button>
<button class="btn btn-danger" data-oper='remove'>Remove</button>
<button class="btn btn-info" data-oper='list'>List</button>
</form>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<script>
$(document).ready(function(){
var formObj = $('form');
$('.btn').click(function(e){
e.preventDefault();
var operation = $(this).data("oper");
console.log(operation);
if(operation === 'list'){
self.location = "/board/list";
}else if(operation === 'remove'){
formObj.attr("action", "/board/remove").attr("method","post");
formObj.submit();
}else if(operation === 'modify'){
formObj.attr("action", "/board/modify").attr("method","post");
formObj.submit();
}
})
})
</script>
<%@include file="../include/footer.jsp" %>
삭제 → 삭제는 따로 할 필요는 없으나 이런 방법이 있다는 것을 알아두면 좋음
BoardController
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno, RedirectAttributes redirectAttributes, Criteria criteria) {
log.info("BoardController remove()......................");
int count = boardService.remove(bno);
if(count == 1) {
redirectAttributes.addFlashAttribute("result", "success");
}
redirectAttributes.addAttribute("pageNum",criteria.getPageNum());
redirectAttributes.addAttribute("amount",criteria.getAmount());
return "redirect:/board/list";
}
total
BoardMapper
int getTotalCount(Criteria criteria); // 검색 조건을 Criteria로 넣기 위해서 파라미터로 Criteria를 넣어줌
BoardMapper.XML
<select id="getTotalCount" resultType="int">
select count(bno) from tbl_board
</select>
BoardService
public int getTotal(Criteria criteria);
BoardServiceImpl
@Override
public int getTotal(Criteria criteria) {
return boardMapper.getTotalCount(criteria);
}
BoardController
@GetMapping("/list")
public void list(Model model, Criteria criteria) {
log.info("BoardController list()................");
log.info("criteria : " + criteria);
model.addAttribute("list", boardService.getList(criteria));
model.addAttribute("page", new PageDTO(criteria, boardService.getTotal(criteria)));
}
검색의 처리
- 제목, 내용, 작성자와 같은 단일 항목
- 제목 + 내용, 제목 + 작성자와 같은 복합 항목
- 검색 항목에 따라서 매번 다른 SQL이 처리될 필요가 있는 상황
→ MyBatis의 동적 쿼리 기능을 이용해서 처리
https://mybatis.org/mybatis-3/dynamic-sql.html
Mybatis 동적 태그
- if
test라는 속성과 함께 특정한 조건이 true가 되었을 때 포함된 SQL을 사용하고자 할 때 작성 - choose(when, otherwise)
if와 달리 여러 상황들 중 하나의 상황에서만 동작 - trim(where, set)
단독으로 사용되지 않고, if, choose와 같은 태그들을 내포하고 있어 SQL들을 연결해주고, 앞 뒤에 필요한 구문들(and, or, where 등)을 추가하거나 생략하는 역할 - foreach
List, Array, Map 등을 이용해서 루프를 처리
BoardMapper
package org.zerock.mapper;
import java.util.List;
import java.util.Map;
import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;
public interface BoardMapper {
(...생략...)
List<BoardVO> searchTest(Map<String, Map<String, String>> map);
}
BoardMapper.xml
<select id="getTotalCount" resultType="int">
select count(bno) from tbl_board
</select>
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<foreach collection="map" index="key" item="value">
#{key} #{value}
</foreach>
<![CDATA[
rownum < 10
]]>
</select>
Key만 출력
package org.zerock.mapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;
import org.zerock.domain.PageDTO;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class BoardMapperTests {
(...생략...)
@Test
public void testSearch() {
Map<String, String> map = new HashMap<String, String>();
map.put("T", "TTT");
map.put("C", "CCC");
Map<String, Map<String, String>> outer = new HashMap<String, Map<String,String>>();
outer.put("map", map);
List<BoardVO> list = boardMapper.searchTest(outer);
log.info("list.................. : " + list);
}
}
BoardMapper.xml
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<foreach collection="map" index="key" item="value">
<if test="key == 'T'.toString()">
title like #{value}
</if>
</foreach>
<![CDATA[
rownum < 10
]]>
</select>
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<trim suffix="and">
<foreach collection="map" index="key" item="value">
<if test="key == 'T'.toString()">
title like #{value}
</if>
</foreach>
</trim>
<![CDATA[
rownum < 10
]]>
</select>
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<trim suffix="and">
<foreach collection="map" index="key" item="value">
<if test="key == 'T'.toString()">
title like #{value}
</if>
<if test="key == 'C'.toString()">
content like #{value}
</if>
<if test="key == 'W'.toString()">
writer like #{value}
</if>
</foreach>
</trim>
<![CDATA[
rownum < 10
]]>
</select>
@Test
public void testSearch() {
Map<String, String> map = new HashMap<String, String>();
map.put("T", "TTT");
map.put("C", "CCC");
map.put("W", "WWW");
Map<String, Map<String, String>> outer = new HashMap<String, Map<String,String>>();
outer.put("map", map);
List<BoardVO> list = boardMapper.searchTest(outer);
log.info("list.................. : " + list);
}
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<trim suffix="and">
<foreach collection="map" index="key" item="value" separator="OR">
<if test="key == 'T'.toString()">
title like #{value}
</if>
<if test="key == 'C'.toString()">
content like #{value}
</if>
<if test="key == 'W'.toString()">
writer like #{value}
</if>
</foreach>
</trim>
<![CDATA[
rownum < 10
]]>
</select>
select * from tbl_board where content like 'CCC' OR title like 'TTT' OR writer like 'WWW'
우선순위 때문에 or 조건은 괄호로 묶어줘야함
<select id="searchTest" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board
where
]]>
<trim suffix="and">
<foreach collection="map" index="key" item="value" separator="OR" open="(" close=")">
<if test="key == 'T'.toString()">
title like #{value}
</if>
<if test="key == 'C'.toString()">
content like #{value}
</if>
<if test="key == 'W'.toString()">
writer like #{value}
</if>
</foreach>
</trim>
<![CDATA[
rownum < 10
]]>
</select>
검색 조건이 없을 때
@Test
public void testSearch() {
Map<String, String> map = new HashMap<String, String>();
Map<String, Map<String, String>> outer = new HashMap<String, Map<String,String>>();
outer.put("map", map);
List<BoardVO> list = boardMapper.searchTest(outer);
log.info("list.................. : " + list);
}
select * from tbl_board where rownum < 10
동적 SQL → 확인 할 수 있는 것이 중요
728x90
'organize > 스프링' 카테고리의 다른 글
스프링 웹 프로젝트 15 (0) | 2025.01.14 |
---|---|
스프링 웹 프로젝트 13 (0) | 2025.01.12 |
스프링 웹 프로젝트 12 (0) | 2025.01.11 |
스프링 웹 프로젝트 11 (0) | 2025.01.09 |
스프링 웹 프로젝트 10 (0) | 2025.01.08 |