자바 프레임워크 8일차 springex_web(Read, Delete, Update)

2026. 3. 27. 20:53대우개발원 수업 내용/spring boot, framework

반응형

[TIL] Todo CRUD 기능 완성 (Read, Delete, Update)

1. 조회(Read) 기능 완성 목록에서 특정 글 클릭 시 상세 내용을 보여주는 기능 구현

  • DB 연동: Mapper와 XML에 selectOne 쿼리 작성 후 Service(getOne) 연결 및 테스트 코드 검증
  • 컨트롤러 최적화: TodoController에 @GetMapping("/read") 추가. 파라미터 바인딩 문제 해결을 위해
    build.gradle에 -parameters 컴파일 옵션 세팅
  • 화면 연결: list.jsp 제목에 링크 추가, read.jsp 생성하여 상세 화면 및 버튼(Modify, List) 이동 로직 구현

2. 삭제(Delete) 기능 완성 수정 화면(modify.jsp)에서 데이터를 안전하게 삭제하는 기능 구현

  • UI/UX 구현: read.jsp를 복사해 modify.jsp 생성. 텍스트 박스의 readonly 속성 해제 후 [Remove] 버튼 추가
  • JS 폼 제어: 자바스크립트를 활용해 버튼 클릭 시 폼의 action을 /todo/remove, method를 post로 동적 변경하여 전송
  • 백엔드 연결: Mapper, XML(<delete>), Service(remove), Controller(@PostMapping("/remove"))를
    관통하는 삭제 로직 완성

3. 수정(Update) 기능 - 1단계: 핵심 DB 처리 (영속성) 실제 데이터베이스 값을 변경하는 백엔드 핵심 로직 구축

  • Mapper 인터페이스와 XML에 단일 행(Row)을 갱신하는 UPDATE 쿼리 작성
  • TodoServiceImpl에서 화면에서 넘어온 DTO를 VO로 변환(ModelMapper)하여 매퍼에 전달하는 실행 로직 완성

4. 수정(Update) 기능 - 2단계: 데이터 검증 및 포맷터 (안정성) 브라우저 입력 데이터를 서버가
     안전하게 처리하기 위한 방어 로직 추가

  • 체크박스 포맷터(CheckboxFormatter): 체크박스의 "on" 문자열을 Boolean으로 변환하는 포맷터 직접 구현 및
    servlet-context.xml 등록 (finished 처리 완벽 호환)
  • 데이터 검증(@Valid): Controller에 @Valid와 BindingResult 적용. 유효성 검사 실패 시 DB 접근을 막고
    기존 수정 화면으로 리다이렉트
  • 안전한 화면 제어: modify.jsp에 에러 메시지를 JS 객체로 변환해 받는 코드 추가. e.preventDefault(),
    e.stopPropagation() 적용으로 이벤트 중복 방지 및 폼 전송 안정성 극대화
더보기
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/
        │   │               │   └── 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


Files Content:

================================================
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.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("/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/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 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);
}



================================================
FILE: src/main/java/com/example/springex_web/service/TodoService.java
================================================
package com.example.springex_web.service;

import com.example.springex_web.dto.TodoDTO;

import java.util.List;

public interface TodoService {
    void register(TodoDTO todoDTO);
    List<TodoDTO> getAll();
    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.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;

@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 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>


</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">
                            <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>
            </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 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);
    }
}


================================================
FILE: src/test/java/com/example/springex_web/service/TodoServiceTests.java
================================================
package com.example.springex_web.service;


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);
    }
}

 

 

 

Todo 조회 기능 개발
• ‘/todo/read?tno=xx'와 같이 TodoController호출시 동작

 

TodoMapper.java에

TodoVO selectOne(Long tno);

이 줄을 추가해서

 

package com.example.springex_web.mapper;

import com.example.springex_web.domain.TodoVO;

import java.util.List;

public interface TodoMapper {
    String getTime();
    void insert(TodoVO todoVo);
    List<TodoVO> selectAll(); //selectAll();  -> DB에 있는 모든 데이터 조회
    TodoVO selectOne(Long tno);
}

 

이렇게 만듬


TodoMapper.xml코드에

<select id="selectOne"
        resultType="com.example.springex_web.domain.TodoVO">
    select * from tbl_todo where tno = #{tno}
</select>

이부분을 추가해서

> resultType="cohttp://m.example.springex_web.domain.TodoVO"> 이부분의 경로 잘 확인!

<?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>

</mapper>

이렇게 만듬


 

mapper에 잘 추가 되어 있는지 확인하기위해 tests 코드에 입력

TodoMapperTests 코드에 추가

@Test
public void testSelectOne() {
    TodoVO todoVO = todoMapper.selectOne(4L);
    log.info(todoVO);
}

 

최종 코드

 
더보기
package com.example.springex_web.mapper;

import com.example.springex_web.domain.TodoVO;
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);
    }
}



그럼

정상적으로 실행됨


TodoService.java에 

TodoDTO getOne(Long tno);

 

추가해서

package com.example.springex_web.service;

import com.example.springex_web.dto.TodoDTO;

import java.util.List;

public interface TodoService {
    void register(TodoDTO todoDTO);
    List<TodoDTO> getAll();
    TodoDTO getOne(Long tno);
}

 

이렇게 만듬


TodoServiceImpl.java에

@Override
public TodoDTO getOne(Long tno) {
    TodoVO todoVO = todoMapper.selectOne(tno);
    TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
    return todoDTO;
}

 

이 부분을 추가해서

 
더보기
package com.example.springex_web.service;


import com.example.springex_web.domain.TodoVO;
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;

@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 TodoDTO getOne(Long tno) {
        TodoVO todoVO = todoMapper.selectOne(tno);
        TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
        return todoDTO;
    }
}

이렇게 만듬


TodoController.java

 

    @GetMapping("/read")
//    public void read(Long tno, Model model){
        public void read(@RequestParam("tno") Long tno, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

이부분 추가해서

원래  public void read(Long tno, Model model) 이 코드이지만

그럼 자바 코드를 컴파일할 때 파라미터 이름 정보가 날아가 버려서 매칭을 못 하고 있는 상황이 발생하기 때문에

@RequestParam을 써서

public void read(@RequestParam("tno") Long tno, Model model) 이렇게 코드를 쓴다

 

더보기
package com.example.springex_web.controller;


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("/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){
        public void read(@RequestParam("tno") Long tno, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

}

 

 

이렇게 만듬


그리고 jsp파일을 추가한다

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>



          </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>


controller에서 계속

@RequestParam

을 매번 입력하기 번거로우니 지우고
build.gradle에 

tasks.withType(JavaCompile).configureEach { options.compilerArgs += ["-parameters"]

구문을 넣어서 입력하지 않아도 되게 변경

더보기
plugins {
    id 'java'
    id 'war'
}

group 'com.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

ext {
    junitVersion = '5.10.2'
    springVersion = '6.1.6'
}

sourceCompatibility = '21'
targetCompatibility = '21'

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

dependencies {
    compileOnly('jakarta.servlet:jakarta.servlet-api:6.0.0')

//    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
//    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
    testImplementation platform("org.junit:junit-bom:${junitVersion}")
    testImplementation "org.junit.jupiter:junit-jupiter"

//    // https://mvnrepository.com/artifact/org.springframework/spring-core
//    implementation group: 'org.springframework', name: 'spring-core', version: '6.2.15'
//    // https://mvnrepository.com/artifact/org.springframework/spring-context
//    implementation group: 'org.springframework', name: 'spring-context', version: '6.2.15'
//    // https://mvnrepository.com/artifact/org.springframework/spring-test
//    testImplementation group: 'org.springframework', name: 'spring-test', version: '6.2.15'
    implementation "org.springframework:spring-core:${springVersion}"
    implementation "org.springframework:spring-context:${springVersion}"
    testImplementation "org.springframework:spring-test:${springVersion}"
    implementation "org.springframework:spring-webmvc:${springVersion}"

    implementation "org.springframework:spring-jdbc:${springVersion}"
    implementation "org.springframework:spring-tx:${springVersion}"

    implementation "org.mybatis:mybatis:3.5.15"
    implementation "org.mybatis:mybatis-spring:3.0.3"

    implementation("org.mariadb.jdbc:mariadb-java-client:3.3.3")
    compileOnly("org.projectlombok:lombok:1.18.44")
    annotationProcessor("org.projectlombok:lombok:1.18.44")
    testCompileOnly("org.projectlombok:lombok:1.18.44")
    testAnnotationProcessor("org.projectlombok:lombok:1.18.44")
    // Source: https://mvnrepository.com/artifact/com.zaxxer/HikariCP
    implementation("com.zaxxer:HikariCP:7.0.2")
    // Source: https://mvnrepository.com/artifact/org.modelmapper/modelmapper
    implementation("org.modelmapper:modelmapper:3.1.1")
    // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
//    implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.17.2")
    implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.22.1'
    // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
    implementation("org.apache.logging.log4j:log4j-api:2.17.2")
    // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
    implementation("org.apache.logging.log4j:log4j-core:2.17.2")

    implementation("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2")
    implementation("org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1")

    implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'

}

test {
    useJUnitPlatform()

    tasks.withType(JavaCompile).configureEach {
        options.compilerArgs += ["-parameters"]
    }
}

그리고 TodoController.java 코드를 바꿈

    @GetMapping("/read")
//    public void read(Long tno, Model model){
        public void read(Long tno, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

최종코드

더보기
package com.example.springex_web.controller;


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("/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){
        public void read(Long tno, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

}

지금까지 한 코드를 실행하게 되면

http://localhost:8080/todo/read?tno=xx

로 입력했을때 정상적으로 실행되는 화면


이제 Modify와 List 버튼을 눌렀을때 링크로 연결되는 코드를 추가

 

read.jsp 코드 수정

script 코드를 추가

<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>

 

 

최종 코드

더보기
<%@ 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>

 

 

Modify는 지금 없기 때문에 안뜨고 List를 눌렀을때는 정상적으로 뜬다


read.jsp와 list.jsp를 연결하는 링크를 걸어줌

 

list.jsp를 수정

<a href="/todo/read?tno=${dto.tno}" class="text-decoration-none" 
   data-tno="${dto.tno}">

 

최종코드

더보기
<%--
  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">
                            <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>
            </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>

 

 

눌렀을때 정상적으로 넘어가게 됨


Todo의 삭제 기능 개발
• 수정 화면에서 POST방식을 통해서 삭제 처리
/todo/register(GET) => /todo/list(GET)  => /todo/read?tno=xxx(GET)   => /todo/modify?tno=xxx(GET)

 

TodoController.java코드에

@GetMapping("/read")

 

부분을

@GetMapping({"/read", "/modify"})

 

으로 바꿈

 

더보기
package com.example.springex_web.controller;


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("/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);
    }

}


modify.jsp가 없기 때문에

read.jsp를 복붙해서 만듬

 

title과 dueDate에서 readonly를 빼줌

 

하단에 사진에 표시 되어 있는 disabled를 삭제

 

 

사진에 표시 되어 있는 부분 (Remove버튼 )추가

<button type="button" class="btn btn-danger">Remove</button>

 

 

form코드를 추가

=> 사용자가 화면에서 수정한 할 일(Todo) 데이터들을 이 상자에 잘 모아서, 눈에 보이지 않는 안전한 방식(post)으로, 

서버의 /todo/modify라는 목적지(action)로 배달해 줘!" 라는 뜻

<form action="/todo/modify" method="post">

script바로 위에서 닫아줌

 

 

 

최종코드

더보기
<%@ 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>
                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>

modify버튼을 클릭해서 들어가면

 

remove 버튼이 보임


 

remove 버튼이 동작하도록 코드 추가

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)

를 추가


TodoController에 remove에 관한 method 추가

list로 redirect되고 로그가 찍히도록

 

아직은 데이터베이스에 연결하지 않아서 실제로 지워지진 않음

 

 

@PostMapping ("/remove")
public String remove(Long tno, RedirectAttributes redirectAttributes) {
    log.info("-----------------remove-------------------");
    log.info("tno: " + tno);
    return "redirect:/todo/list";
}

 

더보기
package com.example.springex_web.controller;


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("/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);
        return "redirect:/todo/list";
    }

}

 


TodoMapper.java

Mapper(매퍼) 인터페이스나 Service(서비스) 계층에서 사용하는 **"데이터 삭제를 위한 약속(메서드 선언)

void delete(Long tno);

추가

더보기
package com.example.springex_web.mapper;

import com.example.springex_web.domain.TodoVO;

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);
}

 


Todomapper.xml

MyBatis(마이바티스) XML 파일에서 실제 데이터베이스에 **"이 번호(tno)를 가진 데이터를 지워라!"**라고 

명령을 내리는 SQL 문장

    <delete id="delete">
        delete from tbl_todo where tno = #{tno}
    </delete>

을 추가

더보기
<?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>


</mapper>

 


TodoService에 remove 인터페이스 추가

Service(서비스) 계층에서 사용하는 메서드 선언으로, **"비즈니스 로직상에서 특정 데이터를 삭제하겠다"**는 의지를 담은 코드

void remove(Long tno);
더보기
package com.example.springex_web.service;

import com.example.springex_web.dto.TodoDTO;

import java.util.List;

public interface TodoService {
    void register(TodoDTO todoDTO);
    List<TodoDTO> getAll();
    TodoDTO getOne(Long tno);
    void remove(Long tno);
}

TodoSetviceImpl에

Service 계층(구현체)에서 진짜로 일을 시키는 부분, 자바 인터페이스에서

"삭제라는 기능을 만들자!"라고 약속(설계)했다면, 이 코드는 그 약속을 실제로 어떻게 실행할지 적어둔 실행 지침서

@Override
public void remove(Long tno) {
    todoMapper.delete(tno);
}

추가

더보기
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() {
    }
}

 

 

TodoController.java에

실행 스위치

=> 컨트롤러(TodoController) 안에서 이 코드를 적으면 앞에서 만든 Service → Mapper → XML(SQL)가

순서대로 동작하며 실제 데이터베이스의 데이터가 삭제

todoService.remove(tno);

추가

더보기
package com.example.springex_web.controller;


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("/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";
    }

}

 

remove 버튼이 잘 동작함


Modify를 동작시키는 코드를 만듬

TodoMapper.java

MyBatis 매퍼(Mapper) 인터페이스에서 **"기존의 할 일 데이터를 수정하겠다"**고 정의한 추상 메서드

void update(TodoVO todoVO);

를 추가해서

더보기
package com.example.springex_web.mapper;

import com.example.springex_web.domain.TodoVO;

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);
}

 


TodoMapper.xml에

 

MyBatis 매퍼 XML 파일에서 사용하는 코드로, 전달받은 객체의 필드값들을 사용하여 

데이터베이스(tbl_todo)에 저장된 기존 할 일 정보를 최신 내용으로 수정하는 SQL 실행문

where tno = #{tno} 조건을 통해 전체 데이터가 아닌 특정 번호(tno)에 해당하는 

단 하나의 행(Row)만 찾아 정확하게 갱신하는 역할

<update id="update">
    update tbl_todo set title = #{title}, dueDate = #{dueDate}, finished = #{finished} where tno = #{tno}
</update>

코드를 추가

더보기
<?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>


</mapper>

 


TodoService.java에

Service(서비스) 계층의 인터페이스에서 **"사용자가 수정한 할 일 데이터를 처리하겠다"**고 선언한 메서드

void modify(TodoDTO todoDTO);

 

를 추가

더보기
package com.example.springex_web.service;


import com.example.springex_web.domain.TodoVO;
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;

@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 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);
    }
}

TodoServiceImpl.java에

아래 코드를 추가

Service 계층(구현체)**에서 **"화면에서 넘어온 수정 데이터를 데이터베이스 형식에 맞춰 실제로 반영하는 실행부

@Override
public void modify(TodoDTO todoDTO) {
    TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
            todoMapper.update(todoVO);
}

 

더보기
package com.example.springex_web.service;


import com.example.springex_web.domain.TodoVO;
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;

@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 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);
    }
}

 

 

이렇게 까지 하면

modify 버튼이 제대로 동작함

 

역할: 데이터의 영속성 부여 및 실제 수정 실행 (Business Logic & Persistence)

 실제로 데이터베이스의 내용을 물리적으로 바꾸는 작업을 수행

  • TodoMapper (Interface & XML): 객체 지향 언어인 자바의 명령을 관계형 데이터베이스(MySQL 등)가 알아듣는 SQL(UPDATE) 문으로 변환하여 실행합니다. 특정 tno를 찾아 실제 레코드를 갱신하는 물리적 처리가 여기서 일어남
  • TodoServiceImpl: 컨트롤러로부터 받은 데이터 주머니(DTO)를 데이터베이스 전용 객체(VO)로 변환(ModelMapper)하고, 매퍼를 호출하는 실행 로직을 담음

 


finished를 되게 하려면 체크박스를 되게하는 포맷터가 필요

 

CheckboxFormatter를 만듬

HTTP 요청 파라미터(String)를 자바 객체의 필드 타입(Boolean)으로 변환하는 '타입 컨버터' 역할을 함

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(); }
}

 

 


Servlet에 등록을 해야됨

servlet-context.xml 코드 수정

이 코드는 **스프링 설정 파일(servlet-context.xml 등)**에서 이전에 만든

CheckboxFormatter를 스프링의 빈(Bean)으로 등록하는 설정

<bean class="com.example.springex_web.controller.formatter.CheckboxFormatter"/>

를 추가

 

더보기
<?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>

 


TodoController.java에서

잘못된 값이 입력되면 안되니때문에 유효성 체크를 해야함

 

수정 화면에서 제출된 데이터를 검증하고, 오류가 있으면 에러 메시지와 함께 수정 페이지로 되돌리며

오류가 없으면 서비스를 호출해 DB 정보를 갱신하는 코드입니다.

데이터 수정을 마친 후에는 사용자를 다시 목록 페이지(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";
    }

를 추가

 

더보기
package com.example.springex_web.controller;


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("/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";
    }

}

modify.jsp를 에

register.jsp에서 script코드를 가져옴

 

JSP의 c:forEach를 사용하여 서버에서 전달된 검증 오류(errors) 목록을

자바스크립트 객체(serverValidResult) 형태로 변환하여 저장하는 코드

이렇게 변환된 객체는 브라우저 콘솔에 출력되거나,

화면에서 사용자에게 어떤 필드(제목, 날짜 등)에 어떤 에러가 발생했는지 동적으로 보여주기 위한 기초 자료로 사용

<script>
  const serverValidResult = {}
  <c:forEach items="${errors}" var="error">
  serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
  </c:forEach>
  console.log(serverValidResult)
</script>

 

아래 코드를 추가

이벤트의 기본 동작과 전파를 중단시킨 후, 자바스크립트를 이용해

폼(formObj)의 목적지(Action)와 전송 방식(Method)을 수정용 설정으로 변경하여 강제 제출하는 코드

주석 처리된 self.location 방식과 달리,

POST 방식을 사용하여 데이터를 안전하게 서버로 보내고 수정 로직을 실행하기 위해 작성

e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/modify"
formObj.method = "post"
formObj.submit()
<%--self.location="/todo/modify?tno="+${dto.tno}--%>

더보기
<%@ 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) {
                  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>

 

이제 modify를 누르면 update가 된다 finished도 가능

 

역할: 외부 데이터의 수신 및 규격화 (Data Ingest & Validation)

이 단계는 사용자가 브라우저에서 보낸 데이터를 서버 내부에서 다룰 수 있는 상태로 만드는 과정

  • CheckboxFormatter: HTTP 프로토콜 특성상 문자열("on")로 전송되는 체크박스 데이터를 자바의 논리형(Boolean)
    필드에 담을 수 있도록 **타입 일치(Type Casting)**를 수행
  • Controller (@Valid): 들어온 데이터가 서버의 규칙(글자 수 제한, 필수 값 등)에 맞는지 유효성 검사를 수행하고,
    틀렸을 경우 다시 입력 페이지로 돌려보내는 흐름 제어를 담당
  • JSP & Script: 클라이언트 측에서 서버로 데이터를 보낼 때, POST 방식을 강제하고 글 번호(tno) 등
    식별자를 정확히 포함하도록 데이터 전송 규격을 맞춤


modify.jsp에

안정성을 위해

e.preventDefault()
e.stopPropagation()

추가

더보기
<%@ 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>