2026. 3. 30. 21:23ㆍ대우개발원 수업 내용/spring boot, framework
Directory structure:
└── ynkite-springex_web/
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
└── src/
├── main/
│ ├── java/
│ │ ├── lombok.config
│ │ └── com/
│ │ └── example/
│ │ └── springex_web/
│ │ ├── HelloServlet.java
│ │ ├── config/
│ │ │ └── ModelMapperConfig.java
│ │ ├── controller/
│ │ │ ├── SampleController.java
│ │ │ ├── TodoController.java
│ │ │ ├── exception/
│ │ │ │ └── CommonExceptionAdvice.java
│ │ │ └── formatter/
│ │ │ ├── CheckboxFormatter.java
│ │ │ └── LocalDateFormatter.java
│ │ ├── domain/
│ │ │ └── TodoVO.java
│ │ ├── dto/
│ │ │ ├── PageRequestDTO.java
│ │ │ ├── PageResponseDTO.java
│ │ │ └── TodoDTO.java
│ │ ├── mapper/
│ │ │ ├── TimeMapper.java
│ │ │ ├── TimeMapper2.java
│ │ │ └── TodoMapper.java
│ │ └── service/
│ │ ├── TodoService.java
│ │ └── TodoServiceImpl.java
│ ├── resources/
│ │ ├── log4j2.xml
│ │ └── mapper/
│ │ ├── TimeMapper2.xml
│ │ └── TodoMapper.xml
│ └── webapp/
│ ├── index.jsp
│ ├── resources/
│ │ └── test.html
│ └── WEB-INF/
│ ├── root-context.xml
│ ├── servlet-context.xml
│ ├── web.xml
│ └── views/
│ ├── custom404.jsp
│ ├── hello.jsp
│ └── todo/
│ ├── list.jsp
│ ├── modify.jsp
│ ├── read.jsp
│ └── register.jsp
└── test/
└── java/
└── com/
└── example/
└── springex_web/
├── mapper/
│ └── TodoMapperTests.java
└── service/
└── TodoServiceTests.java
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: src/main/java/lombok.config
================================================
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
================================================
FILE: src/main/java/com/example/springex_web/HelloServlet.java
================================================
package com.example.springex_web;
import java.io.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
================================================
FILE: src/main/java/com/example/springex_web/config/ModelMapperConfig.java
================================================
package com.example.springex_web.config;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// bean 설정에 대한 클래스다라는 것을 보여줌
public class ModelMapperConfig {
@Bean
public ModelMapper getMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.LOOSE);
return modelMapper;
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/SampleController.java
================================================
package com.example.springex_web.controller;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.LocalDate;
@Controller
@Log4j2
public class SampleController {
@GetMapping("/hello")
public void hello() { log.info("hello..............");}
@GetMapping("/ex1")
public void ex1(String name, int age) {
log.info("ex1......");
log.info("name: " + name);
log.info("age: " + age);
}
@GetMapping("/ex2")
public void ex2(@RequestParam(name = "name", defaultValue = "AAA") String name,
@RequestParam(name = "age", defaultValue = "20")int age) {
log.info("ex2.........");
log.info("name: " + name);
log.info("age: " + age);
}
@GetMapping("/ex3")
public void ex3(@RequestParam(name = "dueDate", defaultValue = "2026-03-24") LocalDate dueDate) {
// public void ex3(@RequestParam("dueDate") LocalDate dueDate) {
log.info("ex3..........");
log.info("dueDate: " + dueDate);
}
@GetMapping("/ex4")
public void ex4(Model model) {
log.info(".........");
log.info("message" , "Hello World");
}
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
log.info(todoDTO);
}
@GetMapping("/ex5")
public String ex5(RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("name", "ABC");
redirectAttributes.addFlashAttribute("result", "success");
return "redirect:/ex6";
}
@GetMapping("/ex6")
public void ex6() {
}
@GetMapping("/ex7")
public void ex7(@RequestParam("p1") String p1,@RequestParam("p2") int p2) {
log.info("p1............" +p1);
log.info("p2............" +p2);
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/TodoController.java
================================================
package com.example.springex_web.controller;
import com.example.springex_web.dto.PageRequestDTO;
import com.example.springex_web.dto.TodoDTO;
import com.example.springex_web.service.TodoService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
// @RequestMapping("/list")
// public void list(Model model){
// log.info("todo list........");
// model.addAttribute("dtoList", todoService.getAll());
// }
// @RequestMapping(value ="/register", method= RequestMethod.GET) //GETMAPPING
@GetMapping("/list")
public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model){
log.info(pageRequestDTO);
if(bindingResult.hasErrors()){
pageRequestDTO = PageRequestDTO.builder().build();
}
model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
}
@GetMapping("/register")
public void register() {
log.info("todo register........");
}
// @PostMapping("/register")
// public void registerPost(TodoDTO todoDTO) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// }
// @PostMapping("/register")
// public String registerPost(TodoDTO todoDTO , RedirectAttributes redirectAttributes) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// return "redirect:/todo/list";
// }
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO ,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register..........");
if(bindingResult.hasErrors()) {
log.info("has error.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}
// @GetMapping("/read")
// public void read(Long tno, Model model){
@GetMapping({"/read", "/modify"})
public void read(Long tno, Model model) {
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO);
}
@PostMapping ("/remove")
public String remove(Long tno, RedirectAttributes redirectAttributes) {
log.info("-----------------remove-------------------");
log.info("tno: " + tno);
todoService.remove(tno);
return "redirect:/todo/list";
}
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if(bindingResult.hasErrors()) {
log.info("has error.............");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/modify";
}
log.info(todoDTO);
todoService.modify(todoDTO);
return "redirect:/todo/list";
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/exception/CommonExceptionAdvice.java
================================================
package com.example.springex_web.controller.exception;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Arrays;
@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
@ResponseBody
@ExceptionHandler(NumberFormatException.class)
public String exceptNumber(NumberFormatException numberFormatException){
log.error("-----------------------------------");
log.error(numberFormatException.getMessage());
return "NUMBER FORMAT EXCEPTION";
}
@ResponseBody
@ExceptionHandler(Exception.class)
public String exceptCommon(Exception exception){
log.error("-----------------------------------");
log.error(exception.getMessage());
StringBuffer buffer = new StringBuffer("<ul>");
buffer.append("<li>" +exception.getMessage()+"</li>");
Arrays.stream(exception.getStackTrace()).forEach(stackTraceElement -> {
buffer.append("<li>"+stackTraceElement+"</li>");
});
buffer.append("</ul>");
return buffer.toString();
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound() {
return "custom404";
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/formatter/CheckboxFormatter.java
================================================
package com.example.springex_web.controller.formatter;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.util.Locale;
public class CheckboxFormatter implements Formatter<Boolean> {
@Override
public Boolean parse(String text, Locale locale) throws ParseException {
if(text == null) {
return false;
}
return text.equals("on");
}
@Override
public String print(Boolean object, Locale locale){return object.toString(); }
}
================================================
FILE: src/main/java/com/example/springex_web/controller/formatter/LocalDateFormatter.java
================================================
package com.example.springex_web.controller.formatter;
import org.springframework.format.Formatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class LocalDateFormatter implements Formatter<LocalDate> {
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
@Override
public String print(LocalDate object, Locale locale) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
}
}
================================================
FILE: src/main/java/com/example/springex_web/domain/TodoVO.java
================================================
package com.example.springex_web.domain;
import lombok.*;
import java.time.LocalDate;
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TodoVO {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
private String writer;
}
================================================
FILE: src/main/java/com/example/springex_web/dto/PageRequestDTO.java
================================================
package com.example.springex_web.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default //기본값 세팅
@Min(value = 1) //최소값 1
@Positive //무조건 양수값을 가져야함
private int page = 1;
@Builder.Default
@Min(value = 10)
@Max(value = 100)
@Positive
private int size = 10;
public int getSkip() {
return (page - 1) * 10;
}
}
================================================
FILE: src/main/java/com/example/springex_web/dto/PageResponseDTO.java
================================================
package com.example.springex_web.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@ToString
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
//시작 페이지 번호
private int start;
//끝 페이지 번호
private int end;
//이전 페이지의 존재 여부
private boolean prev;
//다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList,
int total){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int)(Math.ceil(this.page / 10.0 )) * 10;
this.start = this.end - 9;
int last = (int)(Math.ceil((total/(double)size)));
this.end = end > last ? last: end;
this.prev = this.start > 1;
this.next = total > this.end * this.size;
}
}
================================================
FILE: src/main/java/com/example/springex_web/dto/TodoDTO.java
================================================
package com.example.springex_web.dto;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
@NotEmpty
private String title;
@Future
private LocalDate dueDate;
private boolean finished;
@NotEmpty
private String writer;
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TimeMapper.java
================================================
package com.example.springex_web.mapper;
import org.apache.ibatis.annotations.Select;
public interface TimeMapper {
@Select("select now()")
String getTime();
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TimeMapper2.java
================================================
package com.example.springex_web.mapper;
public interface TimeMapper2 {
String getNow();
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TodoMapper.java
================================================
package com.example.springex_web.mapper;
import com.example.springex_web.domain.TodoVO;
import com.example.springex_web.dto.PageRequestDTO;
import java.util.List;
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVo);
List<TodoVO> selectAll(); //selectAll(); -> DB에 있는 모든 데이터 조회
TodoVO selectOne(Long tno);
void delete(Long tno);
void update(TodoVO todoVO);
List<TodoVO> selectList(PageRequestDTO pageRequestDTO);
int getCount(PageRequestDTO pageRequestDTO);
}
================================================
FILE: src/main/java/com/example/springex_web/service/TodoService.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.dto.PageRequestDTO;
import com.example.springex_web.dto.PageResponseDTO;
import com.example.springex_web.dto.TodoDTO;
import java.util.List;
public interface TodoService {
void register(TodoDTO todoDTO);
// List<TodoDTO> getAll();
PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);
TodoDTO getOne(Long tno);
void remove(Long tno);
void modify(TodoDTO todoDTO);
}
================================================
FILE: src/main/java/com/example/springex_web/service/TodoServiceImpl.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.domain.TodoVO;
import com.example.springex_web.dto.PageRequestDTO;
import com.example.springex_web.dto.PageResponseDTO;
import com.example.springex_web.dto.TodoDTO;
import com.example.springex_web.mapper.TodoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoMapper todoMapper;
private final ModelMapper modelMapper;
public void register(TodoDTO todoDTO) {
log.info(modelMapper);
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
log.info(todoVO);
todoMapper.insert(todoVO);
}
// @Override
// public List<TodoDTO> getAll() {
// List<TodoDTO> dtoList = todoMapper.selectAll().stream()
// .map(vo -> modelMapper.map(vo, TodoDTO.class))
// .collect(Collectors.toList());
// return dtoList;
// }
@Override
public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
List<TodoDTO> dtoList = voList.stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
int total = todoMapper.getCount(pageRequestDTO);
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.total(total)
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
@Override
public TodoDTO getOne(Long tno) {
TodoVO todoVO = todoMapper.selectOne(tno);
TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
return todoDTO;
}
@Override
public void remove(Long tno) {
todoMapper.delete(tno);
}
@Override
public void modify(TodoDTO todoDTO) {
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
todoMapper.update(todoVO);
}
}
================================================
FILE: src/main/resources/log4j2.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="Console" />
</logger>
<logger name="com.example" level="INFO" additivity="false">
<appender-ref ref="Console" />
</logger>
<logger name="com.example.springex_web.mapper" level="TRACE" additivity="false">
<appender-ref ref="Console" />
</logger>
<root level="INFO" additivity="false">
<AppenderRef ref="Console"/>
</root>
</Loggers>
</Configuration>
================================================
FILE: src/main/resources/mapper/TimeMapper2.xml
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springex.mapper.TimeMapper2">
<select id="getNow" resultType="string">
select now()
</select>
</mapper>
================================================
FILE: src/main/resources/mapper/TodoMapper.xml
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springex_web.mapper.TodoMapper">
<select id="getTime" resultType="string">
SELECT now()
</select>
<insert id="insert">
insert into tbl_todo(title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
</insert>
<select id="selectAll" resultType="com.example.springex_web.domain.TodoVO">
SELECT * FROM tbl_todo order by tno desc
</select>
<select id="selectOne"
resultType="com.example.springex_web.domain.TodoVO">
select * from tbl_todo where tno = #{tno}
</select>
<delete id="delete">
delete from tbl_todo where tno = #{tno}
</delete>
<update id="update">
update tbl_todo set title = #{title}, dueDate = #{dueDate}, finished = #{finished} where tno = #{tno}
</update>
<select id="selectList" resultType="com.example.springex_web.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{skip},${size}
</select>
<select id="getCount" resultType="int">
select count(tno) from tbl_todo
</select>
</mapper>
================================================
FILE: src/main/webapp/index.jsp
================================================
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>
================================================
FILE: src/main/webapp/resources/test.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--<h1>Hello, world!</h1>-->
<div class ="container-fluid">
<div class="row">
<!-- <h1>Header</h1>-->
<div class="col">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- header 끝-->
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
<!-- row content 끝 -->
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!-- <h1>Footer</h1>-->
<div class="row fixed-botom" style="z-index: -100">
<footer class="py-1 my-1">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/root-context.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--<beans xmlns="http://www.springframework.org/schema/beans"-->
<!-- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"-->
<!-- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<!-- <context:component-scan base-package="com.example.springex.sample"></context:component-scan>-->
<!-- <bean class="com.example.springex.sample.SampleDAO"></bean>-->
<!-- <bean class="com.example.springex.sample.SampleService"></bean>-->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="1234"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mapper/*"></property>
</bean>
<mybatis:scan base-package="com.example.springex_web.mapper"></mybatis:scan>
<context:component-scan base-package="com.example.springex_web.config"></context:component-scan>
<context:component-scan base-package="com.example.springex_web.service"></context:component-scan>
</beans>
================================================
FILE: src/main/webapp/WEB-INF/servlet-context.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<context:component-scan base-package="com.example.springex_web.controller"/>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="com.example.springex_web.controller.formatter.LocalDateFormatter"/>
<bean class="com.example.springex_web.controller.formatter.CheckboxFormatter"/>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="conversionService" />
</beans>
================================================
FILE: src/main/webapp/WEB-INF/web.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INF/root-context.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/servlet-context.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>
================================================
FILE: src/main/webapp/WEB-INF/views/custom404.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-25
Time: 오후 7:35
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Oops! 페이지를 찾을 수 없습니다!</h1>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/hello.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-24
Time: 오후 7:44
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello JSP</h1>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/list.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-26
Time: 오후 9:04
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--<h1>Hello, world!</h1>-->
<div class ="container-fluid">
<div class="row">
<!-- <h1>Header</h1>-->
<div class="col">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- header 끝-->
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Tno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">DueDate</th>
<th scope="col">Finished</th>
</tr>
</thead>
<tbody>
<%-- <c:forEach items="${dtoList}" var="dto">--%>
<c:forEach items="${responseDTO.dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td>
<a href="/todo/read?tno=${dto.tno}" class="text-decoration-none"
data-tno="${dto.tno}">
<c:out value="${dto.title}"/>
</a>
</td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
<div class="float-end">
<ul class="pagination flex-wrap">
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.start -1}">Previous</a>
</li>
</c:if>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item ${responseDTO.page == num? "active":""}">
<a class="page-link" data-num="${num}">${num}</a>
</li>
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.end +1}">next</a>
</li>
</c:if>
</ul>
</div>
<script>
document.querySelector(".pagination").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
const target = e.target
if(target.tagName !== 'A') {
return
}
const num = target.getAttribute("data-num")
self.location = `/todo/list?page=\${num}` //백틱(` `)을 이용해서 템플릿 처리
},false)
</script>
</div>
</div>
</div>
</div>
<!-- row content 끝 -->
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!-- <h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/modify.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/modify" method="post">
<div class="input-group mb-3">
<span class="input-group-text">TNO</span>
<input type="text" name="tno" class="form-control"
value=<c:out value="${dto.tno}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control"
value='<c:out value="${dto.title}"></c:out>'>
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control"
value=<c:out value="${dto.dueDate}"></c:out> >
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control"
value=<c:out value="${dto.writer}"></c:out> readonly>
</div>
<div class="form-check">
<label class="form-check-label" >
Finished
</label>
<input class="form-check-input" type="checkbox" name="finished" ${dto.finished?"checked":""} >
</div>
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-danger">Remove</button>
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-secondary">List</button>
</div>
</div>
</form>
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
<script>
const formObj = document.querySelector("form")
document.querySelector(".btn-danger").addEventListener("click",function (e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/remove"
formObj.method = "post"
formObj.submit()
},false)
document.querySelector(".btn-primary").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/modify"
formObj.method = "post"
formObj.submit()
<%--self.location="/todo/modify?tno="+${dto.tno}--%>
}, false)
document.querySelector(".btn-secondary").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
self.location="/todo/list"
}, false)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/read.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<div class="input-group mb-3">
<span class="input-group-text">TNO</span>
<input type="text" name="tno" class="form-control"
value=<c:out value="${dto.tno}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control"
value='<c:out value="${dto.title}"></c:out>' readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control"
value=<c:out value="${dto.dueDate}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control"
value=<c:out value="${dto.writer}"></c:out> readonly>
</div>
<div class="form-check">
<label class="form-check-label" >
Finished
</label>
<input class="form-check-input" type="checkbox" name="finished" ${dto.finished?"checked":""} disabled >
</div>
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-secondary">List</button>
</div>
</div>
<script>
document.querySelector(".btn-primary").addEventListener("click", function (e) {
self.location="/todo/modify?tno="+${dto.tno}
}, false)
document.querySelector(".btn-secondary").addEventListener("click", function (e) {
self.location="/todo/list"
}, false)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/register.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<%-- <h1>Content</h1>--%>
</div>
<div class="row footer">
<h1>Footer</h1>
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/test/java/com/example/springex_web/mapper/TodoMapperTests.java
================================================
package com.example.springex_web.mapper;
import com.example.springex_web.domain.TodoVO;
import com.example.springex_web.dto.PageRequestDTO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
@Test
public void testInsert() {
TodoVO todoVO = TodoVO.builder()
.title("스프링 테스트")
.dueDate(LocalDate.of(2026, 03, 26))
.writer("user00")
.build();
todoMapper.insert(todoVO);
}
@Test
public void testSelectAll() {
List<TodoVO> voList = todoMapper.selectAll();
voList.forEach(vo -> log.info(vo));
}
@Test
public void testSelectOne() {
TodoVO todoVO = todoMapper.selectOne(4L);
log.info(todoVO);
}
@Test
public void testSelectList() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.build();
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
}
================================================
FILE: src/test/java/com/example/springex_web/service/TodoServiceTests.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.dto.PageRequestDTO;
import com.example.springex_web.dto.PageResponseDTO;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
@Test
public void testRegister() {
TodoDTO todoDTO = TodoDTO.builder()
.title("Test........")
.dueDate(LocalDate.now())
.writer("user1")
.build();
todoService.register(todoDTO);
}
@Test
public void testPaging() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
PageResponseDTO<TodoDTO> responseDTO = todoService.getList(pageRequestDTO);
log.info(responseDTO);
responseDTO.getDtoList().stream().forEach(todoDTO -> log.info(todoDTO));
}
}
요약
1. DB 세팅 및 Mapper 수정
테이블 새로 만들고 더미 데이터를 넣음. DB에서 LIMIT 구문을 써서 원하는 만큼만 데이터 자르는
selectList 기능과 전체 글 개수를 세는 getCount 쿼리를 XML에 추가함. 테스트 코드로 정상 작동 검증 완료.
2. 요청 및 응답 DTO 생성
PageRequestDTO: 페이지 번호와 사이즈를 담는 요청용 객체를 만듦. 값 유효성 체크와 DB 조회를 위한 skip 계산 로직을 포함
PageResponseDTO: 제네릭을 사용해 범용성을 높인 응답용 객체를 만듦.
화면에 그릴 시작과 끝 페이지 번호, 이전 및 다음 버튼 활성화 여부 계산 로직을 적용함.
3. Service와 Controller 연동
- Service: 기존 전체 조회 대신, 페이지 조건에 맞춰 데이터를 가져온 뒤
PageResponseDTO에 데이터를 깔끔하게 포장해서 반환하도록 로직 수정.
- Controller: 리스트 화면 요청 시 유효성 검사(Valid, BindingResult)를 적용해 이상한 값이 들어오면
기본값으로 덮어씌우도록 함. 서비스가 반환한 최종 응답 객체를 화면(Model)으로 전달 완료.
4. JSP 화면 및 자바스크립트 연결
JSP 화면: 목록을 뿌리는 부분의 변수명을 responseDTO.dtoList로 수정하고, 하단에 페이지네이션 UI를 동적으로 생성함.
JS 이벤트: 특정 페이지 번호를 누르면 해당 페이지로 딱 이동하게 자바스크립트 클릭 이벤트까지 연결 완료.
페이지 처리를 위해 테이블을 삭제하고 새로 만든다
drop table tbl_todo;
create table tbl_todo(
tno int auto_increment primary key,
title varchar(100) not null,
dueDate date not null,
writer varchar(50) not null,
finished tinyint default 0);
더미데이터를 삽입 한다
INSERT INTO tbl_todo (title, dueDate, writer, finished) VALUES
('REST API 설계 문서 작성', '2026-05-02', '오민재', 1),
('Spring Batch 기초 학습', '2026-04-20', '한민수', 1),
('대용량 데이터 처리 테스트', '2026-03-31', '최지훈', 1),
('API 응답 구조 개선', '2026-07-16', '윤민재', 1),
('페이징 처리 구현', '2026-04-23', '윤서준', 0),
('JUnit 테스트 코드 작성', '2026-04-20', '임하준', 0),
('보안 취약점 점검', '2026-04-28', '최지훈', 1),
('대용량 데이터 처리 테스트', '2026-06-04', '정민수', 0),
('JUnit 테스트 코드 작성', '2026-04-08', '장예린', 0),
('페이징 처리 구현', '2026-05-08', '윤서준', 1),
('CI/CD 파이프라인 구축', '2026-04-22', '임도현', 0),
('Spring Security 로그인 구현', '2026-07-22', '임서연', 1),
('Redis 캐싱 적용', '2026-07-17', '윤도윤', 0),
('파일 업로드/다운로드 구현', '2026-06-16', '김하늘', 0),
('DB 정규화 설계', '2026-06-29', '정서연', 0),
('대용량 데이터 처리 테스트', '2026-04-25', '임도현', 1),
('이미지 업로드 최적화', '2026-06-27', '임민수', 1),
('REST API 설계 문서 작성', '2026-07-11', '김하준', 0),
('WebSocket 채팅 기능 구현', '2026-04-18', '오지민', 0),
('API 응답 구조 개선', '2026-04-23', '임유진', 1),
('트랜잭션 처리 이해', '2026-04-05', '한도현', 0),
('Spring Boot 게시판 만들기', '2026-04-27', '최지호', 0),
('JPA 연관관계 매핑 이해', '2026-06-21', '장서윤', 0),
('카카오 로그인 연동', '2026-06-01', '최지훈', 1),
('Redis 캐싱 적용', '2026-04-18', '임지훈', 0),
('AWS EC2 배포 실습', '2026-05-18', '이유진', 1),
('Ajax 비동기 통신 구현', '2026-07-08', '윤도현', 1),
('대용량 데이터 처리 테스트', '2026-07-09', '한도현', 1),
('서비스 레이어 리팩토링', '2026-04-09', '정도윤', 0),
('카카오 로그인 연동', '2026-05-14', '박민재', 0),
('Git 브랜치 전략 정리', '2026-05-22', '장지훈', 1),
('Ajax 비동기 통신 구현', '2026-05-21', '임민수', 1),
('JUnit 테스트 코드 작성', '2026-04-11', '장민수', 1),
('보안 취약점 점검', '2026-04-20', '윤서준', 0),
('JPA 연관관계 매핑 이해', '2026-08-04', '임하준', 0),
('DB 정규화 설계', '2026-06-02', '정도윤', 0),
('파일 업로드/다운로드 구현', '2026-07-05', '윤서연', 0),
('Docker 컨테이너 실행', '2026-04-23', '김민수', 0),
('JWT 인증 처리 구현', '2026-07-11', '박하늘', 0),
('JUnit 테스트 코드 작성', '2026-04-23', '정하늘', 0),
('페이징 처리 구현', '2026-04-25', '장하늘', 1),
('Swagger API 문서 작성', '2026-04-30', '최도윤', 0),
('Docker 컨테이너 실행', '2026-05-26', '한예린', 0),
('AWS EC2 배포 실습', '2026-07-27', '정서연', 1),
('MariaDB 인덱스 튜닝', '2026-06-21', '이서윤', 0),
('Swagger API 문서 작성', '2026-03-29', '장민수', 0),
('파일 썸네일 생성 구현', '2026-05-04', '이하준', 1),
('API 응답 구조 개선', '2026-07-18', '한민수', 1),
('파일 썸네일 생성 구현', '2026-06-13', '오도윤', 1),
('프론트 React 연동', '2026-07-14', '장유진', 1),
('OAuth2 로그인 구현', '2026-04-14', '최서윤', 0),
('성능 테스트 및 튜닝', '2026-05-05', '임지훈', 0),
('Spring Security 로그인 구현', '2026-04-18', '이하준', 0),
('Ajax 비동기 통신 구현', '2026-08-06', '한예린', 0),
('Nginx 리버스 프록시 설정', '2026-07-29', '이유진', 1),
('코드 리뷰 및 개선', '2026-04-12', '장지훈', 1),
('Spring AOP 적용', '2026-06-03', '오도윤', 1),
('파일 업로드/다운로드 구현', '2026-06-17', '정유진', 1),
('Redis 캐싱 적용', '2026-04-21', '오지훈', 1),
('트랜잭션 처리 이해', '2026-06-14', '이민수', 0),
('Docker 컨테이너 실행', '2026-05-12', '임민재', 1),
('회원가입 유효성 검사 구현', '2026-05-27', '한유진', 1),
('OAuth2 로그인 구현', '2026-04-08', '이하준', 0),
('회원가입 유효성 검사 구현', '2026-05-05', '오지민', 0),
('파일 업로드/다운로드 구현', '2026-08-03', '박민재', 0),
('Nginx 리버스 프록시 설정', '2026-07-04', '최지훈', 0),
('Nginx 리버스 프록시 설정', '2026-08-05', '최지호', 1),
('파일 업로드/다운로드 구현', '2026-07-16', '최지호', 1),
('보안 취약점 점검', '2026-05-31', '오도현', 0),
('댓글 CRUD 기능 완성', '2026-08-06', '최서윤', 1),
('Thymeleaf 레이아웃 구성', '2026-07-17', '장하늘', 1),
('프론트 React 연동', '2026-04-23', '김하준', 0),
('이미지 업로드 최적화', '2026-04-15', '이서윤', 1),
('Spring Security 로그인 구현', '2026-05-18', '한민수', 1),
('AWS EC2 배포 실습', '2026-07-14', '윤도현', 1),
('댓글 CRUD 기능 완성', '2026-06-26', '최서연', 0),
('AWS EC2 배포 실습', '2026-06-19', '최도윤', 0),
('AWS EC2 배포 실습', '2026-06-17', '이민재', 0),
('JPA 연관관계 매핑 이해', '2026-06-26', '김지민', 0),
('Swagger API 문서 작성', '2026-07-11', '한도윤', 1),
('Thymeleaf 레이아웃 구성', '2026-04-17', '이서연', 0),
('Spring Security 로그인 구현', '2026-05-20', '한예린', 0),
('REST API 설계 문서 작성', '2026-07-06', '임민수', 0),
('JWT 인증 처리 구현', '2026-06-12', '박서연', 0),
('MariaDB 인덱스 튜닝', '2026-08-02', '장유진', 0),
('Nginx 리버스 프록시 설정', '2026-07-06', '한지훈', 0),
('QueryDSL 검색 기능 구현', '2026-06-17', '김지민', 1),
('API 응답 구조 개선', '2026-07-09', '박서윤', 1),
('Spring AOP 적용', '2026-05-18', '윤민재', 1),
('에러 로그 분석', '2026-07-29', '이하준', 0),
('OAuth2 로그인 구현', '2026-07-17', '임하준', 0),
('DB 정규화 설계', '2026-04-24', '박민재', 0),
('트랜잭션 처리 이해', '2026-07-23', '임도현', 0),
('JWT 인증 처리 구현', '2026-05-30', '박지훈', 1),
('Git 브랜치 전략 정리', '2026-03-29', '최지훈', 0),
('Swagger API 문서 작성', '2026-04-22', '오지훈', 1),
('AWS EC2 배포 실습', '2026-03-29', '오지민', 0),
('JWT 인증 처리 구현', '2026-04-05', '한유진', 0),
('Ajax 비동기 통신 구현', '2026-04-22', '정유진', 1),
('보안 취약점 점검', '2026-06-26', '박지훈', 1),
('Spring Batch 기초 학습', '2026-07-13', '오민재', 0),
('트랜잭션 처리 이해', '2026-04-18', '박지훈', 0),
('성능 테스트 및 튜닝', '2026-04-11', '이서연', 0),
('에러 로그 분석', '2026-05-30', '정서연', 1),
('JPA 연관관계 매핑 이해', '2026-07-20', '한서윤', 1),
('REST API 설계 문서 작성', '2026-07-06', '한도윤', 1),
('CI/CD 파이프라인 구축', '2026-06-15', '오도현', 0),
('WebSocket 채팅 기능 구현', '2026-05-21', '한지민', 0),
('코드 리뷰 및 개선', '2026-06-27', '박하준', 1),
('로그인 예외 처리 개선', '2026-07-31', '정유진', 1),
('보안 취약점 점검', '2026-04-23', '최서연', 1),
('에러 로그 분석', '2026-07-27', '한유진', 0),
('Git 브랜치 전략 정리', '2026-06-03', '한민수', 1),
('이미지 업로드 최적화', '2026-07-09', '김지민', 0),
('Swagger API 문서 작성', '2026-06-08', '이민수', 0),
('성능 테스트 및 튜닝', '2026-08-04', '한하늘', 1),
('프론트 React 연동', '2026-05-31', '이서윤', 1),
('서비스 레이어 리팩토링', '2026-04-01', '정다은', 0),
('QueryDSL 검색 기능 구현', '2026-05-14', '윤하준', 1),
('성능 테스트 및 튜닝', '2026-06-16', '최도현', 0),
('OAuth2 로그인 구현', '2026-08-05', '오민수', 1),
('코드 리뷰 및 개선', '2026-03-28', '정지훈', 1),
('Docker 컨테이너 실행', '2026-06-15', '이민재', 1),
('성능 테스트 및 튜닝', '2026-03-29', '박지훈', 0),
('Thymeleaf 레이아웃 구성', '2026-04-29', '임유진', 0),
('트랜잭션 처리 이해', '2026-06-21', '한지민', 1),
('댓글 CRUD 기능 완성', '2026-05-24', '최도현', 0),
('AWS EC2 배포 실습', '2026-06-02', '정서연', 0),
('API 응답 구조 개선', '2026-08-02', '오도윤', 0),
('QueryDSL 검색 기능 구현', '2026-07-02', '이도윤', 1),
('카카오 로그인 연동', '2026-04-30', '이유진', 1),
('MariaDB 인덱스 튜닝', '2026-07-19', '윤민재', 1),
('AWS EC2 배포 실습', '2026-07-28', '한민수', 0),
('JWT 인증 처리 구현', '2026-04-28', '임하준', 1),
('코드 리뷰 및 개선', '2026-06-24', '오민재', 1),
('Thymeleaf 레이아웃 구성', '2026-05-04', '김유진', 1),
('대용량 데이터 처리 테스트', '2026-06-24', '임지훈', 1),
('DB 정규화 설계', '2026-05-04', '박지훈', 0),
('페이징 처리 구현', '2026-06-21', '박유진', 0),
('카카오 로그인 연동', '2026-03-30', '오도윤', 1),
('Redis 캐싱 적용', '2026-05-07', '윤하준', 0),
('Ajax 비동기 통신 구현', '2026-07-01', '정하늘', 1),
('Git 브랜치 전략 정리', '2026-05-27', '김지민', 1),
('Git 브랜치 전략 정리', '2026-04-17', '박도현', 1),
('Swagger API 문서 작성', '2026-07-28', '정유진', 1),
('JWT 인증 처리 구현', '2026-04-06', '장하늘', 0),
('로그인 예외 처리 개선', '2026-05-20', '이민수', 1),
('Spring Boot 게시판 만들기', '2026-04-05', '임지훈', 1),
('Thymeleaf 레이아웃 구성', '2026-07-26', '정다은', 1),
('JWT 인증 처리 구현', '2026-04-29', '김지훈', 0),
('API 응답 구조 개선', '2026-06-24', '정하늘', 0),
('OAuth2 로그인 구현', '2026-04-26', '이하늘', 1),
('JPA 연관관계 매핑 이해', '2026-04-20', '윤서연', 0),
('Swagger API 문서 작성', '2026-07-29', '이유진', 1),
('프론트 React 연동', '2026-06-25', '오하늘', 1),
('AWS EC2 배포 실습', '2026-06-14', '한유진', 0),
('WebSocket 채팅 기능 구현', '2026-07-12', '장유진', 1),
('API 응답 구조 개선', '2026-04-08', '이도윤', 0),
('Spring Security 로그인 구현', '2026-06-10', '임지훈', 0),
('에러 로그 분석', '2026-05-07', '장지훈', 0),
('WebSocket 채팅 기능 구현', '2026-05-08', '한지민', 0),
('Spring AOP 적용', '2026-03-31', '김지훈', 1),
('서비스 레이어 리팩토링', '2026-05-09', '이민수', 1),
('코드 리뷰 및 개선', '2026-07-24', '박민재', 0),
('JWT 인증 처리 구현', '2026-04-07', '장지훈', 1),
('Spring Boot 게시판 만들기', '2026-06-07', '김민재', 1),
('CI/CD 파이프라인 구축', '2026-06-19', '윤지훈', 0),
('Nginx 리버스 프록시 설정', '2026-06-27', '임지훈', 1),
('Spring Security 로그인 구현', '2026-06-08', '최유진', 1),
('파일 업로드/다운로드 구현', '2026-07-23', '윤민재', 1),
('보안 취약점 점검', '2026-03-30', '윤지훈', 0),
('JPA 연관관계 매핑 이해', '2026-07-16', '김도윤', 0),
('JPA 연관관계 매핑 이해', '2026-06-12', '박하늘', 0),
('QueryDSL 검색 기능 구현', '2026-04-04', '오유진', 1),
('Axios로 API 호출 테스트', '2026-07-21', '김유진', 0),
('댓글 CRUD 기능 완성', '2026-07-17', '임서연', 0),
('AWS EC2 배포 실습', '2026-05-28', '박민재', 1),
('AWS EC2 배포 실습', '2026-05-11', '이도윤', 1),
('Nginx 리버스 프록시 설정', '2026-07-30', '한민수', 1),
('OAuth2 로그인 구현', '2026-07-21', '이서연', 1),
('REST API 설계 문서 작성', '2026-04-14', '정도윤', 0),
('Spring Boot 게시판 만들기', '2026-04-17', '장지훈', 0),
('Nginx 리버스 프록시 설정', '2026-03-31', '정지훈', 1),
('코드 리뷰 및 개선', '2026-04-03', '오하준', 0),
('Axios로 API 호출 테스트', '2026-07-21', '최유진', 0),
('OAuth2 로그인 구현', '2026-06-20', '최도윤', 0),
('CI/CD 파이프라인 구축', '2026-07-31', '김하늘', 0),
('Docker 컨테이너 실행', '2026-05-20', '박하준', 1),
('서비스 레이어 리팩토링', '2026-07-11', '장하늘', 0),
('회원가입 유효성 검사 구현', '2026-06-02', '윤서연', 1),
('JWT 인증 처리 구현', '2026-04-22', '박유진', 0),
('Spring AOP 적용', '2026-06-15', '최민수', 0),
('Ajax 비동기 통신 구현', '2026-07-15', '이도현', 0),
('DB 정규화 설계', '2026-06-29', '임하준', 1),
('Axios로 API 호출 테스트', '2026-04-02', '박민재', 0),
('MariaDB 인덱스 튜닝', '2026-07-01', '정유진', 0),
('DB 정규화 설계', '2026-07-22', '이서연', 1),
('댓글 CRUD 기능 완성', '2026-05-24', '정유진', 1),
('JWT 인증 처리 구현', '2026-04-11', '장하늘', 0),
('회원가입 유효성 검사 구현', '2026-06-27', '장하늘', 0),
('파일 업로드/다운로드 구현', '2026-06-05', '김지민', 0),
('Spring AOP 적용', '2026-03-29', '한지민', 1),
('파일 썸네일 생성 구현', '2026-03-29', '정유진', 1),
('Ajax 비동기 통신 구현', '2026-05-20', '정지훈', 1),
('Swagger API 문서 작성', '2026-05-06', '김도윤', 0),
('보안 취약점 점검', '2026-08-02', '오민재', 0),
('보안 취약점 점검', '2026-07-27', '장하늘', 1),
('대용량 데이터 처리 테스트', '2026-06-11', '임도현', 0),
('Git 브랜치 전략 정리', '2026-06-27', '한서윤', 1),
('에러 로그 분석', '2026-04-03', '정유진', 1),
('Spring Boot 게시판 만들기', '2026-04-29', '오하늘', 1),
('트랜잭션 처리 이해', '2026-08-02', '최도윤', 1),
('JUnit 테스트 코드 작성', '2026-07-10', '한유진', 1),
('REST API 설계 문서 작성', '2026-07-29', '한하늘', 0),
('DB 정규화 설계', '2026-07-12', '박지훈', 0),
('Spring Security 로그인 구현', '2026-05-10', '김민수', 1),
('이미지 업로드 최적화', '2026-05-22', '정도윤', 0),
('AWS EC2 배포 실습', '2026-06-24', '한하늘', 1),
('AWS EC2 배포 실습', '2026-04-14', '윤도현', 0),
('QueryDSL 검색 기능 구현', '2026-06-23', '박도현', 1),
('QueryDSL 검색 기능 구현', '2026-05-09', '윤도윤', 1),
('JPA 연관관계 매핑 이해', '2026-03-30', '오준호', 0),
('댓글 CRUD 기능 완성', '2026-06-28', '김민재', 0),
('AWS EC2 배포 실습', '2026-05-18', '한지훈', 0),
('Spring Security 로그인 구현', '2026-07-14', '이서연', 0),
('로그인 예외 처리 개선', '2026-06-22', '오준호', 1),
('보안 취약점 점검', '2026-07-13', '장예린', 1),
('댓글 CRUD 기능 완성', '2026-03-31', '김민수', 0),
('트랜잭션 처리 이해', '2026-04-27', '김도현', 0),
('OAuth2 로그인 구현', '2026-07-22', '오준호', 0),
('성능 테스트 및 튜닝', '2026-04-24', '김지훈', 0),
('REST API 설계 문서 작성', '2026-07-26', '김도윤', 0),
('Thymeleaf 레이아웃 구성', '2026-05-12', '오민재', 1),
('OAuth2 로그인 구현', '2026-06-18', '김도윤', 0),
('AWS EC2 배포 실습', '2026-05-29', '최민수', 1),
('카카오 로그인 연동', '2026-05-07', '이하준', 0),
('JUnit 테스트 코드 작성', '2026-04-20', '장하늘', 1),
('CI/CD 파이프라인 구축', '2026-05-24', '임민재', 1),
('CI/CD 파이프라인 구축', '2026-07-07', '김지민', 1),
('CI/CD 파이프라인 구축', '2026-04-20', '장지훈', 0),
('파일 업로드/다운로드 구현', '2026-06-25', '이서윤', 0),
('JWT 인증 처리 구현', '2026-06-11', '장지훈', 1),
('REST API 설계 문서 작성', '2026-04-04', '장지훈', 1),
('Spring Batch 기초 학습', '2026-06-19', '정다은', 1),
('Ajax 비동기 통신 구현', '2026-05-06', '정하늘', 1),
('API 응답 구조 개선', '2026-04-06', '오도현', 1),
('WebSocket 채팅 기능 구현', '2026-06-09', '윤도현', 0),
('Ajax 비동기 통신 구현', '2026-04-29', '정지훈', 0),
('API 응답 구조 개선', '2026-06-20', '정하늘', 1),
('Spring Boot 게시판 만들기', '2026-04-18', '임지훈', 0),
('Spring Boot 게시판 만들기', '2026-04-01', '정유진', 0),
('프론트 React 연동', '2026-07-18', '김도현', 0),
('파일 썸네일 생성 구현', '2026-05-15', '최민수', 0),
('CI/CD 파이프라인 구축', '2026-04-14', '한도현', 1),
('Swagger API 문서 작성', '2026-04-16', '윤하준', 0),
('REST API 설계 문서 작성', '2026-06-05', '오도윤', 0),
('로그인 예외 처리 개선', '2026-04-16', '한민수', 1),
('AWS EC2 배포 실습', '2026-06-11', '한유진', 1),
('WebSocket 채팅 기능 구현', '2026-05-03', '정하늘', 1),
('트랜잭션 처리 이해', '2026-05-30', '정지훈', 1),
('Spring AOP 적용', '2026-06-03', '이도윤', 0),
('Redis 캐싱 적용', '2026-06-04', '최하늘', 1),
('REST API 설계 문서 작성', '2026-05-27', '정하늘', 0),
('프론트 React 연동', '2026-05-03', '윤도현', 0),
('JUnit 테스트 코드 작성', '2026-08-05', '박도현', 1),
('Spring Security 로그인 구현', '2026-04-08', '박서윤', 0),
('Spring Security 로그인 구현', '2026-07-30', '장지훈', 0),
('Nginx 리버스 프록시 설정', '2026-07-19', '한도윤', 1),
('JPA 연관관계 매핑 이해', '2026-06-30', '박유진', 0),
('Spring Batch 기초 학습', '2026-07-11', '오도윤', 0),
('WebSocket 채팅 기능 구현', '2026-05-21', '이하늘', 1),
('대용량 데이터 처리 테스트', '2026-08-06', '장하늘', 1),
('코드 리뷰 및 개선', '2026-06-01', '장유진', 1),
('Swagger API 문서 작성', '2026-06-01', '임도현', 1),
('JPA 연관관계 매핑 이해', '2026-04-02', '김민수', 1),
('Redis 캐싱 적용', '2026-05-25', '정유진', 0),
('카카오 로그인 연동', '2026-04-21', '오도윤', 1),
('이미지 업로드 최적화', '2026-04-27', '박유진', 0),
('Axios로 API 호출 테스트', '2026-06-28', '장지훈', 1),
('Swagger API 문서 작성', '2026-07-22', '정유진', 1),
('Thymeleaf 레이아웃 구성', '2026-07-29', '김하늘', 1),
('회원가입 유효성 검사 구현', '2026-05-24', '한서윤', 1),
('Spring Boot 게시판 만들기', '2026-04-13', '장서윤', 0),
('AWS EC2 배포 실습', '2026-03-28', '장서윤', 1),
('JUnit 테스트 코드 작성', '2026-04-19', '오준호', 1),
('AWS EC2 배포 실습', '2026-06-02', '장민재', 0),
('CI/CD 파이프라인 구축', '2026-06-12', '윤지훈', 1),
('Spring Batch 기초 학습', '2026-05-23', '윤하준', 1),
('Redis 캐싱 적용', '2026-07-03', '오하늘', 0),
('댓글 CRUD 기능 완성', '2026-05-16', '윤도현', 0),
('Swagger API 문서 작성', '2026-07-09', '한지민', 1),
('Thymeleaf 레이아웃 구성', '2026-04-01', '정다은', 0),
('Spring AOP 적용', '2026-05-16', '이민수', 0),
('AWS EC2 배포 실습', '2026-05-26', '이유진', 1),
('Ajax 비동기 통신 구현', '2026-06-14', '이서윤', 1),
('댓글 CRUD 기능 완성', '2026-07-23', '김하준', 0),
('Swagger API 문서 작성', '2026-07-26', '최유진', 0),
('JPA 연관관계 매핑 이해', '2026-07-31', '장지훈', 1),
('코드 리뷰 및 개선', '2026-06-16', '최민수', 1),
('WebSocket 채팅 기능 구현', '2026-03-28', '한유진', 0),
('트랜잭션 처리 이해', '2026-04-06', '장지훈', 1),
('Thymeleaf 레이아웃 구성', '2026-05-30', '한서윤', 1),
('Spring Boot 게시판 만들기', '2026-06-27', '박서윤', 0),
('페이징 처리 구현', '2026-05-28', '장서윤', 1),
('API 응답 구조 개선', '2026-07-20', '박유진', 1),
('QueryDSL 검색 기능 구현', '2026-06-13', '오도현', 1),
('DB 정규화 설계', '2026-07-04', '임지훈', 0),
('Docker 컨테이너 실행', '2026-05-10', '한도윤', 1),
('Axios로 API 호출 테스트', '2026-05-18', '윤서준', 0),
('JPA 연관관계 매핑 이해', '2026-06-19', '최유진', 1),
('서비스 레이어 리팩토링', '2026-05-21', '이유진', 0),
('이미지 업로드 최적화', '2026-06-05', '이민재', 1),
('Redis 캐싱 적용', '2026-07-23', '오도현', 0),
('OAuth2 로그인 구현', '2026-06-15', '정서윤', 1),
('AWS EC2 배포 실습', '2026-06-27', '장서윤', 0),
('AWS EC2 배포 실습', '2026-06-01', '한민수', 0),
('OAuth2 로그인 구현', '2026-06-07', '윤민재', 0),
('JUnit 테스트 코드 작성', '2026-06-19', '한도현', 0),
('에러 로그 분석', '2026-05-15', '한지민', 1),
('Spring Batch 기초 학습', '2026-04-15', '최서윤', 1),
('페이징 처리 구현', '2026-07-22', '장지훈', 1),
('Thymeleaf 레이아웃 구성', '2026-07-02', '오하준', 1),
('로그인 예외 처리 개선', '2026-07-01', '정다은', 0),
('Ajax 비동기 통신 구현', '2026-04-25', '한지민', 1),
('페이징 처리 구현', '2026-04-29', '박하늘', 0),
('Thymeleaf 레이아웃 구성', '2026-04-10', '이하늘', 1),
('트랜잭션 처리 이해', '2026-06-05', '박서연', 1),
('JPA 연관관계 매핑 이해', '2026-05-23', '장하늘', 1),
('보안 취약점 점검', '2026-06-25', '최지훈', 0),
('카카오 로그인 연동', '2026-05-02', '최서연', 0),
('성능 테스트 및 튜닝', '2026-05-17', '최지훈', 0),
('Spring AOP 적용', '2026-07-14', '윤지훈', 1),
('OAuth2 로그인 구현', '2026-06-03', '정서윤', 0),
('카카오 로그인 연동', '2026-04-28', '이유진', 0),
('JWT 인증 처리 구현', '2026-04-09', '임서연', 0),
('프론트 React 연동', '2026-05-08', '장하늘', 0),
('Git 브랜치 전략 정리', '2026-04-07', '박유진', 0),
('보안 취약점 점검', '2026-04-21', '박지훈', 0),
('페이징 처리 구현', '2026-04-19', '최서연', 0),
('파일 썸네일 생성 구현', '2026-07-17', '정유진', 1),
('WebSocket 채팅 기능 구현', '2026-06-29', '최지훈', 0),
('프론트 React 연동', '2026-05-15', '오민수', 1),
('회원가입 유효성 검사 구현', '2026-03-30', '장서윤', 0),
('트랜잭션 처리 이해', '2026-05-29', '장서연', 1),
('DB 정규화 설계', '2026-05-08', '윤도윤', 1),
('Redis 캐싱 적용', '2026-06-03', '최민수', 1),
('트랜잭션 처리 이해', '2026-03-29', '오도현', 0),
('QueryDSL 검색 기능 구현', '2026-07-11', '김지민', 0),
('JUnit 테스트 코드 작성', '2026-07-30', '윤도현', 1),
('Swagger API 문서 작성', '2026-08-03', '김지훈', 1),
('Swagger API 문서 작성', '2026-07-09', '장예린', 1),
('프론트 React 연동', '2026-04-22', '최도현', 1),
('DB 정규화 설계', '2026-07-15', '장유진', 1),
('Nginx 리버스 프록시 설정', '2026-06-03', '장서윤', 1),
('회원가입 유효성 검사 구현', '2026-05-31', '한도윤', 1),
('파일 썸네일 생성 구현', '2026-05-15', '최유진', 1),
('회원가입 유효성 검사 구현', '2026-04-20', '장예린', 0),
('Axios로 API 호출 테스트', '2026-07-25', '최유진', 0),
('JUnit 테스트 코드 작성', '2026-05-03', '정지훈', 1),
('Redis 캐싱 적용', '2026-05-28', '장서윤', 1),
('Spring Batch 기초 학습', '2026-04-20', '최하늘', 1),
('REST API 설계 문서 작성', '2026-06-02', '임지훈', 1),
('회원가입 유효성 검사 구현', '2026-07-01', '최도윤', 1),
('파일 업로드/다운로드 구현', '2026-07-01', '한지훈', 1),
('AWS EC2 배포 실습', '2026-05-06', '이하늘', 1),
('CI/CD 파이프라인 구축', '2026-06-08', '임민재', 0),
('Ajax 비동기 통신 구현', '2026-06-10', '임하준', 0),
('댓글 CRUD 기능 완성', '2026-07-20', '이서윤', 1),
('JWT 인증 처리 구현', '2026-07-06', '윤도현', 1),
('JWT 인증 처리 구현', '2026-05-22', '정지훈', 1),
('WebSocket 채팅 기능 구현', '2026-07-25', '박서윤', 1),
('서비스 레이어 리팩토링', '2026-06-06', '장민수', 1),
('QueryDSL 검색 기능 구현', '2026-06-05', '김하늘', 1),
('QueryDSL 검색 기능 구현', '2026-05-03', '김하준', 1),
('보안 취약점 점검', '2026-06-26', '김하준', 1),
('API 응답 구조 개선', '2026-04-25', '한서윤', 1),
('로그인 예외 처리 개선', '2026-05-16', '한서윤', 0),
('프론트 React 연동', '2026-05-21', '임민재', 0),
('JPA 연관관계 매핑 이해', '2026-06-24', '한도윤', 1),
('Ajax 비동기 통신 구현', '2026-07-25', '정하늘', 0),
('파일 썸네일 생성 구현', '2026-04-26', '한지민', 1),
('이미지 업로드 최적화', '2026-05-22', '임민재', 0),
('Spring Security 로그인 구현', '2026-07-06', '이도윤', 0),
('로그인 예외 처리 개선', '2026-04-11', '장지훈', 0),
('페이징 처리 구현', '2026-04-19', '이도윤', 0),
('Redis 캐싱 적용', '2026-07-04', '한유진', 1),
('Swagger API 문서 작성', '2026-05-18', '윤서연', 0),
('JPA 연관관계 매핑 이해', '2026-06-26', '최유진', 1),
('JPA 연관관계 매핑 이해', '2026-07-31', '한지훈', 1),
('성능 테스트 및 튜닝', '2026-05-08', '한유진', 1),
('DB 정규화 설계', '2026-05-08', '김하준', 0),
('Spring AOP 적용', '2026-06-06', '박하늘', 1),
('트랜잭션 처리 이해', '2026-06-28', '김민수', 1),
('카카오 로그인 연동', '2026-04-04', '장하늘', 0),
('WebSocket 채팅 기능 구현', '2026-04-09', '장서윤', 1),
('JUnit 테스트 코드 작성', '2026-03-30', '한민수', 1),
('Axios로 API 호출 테스트', '2026-08-05', '박지훈', 1),
('CI/CD 파이프라인 구축', '2026-04-14', '박지훈', 1),
('로그인 예외 처리 개선', '2026-06-19', '임민수', 0),
('회원가입 유효성 검사 구현', '2026-07-20', '임지훈', 1),
('에러 로그 분석', '2026-05-25', '김민재', 1),
('REST API 설계 문서 작성', '2026-04-13', '김민수', 0),
('대용량 데이터 처리 테스트', '2026-07-07', '윤지훈', 1),
('대용량 데이터 처리 테스트', '2026-04-07', '정민수', 0),
('Git 브랜치 전략 정리', '2026-04-16', '장하늘', 1),
('JPA 연관관계 매핑 이해', '2026-05-18', '김지민', 0),
('Redis 캐싱 적용', '2026-07-07', '임민수', 1),
('WebSocket 채팅 기능 구현', '2026-05-05', '장서윤', 0),
('Redis 캐싱 적용', '2026-05-02', '박유진', 0),
('QueryDSL 검색 기능 구현', '2026-05-17', '박서윤', 1),
('JUnit 테스트 코드 작성', '2026-04-01', '김지민', 0),
('JWT 인증 처리 구현', '2026-05-25', '장민재', 0),
('파일 업로드/다운로드 구현', '2026-05-21', '이서윤', 0),
('Ajax 비동기 통신 구현', '2026-04-07', '김서연', 0),
('WebSocket 채팅 기능 구현', '2026-05-05', '오도윤', 0),
('QueryDSL 검색 기능 구현', '2026-08-02', '정서윤', 0),
('에러 로그 분석', '2026-07-12', '임서윤', 0),
('Spring Security 로그인 구현', '2026-06-21', '이유진', 1),
('Spring AOP 적용', '2026-05-23', '김도윤', 1),
('서비스 레이어 리팩토링', '2026-07-06', '한도현', 1),
('코드 리뷰 및 개선', '2026-07-23', '박서윤', 0),
('Spring Security 로그인 구현', '2026-07-09', '이도현', 0),
('Swagger API 문서 작성', '2026-07-04', '임하준', 0),
('WebSocket 채팅 기능 구현', '2026-07-25', '윤도현', 0),
('Docker 컨테이너 실행', '2026-05-08', '윤지훈', 0),
('API 응답 구조 개선', '2026-05-12', '박도현', 0),
('Docker 컨테이너 실행', '2026-05-26', '최민수', 1),
('OAuth2 로그인 구현', '2026-07-31', '장지훈', 0),
('Spring AOP 적용', '2026-04-25', '오도현', 1),
('보안 취약점 점검', '2026-04-05', '한지민', 1),
('REST API 설계 문서 작성', '2026-07-24', '정지훈', 0),
('CI/CD 파이프라인 구축', '2026-06-03', '정지훈', 0),
('Redis 캐싱 적용', '2026-04-11', '이유진', 1),
('에러 로그 분석', '2026-07-11', '정도윤', 0),
('Axios로 API 호출 테스트', '2026-05-20', '오지훈', 1),
('Spring Boot 게시판 만들기', '2026-07-17', '이서연', 0),
('성능 테스트 및 튜닝', '2026-06-15', '정지훈', 0),
('WebSocket 채팅 기능 구현', '2026-05-25', '정민수', 0),
('페이징 처리 구현', '2026-07-11', '최서윤', 0),
('Redis 캐싱 적용', '2026-04-18', '김도현', 1),
('성능 테스트 및 튜닝', '2026-05-25', '이유진', 1),
('대용량 데이터 처리 테스트', '2026-06-28', '장하늘', 0),
('DB 정규화 설계', '2026-06-20', '이도윤', 1),
('OAuth2 로그인 구현', '2026-06-30', '오하준', 0),
('프론트 React 연동', '2026-06-16', '이서윤', 0),
('AWS EC2 배포 실습', '2026-04-26', '김유진', 0),
('CI/CD 파이프라인 구축', '2026-04-28', '한지민', 1),
('DB 정규화 설계', '2026-06-11', '이민재', 0),
('대용량 데이터 처리 테스트', '2026-06-25', '임서윤', 0),
('Git 브랜치 전략 정리', '2026-06-15', '오하늘', 1),
('AWS EC2 배포 실습', '2026-04-06', '최도현', 0),
('서비스 레이어 리팩토링', '2026-05-15', '이민수', 1),
('카카오 로그인 연동', '2026-07-30', '임도현', 0),
('AWS EC2 배포 실습', '2026-04-21', '정서연', 0),
('파일 썸네일 생성 구현', '2026-07-24', '한하늘', 0),
('Redis 캐싱 적용', '2026-06-16', '정서연', 0),
('보안 취약점 점검', '2026-04-16', '이서연', 1),
('카카오 로그인 연동', '2026-05-19', '장유진', 0),
('코드 리뷰 및 개선', '2026-05-25', '윤하준', 1),
('QueryDSL 검색 기능 구현', '2026-06-14', '박서연', 1),
('서비스 레이어 리팩토링', '2026-07-03', '장서연', 1),
('REST API 설계 문서 작성', '2026-08-06', '최도윤', 1),
('REST API 설계 문서 작성', '2026-07-18', '이유진', 0),
('Ajax 비동기 통신 구현', '2026-07-18', '김도현', 1),
('REST API 설계 문서 작성', '2026-07-17', '한도현', 0),
('페이징 처리 구현', '2026-04-26', '정유진', 0),
('회원가입 유효성 검사 구현', '2026-06-22', '임하준', 0),
('QueryDSL 검색 기능 구현', '2026-06-08', '임민수', 1),
('API 응답 구조 개선', '2026-03-27', '장하늘', 0),
('트랜잭션 처리 이해', '2026-06-18', '최유진', 0),
('이미지 업로드 최적화', '2026-07-09', '윤지훈', 0),
('댓글 CRUD 기능 완성', '2026-07-25', '이민재', 1),
('서비스 레이어 리팩토링', '2026-06-17', '장예린', 0),
('AWS EC2 배포 실습', '2026-05-08', '김도현', 1),
('Redis 캐싱 적용', '2026-06-16', '이민수', 1),
('로그인 예외 처리 개선', '2026-04-16', '임민수', 0),
('보안 취약점 점검', '2026-04-29', '박하준', 1),
('Spring Batch 기초 학습', '2026-06-11', '정서연', 0),
('회원가입 유효성 검사 구현', '2026-06-07', '임도현', 1),
('API 응답 구조 개선', '2026-04-17', '장하늘', 0),
('Thymeleaf 레이아웃 구성', '2026-03-30', '장유진', 0),
('Ajax 비동기 통신 구현', '2026-04-26', '박하늘', 0),
('성능 테스트 및 튜닝', '2026-04-16', '한하늘', 0),
('이미지 업로드 최적화', '2026-06-22', '장민재', 0),
('파일 업로드/다운로드 구현', '2026-07-28', '오하준', 1),
('파일 썸네일 생성 구현', '2026-04-09', '김도윤', 1),
('Nginx 리버스 프록시 설정', '2026-06-12', '장민재', 0),
('Swagger API 문서 작성', '2026-06-26', '장지훈', 1),
('AWS EC2 배포 실습', '2026-05-03', '최유진', 0),
('페이징 처리 구현', '2026-08-06', '윤민재', 0),
('CI/CD 파이프라인 구축', '2026-05-21', '이서윤', 1),
('카카오 로그인 연동', '2026-05-18', '박유진', 0),
('MariaDB 인덱스 튜닝', '2026-06-03', '오준호', 0),
('대용량 데이터 처리 테스트', '2026-07-28', '한지민', 0),
('에러 로그 분석', '2026-04-04', '한도윤', 1),
('에러 로그 분석', '2026-05-31', '장서윤', 1),
('Spring Batch 기초 학습', '2026-07-25', '임하준', 1),
('Redis 캐싱 적용', '2026-07-08', '한유진', 1),
('댓글 CRUD 기능 완성', '2026-07-25', '이유진', 0),
('CI/CD 파이프라인 구축', '2026-07-23', '윤하준', 0);


MariaDB/MySQL에서는 아래 구문처럼 limit을 사용해서 페이징 쿼리를 작성
SELECT * FROM tbl_todo ORDER BY tno DESC limit 10;

특정 개수만큼의 데이터를 가져올 때 사용
500~491 10개가 출력
아래 구문은 특정 위치부터 데이터를 가져올 때 사용
SELECT * FROM tbl_todo ORDER BY tno DESC limit 10, 10;

10개를 건너뛰고 10개 (490~481이 출력)
페이지 처리하기 위해 PageRequestDTO.java 생성

package com.example.springex_web.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default //기본값 세팅
@Min(value = 1) //최소값 1
@Positive //무조건 양수값을 가져야함
private int page = 1;
@Builder.Default
@Min(value = 10)
@Max(value = 100)
@Positive
private int size = 10;
public int getSkip() {
return (page - 1) * 10;
}
}
전체 데이터를 다 부르지 않고, 원하는 위치(skip)부터 원하는 개수(size)만큼만 잘라오기 위해서
TodoMapper.java에 selectList를 추가

SQL에 데이터를 저장하기 위해 TodoMapper.xml에 다음과 같은 내용을 추가한다.

mapper 코드가 제대로 작성되었는지 확인하기 위해 test코드 작성
- 쿼리 검증: 작성한 SQL 문법이 DB에서 정상 작동하는지 확인.
- 페이징 로직 체크: page(1) 입력 시 skip(0)이 적용되어 첫 10개가 잘 나오는지 확인.
- 데이터 확인: 실제 DB의 데이터가 VO 객체에 잘 담겨오는지 로그로 최종 점검.
@Test
public void testSelectList() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.build();
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
Test passed가 나오며 아래와 같이 10개의 데이터가 나온다.

전체 페이지 수를 알기 위해 아까 했던 방식과 동일하게 getCount메서드를 넣어준다
TodoMapper.java
int getCount(PageRequestDTO pageRequestDTO);

TodoMapper.xml
<select id="getCount" resultType="int">
select count(tno) from tbl_todo
</select>

공통적인 처리를 위해
PageResponseDTO를 만드는데 제네릭을 추가한다
페이징 계산기(틀)는 하나만 만들어두고, 내용물(Todo, 게시글 등)만 내 맘대로 갈아 끼워 재사용하기 위하여 제네릭을 사용한다
package com.example.springex_web.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@ToString
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
//시작 페이지 번호
private int start;
//끝 페이지 번호
private int end;
//이전 페이지의 존재 여부
private boolean prev;
//다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList,
int total){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int)(Math.ceil(this.page / 10.0 )) * 10;
this.start = this.end - 9;
int last = (int)(Math.ceil((total/(double)size)));
this.end = end > last ? last: end;
this.prev = this.start > 1;
this.next = total > this.end * this.size;
}
}
TodoService.java에 메서드 추가
기존 DTO를 반환하는 (list 호출 하는 메서드)를
// List<TodoDTO> getAll();
주석처리 하고 새로 작성
PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);
interface에서는 없어 졌는데 TodoServieImpl에서는 남아 있어서 오류가 나기 때문에
주석 처리
@Override
// public List<TodoDTO> getAll() {
// List<TodoDTO> dtoList = todoMapper.selectAll().stream()
// .map(vo -> modelMapper.map(vo, TodoDTO.class))
// .collect(Collectors.toList());
// return dtoList;
// }
할 일(Todo) 목록을 페이징 처리해서 가져오는 서비스 메서드
- 요청 객체(PageRequestDTO)
- 데이터 조회(todoMapper.selectList)
- DTO 변환(modelMapper.map)
- 전체 개수(todoMapper.getCount)
- PageResponseDTO 빌드
- 반환(return)
- 최종적으로 PageResponseDTO를 반환
즉, 할 일 목록을 페이지 단위로 조회해서 클라이언트에 전달하는 서비스 로직
@Override
public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
List<TodoDTO> dtoList = voList.stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
int total = todoMapper.getCount(pageRequestDTO);
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.total(total)
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
TodoServiceTests에서 테스트를 아래 구문을 입력해서 테스트
@Test
public void testPaging() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
PageResponseDTO<TodoDTO> responseDTO = todoService.getList(pageRequestDTO);
log.info(responseDTO);
responseDTO.getDtoList().stream().forEach(todoDTO -> log.info(todoDTO));
}
테스트 코드에서 오류가 난다
TodoController.java코드에서 아래 구문을 주석
// model.addAttribute("dtoList", todoService.getAll());

주석처리를 하면
아래 사진처럼 정상적으로 PageResponseDTO(page=1, size=10, total=500, start=1, .......이 뜬다

TodoController.java에
list 부분을 전체 주석처리 후
// @RequestMapping("/list")
// public void list(Model model){
// log.info("todo list........");
// model.addAttribute("dtoList", todoService.getAll());
// }
// @RequestMapping(value ="/register", method= RequestMethod.GET) //GETMAPPING
아래 코드를 추가
아래 메서드는 스프링 MVC 컨트롤러에서 GET 요청 /list를 받아 페이지 요청 DTO를 검증하고,
검증된 요청으로 할 일 목록을 조회한 뒤 responseDTO를 모델에 담아 뷰로 전달하는 역할을 함
@GetMapping("/list")
public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model){
log.info(pageRequestDTO);
if(bindingResult.hasErrors()){
pageRequestDTO = PageRequestDTO.builder().build();
}
model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
}
dtoList
에서
responseDTO
로 바뀌었으니
뷰에서 받는 이름도 바꿔줘야함
=> jsp 수정
<%-- <c:forEach items="${dtoList}" var="dto">--%>
을 주석처리
<c:forEach items="${responseDTO.dtoLidt}" var="dto">
코드를 추가
코드를 실행하면
http://localhost:8080/todo/list?page=4&size=15
=> 페이지 4번에 15개씩 출력

SELECT * FROM tbl_todo ORDER BY tno DESC limit 10, 10;
라는 SQL문을 실행 했었기 때문에
http://localhost:8080/todo/list?page=3&size=7 같은 주소로 입력을 해도
list 개수가 7개가 아닌 10개로 출력이 된다
list.jsp안에 이 JSP 코드 블록은 페이지네이션(페이지 번호 목록)을 화면에 출력하는 역할을 하는 코드
이 코드를 테이블 코드 아래에 추가한다
<div class="float-end">
<ul class="pagination flex-wrap">
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item">
<a class="page-link" data-num="${num}">${num}</a>
</li>
</c:forEach>
</ul>
</div>
list.jsp안에 코드 수정
responseDTO.prev가 true일 때만 이전 페이지 버튼을 출력하고, 클릭 시 start - 1 페이지로 이동
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.start -1}">Previous</a>
</li>
</c:if>
responseDTO.next가 true일 때만 다음 페이지 버튼을 출력하며, 클릭 시 다음 페이지 그룹으로 이동할 수 있게 함
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.end +1}">next</a>
</li>
</c:if>
list.jsp에
현재 페이지 번호와 같은<li>에 active 클래스를 붙여서 강조 표시하는 역할
즉, response.page == num이면 "active"가 적용되어 부트스트랩 페이지네이션에서 현재 페이지가 시각적으로 구분
<li class="page-item ${response.page == num? "active":""}">
를 추가

현재 페이지가 강조가 되는 모습
페이지네이션 영역을 클릭했을 때 이벤트를 가로채서 처리하는 역할
즉, 클릭된 버튼의 data-num 값을 읽어와 /todo/list?page=${num} 주소로 이동시켜 해당 페이지 목록을 보여줌
<script>
document.querySelector(".pagination").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
const target = e.target
if(target.tagName !== 'A') {
return
}
const num = target.getAttribute("data-num")
self.location = `/todo/list?page=\${num}` //백틱(` `)을 이용해서 템플릿 처리
},false)
</script>
이 코드를 넣으면
페이지를 클릭하거나 Previous와 next버튼을 누르면 정상적으로 작동



'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바 프레임 워크 11일차 springex_web 검색 및 데이터 보존 페이징처리 (1) | 2026.04.01 |
|---|---|
| 자바 프레임 워크 10일차 springex_web 페이징처리, 검색처리 (1) | 2026.04.01 |
| 자바 프레임워크 8일차 springex_web(Read, Delete, Update) (0) | 2026.03.27 |
| 자바 프레임워크 7일차 springex_web(Spring MVC & 데이터베이스 연동 핵심 기능 구현) (0) | 2026.03.26 |
| 자바프레임워크 6일차 springex 마무리 springex_web(자바 웹 개발 기본 환경 세팅 및 Servlet/JSP 구현) 만들기 (0) | 2026.03.25 |



