자바프레임워크 9일차 springex_web 페이징 처리

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 &nbsp;
              </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 &nbsp;
              </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) 목록을 페이징 처리해서 가져오는 서비스 메서드

  1. 요청 객체(PageRequestDTO)
  2. 데이터 조회(todoMapper.selectList)
  3. DTO 변환(modelMapper.map)
  4. 전체 개수(todoMapper.getCount)
  5. PageResponseDTO 빌드
  6. 반환(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버튼을 누르면 정상적으로 작동