java 프레임워크 기반 프로그래밍 게시판 기능 프로젝트

2026. 4. 8. 19:24프로젝트

반응형

26-04-08 

spring_framework

 

더보기
Directory structure:
└── ynkite-spring_framework/
    ├── gradlew
    ├── gradlew.bat
    ├── gradle/
    │   └── wrapper/
    │       └── gradle-wrapper.properties
    └── src/
        ├── main/
        │   ├── java/
        │   │   ├── lombok.config
        │   │   └── com/
        │   │       └── example/
        │   │           └── spring_framework/
        │   │               ├── HelloServlet.java
        │   │               ├── config/
        │   │               │   └── ModelMapperConfig.java
        │   │               ├── controller/
        │   │               │   ├── BoardController.java
        │   │               │   ├── exception/
        │   │               │   │   └── CommonExceptionAdvice.java
        │   │               │   └── formatter/
        │   │               │       ├── CheckboxFormatter.java
        │   │               │       └── LocalDateFormatter.java
        │   │               ├── domain/
        │   │               │   └── BoardVO.java
        │   │               ├── dto/
        │   │               │   ├── BoardDTO.java
        │   │               │   ├── PageRequestDTO.java
        │   │               │   └── PageResponseDTO.java
        │   │               ├── mapper/
        │   │               │   └── BoardMapper.java
        │   │               └── service/
        │   │                   ├── BoardService.java
        │   │                   └── BoardServiceImpl.java
        │   ├── resources/
        │   │   ├── log4j2.xml
        │   │   └── mappers/
        │   │       └── BoardMapper.xml
        │   └── webapp/
        │       ├── index.jsp
        │       ├── resources/
        │       │   └── test.html
        │       └── WEB-INF/
        │           ├── root-context.xml
        │           ├── servlet-context.xml
        │           ├── web.xml
        │           └── views/
        │               ├── custom404.jsp
        │               └── board/
        │                   ├── list.jsp
        │                   ├── modify.jsp
        │                   ├── read.jsp
        │                   └── register.jsp
        └── test/
            └── java/
                └── com/
                    └── example/
                        └── spring_framework/
                            ├── mapper/
                            │   └── BoardMapperTests.java
                            └── service/
                                └── BoardServiceTests.java

 

더보기
================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi

# Collect all arguments for the java command;
#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
#     shell script including quotes and variable substitutions, so put them in
#     double quotes to make sure that they get re-expanded; and
#   * put everything else in single quotes, so that it's not re-expanded.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"



================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega



================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists



================================================
FILE: src/main/java/lombok.config
================================================
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier




================================================
FILE: src/main/java/com/example/spring_framework/HelloServlet.java
================================================
package com.example.spring_framework;

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/spring_framework/config/ModelMapperConfig.java
================================================
package com.example.spring_framework.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
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/spring_framework/controller/BoardController.java
================================================
package com.example.spring_framework.controller;

import com.example.spring_framework.dto.BoardDTO;
import com.example.spring_framework.dto.PageRequestDTO;
import com.example.spring_framework.service.BoardService;
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("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @GetMapping("/list")
    public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
        if(bindingResult.hasErrors()){
            pageRequestDTO = PageRequestDTO.builder().build();
        }
        model.addAttribute("responseDTO", boardService.getList(pageRequestDTO));
    }

    @GetMapping("/register")
    public void register(PageRequestDTO pageRequestDTO) {
    }

    @PostMapping("/register")
    public String registerPost(@Valid PageRequestDTO pageRequestDTO, BoardDTO boardDTO,
                               BindingResult bindingResult,
                               RedirectAttributes redirectAttributes) {
        if(bindingResult.hasErrors()) {
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/register";
        }
        boardService.register(boardDTO);
        return "redirect:/board/list";
    }

    @GetMapping({"/read", "/modify"})
    public void read(Long bno, PageRequestDTO pageRequestDTO, Model model) {
        BoardDTO boardDTO = boardService.getOne(bno);
        model.addAttribute("dto", boardDTO);
    }

    @PostMapping("/remove")
    public String remove(Long bno, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes) {
        boardService.remove(bno);
        log.info("-----------------remove-------------------");
        log.info("bno: " + bno);
        return "redirect:/board/list?" + pageRequestDTO.getLink();
    }

    @PostMapping("/modify")
    public String modify(@Valid BoardDTO boardDTO,
                         BindingResult bindingResult,
                         PageRequestDTO pageRequestDTO,
                         RedirectAttributes redirectAttributes) {
        if(bindingResult.hasErrors()) {
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("bno", boardDTO.getBno());
            return "redirect:/board/modify";
        }
        boardService.modify(boardDTO);
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/read?" + pageRequestDTO.getLink();
    }
}


================================================
FILE: src/main/java/com/example/spring_framework/controller/exception/CommonExceptionAdvice.java
================================================
package com.example.spring_framework.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());

        StringBuilder buffer = new StringBuilder("<html><body>");
        buffer.append("<h2>시스템 에러가 발생했습니다.</h2>");
        buffer.append("<p>에러 내용: " + exception.getMessage() + "</p>");
        buffer.append("<ul>");


        Arrays.stream(exception.getStackTrace()).forEach(stackTraceElement -> {
            buffer.append("<li>" + stackTraceElement + "</li>");
        });

        buffer.append("</ul></body></html>");
        return buffer.toString();
    }


    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String notFound() {
        return "custom404";
    }
}


================================================
FILE: src/main/java/com/example/spring_framework/controller/formatter/CheckboxFormatter.java
================================================
package com.example.spring_framework.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/spring_framework/controller/formatter/LocalDateFormatter.java
================================================
package com.example.spring_framework.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/spring_framework/domain/BoardVO.java
================================================
package com.example.spring_framework.domain;

import lombok.*;
import java.time.LocalDate;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class BoardVO {
    private Long bno;
    private String title;
    private String content;
    private String writer;
    private String category;
    private String location;
    private LocalDate travelDate;
    private int maxPeople;
    private int currentPeople;
    private String status;
    private LocalDate regDate;
    private LocalDate modDate;
}


================================================
FILE: src/main/java/com/example/spring_framework/dto/BoardDTO.java
================================================
package com.example.spring_framework.dto;

import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {

    private Long bno;

    @NotEmpty
    private String title;

    @NotEmpty
    private String content;

    @NotEmpty
    private String writer;

    private String category;

    private String status;

    @NotEmpty
    private String location;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate travelDate;
    
    private int maxPeople;
    private int currentPeople;

    private LocalDate regDate;
    private LocalDate modDate;
}


================================================
FILE: src/main/java/com/example/spring_framework/dto/PageRequestDTO.java
================================================
package com.example.spring_framework.dto;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.util.Arrays;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {

    @Builder.Default
    @Min(value = 1)
    @Positive
    private int page = 1;

    @Builder.Default
    @Min(value = 10)
    @Max(value = 100)
    @Positive
    private int size = 10;

    private String link;
    private String[] types;
    private String keyword;

    private LocalDate from;
    private LocalDate to;

    public int getSkip() {
        return (page - 1) * 10;
    }

    public String getLink() {
        if (link == null) {
            StringBuilder builder = new StringBuilder();

            builder.append("page=" + this.page);
            builder.append("&size=" + this.size);

            if (types != null && types.length > 0) {
                for (int i = 0; i < types.length; i++) {
                    builder.append("&types=" + types[i]);
                }
            }

            if (keyword != null) {
                try {
                    builder.append("&keyword=" + URLEncoder.encode(keyword, "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }

            if (from != null) {
                builder.append("&from=" + from.toString());
            }

            if (to != null) {
                builder.append("&to=" + to.toString());
            }

            link = builder.toString();
        }
        return link;
    }

    public boolean checkType(String type) {
        if (types == null || types.length == 0) {
            return false;
        }
        return Arrays.asList(types).contains(type);
    }
}


================================================
FILE: src/main/java/com/example/spring_framework/dto/PageResponseDTO.java
================================================
package com.example.spring_framework.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.util.List;

@Getter
@ToString
public class PageResponseDTO<E> {

    private int page;
    private int size;
    private int total;

    private int start;
    private int end;

    private boolean prev;
    private boolean next;

    private List<E> dtoList;

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
        this.page = pageRequestDTO.getPage();
        this.size = pageRequestDTO.getSize();
        this.total = total;
        this.dtoList = dtoList;

        this.end = (int)(Math.ceil(this.page / 10.0)) * 10;
        this.start = this.end - 9;

        int last = (int)(Math.ceil((total / (double)size)));

        this.end = end > last ? last : end;
        this.prev = this.start > 1;
        this.next = total > this.end * this.size;
    }
}


================================================
FILE: src/main/java/com/example/spring_framework/mapper/BoardMapper.java
================================================
package com.example.spring_framework.mapper;


import com.example.spring_framework.domain.BoardVO;
import com.example.spring_framework.dto.PageRequestDTO;

import java.util.List;

public interface BoardMapper {

    String getTime();

    void insert(BoardVO boardVO);

    List<BoardVO> selectAll();

    BoardVO selectOne(Long bno);

    void delete(Long bno);
    void update(BoardVO boardVO);

    List<BoardVO> selectList(PageRequestDTO pageRequestDTO);
    int getCount(PageRequestDTO pageRequestDTO);
}



================================================
FILE: src/main/java/com/example/spring_framework/service/BoardService.java
================================================
package com.example.spring_framework.service;

import com.example.spring_framework.dto.BoardDTO;
import com.example.spring_framework.dto.PageRequestDTO;
import com.example.spring_framework.dto.PageResponseDTO;

public interface BoardService {


    void register(BoardDTO boardDTO);
    PageResponseDTO<BoardDTO> getList(PageRequestDTO pageRequestDTO);
    BoardDTO getOne(Long bno);
    void remove(Long bno);
    void modify(BoardDTO boardDTO);
}


================================================
FILE: src/main/java/com/example/spring_framework/service/BoardServiceImpl.java
================================================
package com.example.spring_framework.service;

import com.example.spring_framework.domain.BoardVO;
import com.example.spring_framework.dto.BoardDTO;
import com.example.spring_framework.dto.PageRequestDTO;
import com.example.spring_framework.dto.PageResponseDTO;
import com.example.spring_framework.mapper.BoardMapper;
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 BoardServiceImpl implements BoardService {

    // TodoMapper 대신 BoardMapper 주입
    private final BoardMapper boardMapper;
    private final ModelMapper modelMapper;

    @Override
    public void register(BoardDTO boardDTO) {
        log.info(modelMapper);
        // DTO를 VO로 변환
        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        log.info(boardVO);
        boardMapper.insert(boardVO);
    }

    @Override
    public PageResponseDTO<BoardDTO> getList(PageRequestDTO pageRequestDTO) {
        List<BoardVO> voList = boardMapper.selectList(pageRequestDTO);

        List<BoardDTO> dtoList = voList.stream()
                .map(vo -> modelMapper.map(vo, BoardDTO.class))
                .collect(Collectors.toList());

        int total = boardMapper.getCount(pageRequestDTO);

        PageResponseDTO<BoardDTO> pageResponseDTO = PageResponseDTO.<BoardDTO>withAll()
                .dtoList(dtoList)
                .total(total)
                .pageRequestDTO(pageRequestDTO)
                .build();

        return pageResponseDTO;
    }

    @Override
    public BoardDTO getOne(Long bno) {
        BoardVO boardVO = boardMapper.selectOne(bno);
        BoardDTO boardDTO = modelMapper.map(boardVO, BoardDTO.class);
        return boardDTO;
    }

    @Override
    public void remove(Long bno) {
        boardMapper.delete(bno);
    }

    @Override
    public void modify(BoardDTO boardDTO) {
        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        boardMapper.update(boardVO);
    }
}



================================================
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/mappers/BoardMapper.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.spring_framework.mapper.BoardMapper">

    <sql id="search">
        <where>
            <if test="types != null and types.length > 0">
                <foreach collection="types" item="type" open="(" close=")" separator=" OR ">
                    <if test="type == 't'.toString()">
                        title like concat('%', #{keyword}, '%')
                    </if>
                    <if test="type == 'c'.toString()">
                        content like concat('%', #{keyword}, '%')
                    </if>
                    <if test="type == 'w'.toString()">
                        writer like concat('%', #{keyword}, '%')
                    </if>
                </foreach>
            </if>

            <if test="from != null">
                AND regDate >= #{from}
            </if>
            <if test="to != null">
                <![CDATA[
            AND regDate <= #{to}
            ]]>
            </if>
        </where>
    </sql>

    <select id="getTime" resultType="string">
        SELECT now()
    </select>

    <insert id="insert">
        insert into tbl_trip_board (title, content, writer, category, location, travelDate, maxPeople, currentPeople, status)
        values (#{title}, #{content}, #{writer}, #{category}, #{location}, #{travelDate}, #{maxPeople}, #{currentPeople}, #{status})
    </insert>

    <select id="selectAll" resultType="com.example.spring_framework.domain.BoardVO">
        SELECT * FROM tbl_trip_board ORDER BY bno DESC
    </select>

    <select id="selectOne" resultType="com.example.spring_framework.domain.BoardVO">
        SELECT * FROM tbl_trip_board WHERE bno = #{bno}
    </select>

    <delete id="delete">
        DELETE FROM tbl_trip_board WHERE bno = #{bno}
    </delete>

    <update id="update">
        UPDATE tbl_trip_board
        SET title = #{title},
            content = #{content},
            category = #{category},
            location = #{location},
            travelDate = #{travelDate},
            maxPeople = #{maxPeople},
            currentPeople = #{currentPeople},
            status = #{status},
            modDate = now()
        WHERE bno = #{bno}
    </update>

    <select id="selectList" resultType="com.example.spring_framework.domain.BoardVO">
        SELECT * FROM tbl_trip_board
        <include refid="search"></include>
        ORDER BY bno DESC LIMIT #{skip}, #{size}
    </select>

    <select id="getCount" resultType="int">
        SELECT count(bno) FROM tbl_trip_board
        <include refid="search"></include>
    </select>

</mapper>


================================================
FILE: src/main/webapp/index.jsp
================================================
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>


================================================
FILE: src/main/webapp/resources/test.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

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

    <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:/mappers/*.xml"></property>
    </bean>
    <bean id="modelMapper" class="org.modelmapper.ModelMapper" />

    <mybatis:scan base-package="com.example.spring_framework.mapper"></mybatis:scan>

    <context:component-scan base-package="com.example.spring_framework.config"></context:component-scan>

    <context:component-scan base-package="com.example.spring_framework.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 conversion-service="conversionService" />

    <mvc:resources mapping="/resources/**" location="/resources/" />

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".jsp" />
    </bean>

    <context:component-scan base-package="com.example.spring_framework.controller"/>

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="formatters">
            <set>
                <bean class="com.example.spring_framework.controller.formatter.LocalDateFormatter"/>
                <bean class="com.example.spring_framework.controller.formatter.CheckboxFormatter"/>
            </set>
        </property>
    </bean>

</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>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>


================================================
FILE: src/main/webapp/WEB-INF/views/custom404.jsp
================================================
<%@ 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/board/list.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!doctype html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>여행 파트너 구인 게시판</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>

        body { padding-bottom: 120px; }
        .container-fluid { max-width: 1400px; }
        .table th { text-align: center; background-color: #f8f9fa; }
        .table td { vertical-align: middle; text-align: center; }
        .table td.text-start { text-align: left !important; }
        .status-dot {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 4px;
        }
        .dot-active { background-color: #28a745; }
        .dot-closed { background-color: #adb5bd; }
    </style>
</head>
<body class="bg-light">

<div class="container-fluid mt-3">
    <nav class="navbar navbar-expand-lg bg-white border mb-3">
        <div class="container-fluid">
            <a class="navbar-brand fw-bold" href="/board/list">TRIP PARTNER</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <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" href="/board/list">Home</a></li>--%>
<%--                </ul>--%>
            </div>
        </div>
    </nav>

    <div class="card mb-3">
        <div class="card-body">
            <h5 class="card-title">검색 필터</h5>
            <form action="/board/list" method="get">
                <input type="hidden" name="size" value="${pageRequestDTO.size}">

                <div class="mb-3">
                    <input type="checkbox" name="types" value="t" ${pageRequestDTO.checkType("t")?"checked":""}> 제목
                    <input type="checkbox" name="types" value="w" ${pageRequestDTO.checkType("w")?"checked":""}> 작성자
                    <input type="text" name="keyword" class="form-control d-inline-block w-auto ms-2" value='<c:out value="${pageRequestDTO.keyword}"/>' placeholder="검색어 입력">
                </div>

                <div class="input-group mb-3 dueDateDiv w-50">
                    <span class="input-group-text">작성일</span>
                    <input type="date" name="from" class="form-control" value="${pageRequestDTO.from}">
                    <input type="date" name="to" class="form-control" value="${pageRequestDTO.to}">
                </div>

                <div class="float-end">
                    <button class="btn btn-primary" type="submit">조회</button>
                    <button class="btn btn-secondary clearBtn" type="button">초기화</button>
                </div>
            </form>
        </div>
    </div>

    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <span>모집 목록</span>
            <button type="button" class="btn btn-sm btn-outline-primary"
                    onclick="location.href='/board/register?${pageRequestDTO.link}'">새 글 등록</button>
        </div>
        <div class="card-body p-0">
            <table class="table table-bordered table-hover m-0">
                <thead>
                <tr>
                    <th>번호</th>
                    <th>카테고리</th>
                    <th style="width: 35%">제목</th>
                    <th>작성자</th>
                    <th>상태</th>
                    <th>여행날짜</th>
                    <th>인원</th>
                    <th>작성일</th>
                </tr>
                </thead>
                <tbody>
                <c:forEach items="${responseDTO.dtoList}" var="dto">
                    <tr>
                        <td><c:out value="${dto.bno}"/></td>
                        <td class="text-muted fst-italic small"><c:out value="${dto.category}"/></td>
                        <td class="text-start ps-3">
                            <a href="/board/read?bno=${dto.bno}&${pageRequestDTO.link}" class="text-decoration-none text-primary fw-bold">
                                <c:out value="${dto.title}"/>
                            </a>
                        </td>
                        <td><c:out value="${dto.writer}"/></td>
                        <td>
                            <c:choose>
                                <c:when test="${dto.status == 'true' || dto.status == '1' || dto.status == '모집중'}">
                                    <span class="status-dot dot-active"></span><span class="text-success fw-bold">모집중</span>
                                </c:when>
                                <c:otherwise>
                                    <span class="status-dot dot-closed"></span><span class="text-muted">마감</span>
                                </c:otherwise>
                            </c:choose>
                        </td>
                        <td><c:out value="${dto.travelDate}"/></td>
                        <td class="fw-bold"><span class="text-primary">${dto.currentPeople}</span><span class="text-dark"> / ${dto.maxPeople}</span></td>
                        <td><c:out value="${dto.regDate}"/></td>
                    </tr>
                </c:forEach>
                </tbody>
            </table>

            <div class="m-3 float-end">
                <ul class="pagination">
                    <c:if test="${responseDTO.prev}">
                        <li class="page-item"><a class="page-link" data-num="${responseDTO.start - 1}">Previous</a></li>
                    </c:if>
                    <c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
                        <li class="page-item ${responseDTO.page == num ? 'active':''}">
                            <a class="page-link" data-num="${num}">${num}</a>
                        </li>
                    </c:forEach>
                    <c:if test="${responseDTO.next}">
                        <li class="page-item"><a class="page-link" data-num="${responseDTO.end + 1}">Next</a></li>
                    </c:if>
                </ul>
            </div>
        </div>
    </div>
    <div class="row footer">
        <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>
    document.querySelector(".pagination").addEventListener("click", function (e) {
        e.preventDefault()
        e.stopPropagation()
        const target = e.target
        if(target.tagName !== 'A') {
            return
        }
        const num = target.getAttribute("data-num")
        const formObj = document.querySelector("form")

        formObj.innerHTML += `<input type='hidden' name='page' value='\${num}'>`
        formObj.submit();
    }, false)

    document.querySelector(".clearBtn").addEventListener("click", function (e){
        e.preventDefault()
        e.stopPropagation()
        self.location = '/board/list'
    }, false)
</script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>


================================================
FILE: src/main/webapp/WEB-INF/views/board/modify.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!doctype html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Board Modify</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container-fluid { max-width: 1000px; }
        .card { border: none; }
    </style>
</head>
<body class="bg-light">


<div class="container-fluid" style="margin-top: 30px;">
    <div class="row">
        <div class="col-lg-10 mx-auto">
            <nav class="navbar navbar-expand-lg bg-white border-bottom rounded shadow-sm">
                <div class="container-fluid">
                    <a class="navbar-brand fw-bold" href="/board/list">TRIP PARTNER</a>
                    <div class="collapse navbar-collapse" id="navbarNav">
                    </div>
                </div>
            </nav>
        </div>
    </div>

    <div class="row content mt-3">
        <div class="col-lg-10 mx-auto">
            <div class="card shadow-sm border-0">
                <div class="card-header bg-white border-bottom py-3 fw-bold">
                    게시글 수정
                </div>
                <div class="card-body">
                    <form action="/board/modify" method="post">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label class="form-label small fw-bold text-muted">글번호</label>
                                <input type="text" name="bno" class="form-control bg-light" value='<c:out value="${dto.bno}"/>' readonly>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label class="form-label small fw-bold text-muted">작성자</label>
                                <input type="text" name="writer" class="form-control bg-light" value='<c:out value="${dto.writer}"/>' readonly>
                            </div>
                        </div>

                        <div class="row">
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">카테고리</label>
                                <select name="category" class="form-select">
                                    <option value="배낭여행" ${dto.category == '배낭여행' ? 'selected' : ''}>배낭여행</option>
                                    <option value="자유여행" ${dto.category == '자유여행' ? 'selected' : ''}>자유여행</option>
                                    <option value="패키지" ${dto.category == '패키지' ? 'selected' : ''}>패키지</option>
                                    <option value="맛집탐방" ${dto.category == '맛집탐방' ? 'selected' : ''}>맛집탐방</option>
                                    <option value="액티비티" ${dto.category == '액티비티' ? 'selected' : ''}>액티비티</option>
                                    <option value="기타" ${dto.category == '기타' ? 'selected' : ''}>기타</option>
                                </select>
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">여행지</label>
                                <input type="text" name="location" class="form-control" value='<c:out value="${dto.location}"/>'>
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">여행날짜</label>
                                <input type="date" name="travelDate" class="form-control" value='${dto.travelDate}'>
                            </div>
                        </div>

                        <div class="row">
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">현재 참여인원</label>
                                <input type="number" name="currentPeople" class="form-control" value="${dto.currentPeople}">
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">최대 모집인원</label>
                                <input type="number" name="maxPeople" class="form-control" value="${dto.maxPeople}">
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label small fw-bold text-muted">모집상태</label>
                                <select name="status" class="form-select">
                                    <option value="1" ${dto.status == 'true' || dto.status == '1' || dto.status == '모집중' ? 'selected' : ''}>모집중</option>
                                    <option value="0" ${dto.status == 'false' || dto.status == '0' || dto.status == '마감' ? 'selected' : ''}>마감</option>
                                </select>
                            </div>
                        </div>

                        <div class="mb-3">
                            <label class="form-label small fw-bold text-muted">제목</label>
                            <input type="text" name="title" class="form-control" value='<c:out value="${dto.title}"/>'>
                        </div>

                        <div class="mb-4">
                            <label class="form-label small fw-bold text-muted">내용</label>
                            <textarea name="content" class="form-control" rows="8"><c:out value="${dto.content}"/></textarea>
                        </div>

                        <div class="d-flex justify-content-end gap-2 border-top pt-3">
                            <button type="button" class="btn btn-danger btn-remove px-4">삭제</button>
                            <button type="button" class="btn btn-primary btn-modify px-4">저장</button>
                            <button type="button" class="btn btn-outline-secondary btn-list px-4">목록</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div class="row footer">
        <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>
    const formObj = document.querySelector("form")

    document.querySelector(".btn-remove").addEventListener("click",function(e) {
        e.preventDefault()
        e.stopPropagation()
        // formObj.action ="/board/remove"
        formObj.action=`/board/remove?${pageRequestDTO.link}`
        formObj.method ="post"
        formObj.submit()
    },false);

    document.querySelector(".btn-modify").addEventListener("click", function(e){
        e.preventDefault()
        e.stopPropagation()
        formObj.action ="/board/modify"
        formObj.method ="post"
        formObj.submit()
    },false)

    document.querySelector(".btn-list").addEventListener("click", function(e){
        e.preventDefault()
        e.stopPropagation()
        // self.location = "/board/list"
        self.location= `/board/list?${pageRequestDTO.link}`
    },false)
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>


================================================
FILE: src/main/webapp/WEB-INF/views/board/read.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Board Read</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    .status-dot {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      margin-right: 6px;
    }
    .dot-active { background-color: #28a745; box-shadow: 0 0 5px rgba(40, 167, 69, 0.5); }
    .dot-closed { background-color: #adb5bd; }
    .container-fluid { max-width: 1000px; }
    .card { border: none; }
  </style>
</head>
<body class="bg-light">

<div class="container-fluid" style="margin-top: 30px;">
  <div class="row">
    <div class="col-lg-10 mx-auto">
      <nav class="navbar navbar-expand-lg bg-white border-bottom rounded shadow-sm">
        <div class="container-fluid">
          <a class="navbar-brand fw-bold" href="/board/list">TRIP PARTNER</a>
          <div class="collapse navbar-collapse" id="navbarNav">
          </div>
        </div>
      </nav>
    </div>
  </div>

  <div class="row mt-3">
    <div class="col-lg-10 mx-auto">
      <div class="card shadow-sm border-0">
        <div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
          <h5 class="mb-0 fw-bold">상세 보기</h5>
          <span class="badge bg-light text-dark border">Bno: <c:out value="${dto.bno}"/></span>
        </div>
        <div class="card-body">
          <div class="row mb-3">
            <div class="col-md-4">
              <label class="form-label small text-muted">작성자</label>
              <div class="p-2 border-bottom fw-medium"><c:out value="${dto.writer}"/></div>
            </div>
            <div class="col-md-4">
              <label class="form-label small text-muted">등록일</label>
              <div class="p-2 border-bottom"><c:out value="${dto.regDate}"/></div>
            </div>
            <div class="col-md-4">
              <label class="form-label small text-muted">상태</label>
              <div class="p-2 border-bottom">
                <c:choose>
                  <c:when test="${dto.status == 'true' || dto.status == '1' || dto.status == '모집중'}">
                    <span class="status-dot dot-active"></span><span class="text-success fw-bold" style="font-size: 0.9rem;">모집중</span>
                  </c:when>
                  <c:otherwise>
                    <span class="status-dot dot-closed"></span><span class="text-muted" style="font-size: 0.9rem;">마감</span>
                  </c:otherwise>
                </c:choose>
              </div>
            </div>
          </div>

          <div class="row mb-3">
            <div class="col-md-4">
              <label class="form-label small text-muted">카테고리</label>
              <div class="p-2 border-bottom"><c:out value="${dto.category}"/></div>
            </div>
            <div class="col-md-4">
              <label class="form-label small text-muted">여행지</label>
              <div class="p-2 border-bottom"><c:out value="${dto.location}"/></div>
            </div>
            <div class="col-md-4">
              <label class="form-label small text-muted">참여 인원</label>
              <div class="p-2 border-bottom text-primary fw-bold">${dto.currentPeople} / ${dto.maxPeople} 명</div>
            </div>
          </div>

          <div class="mb-3">
            <label class="form-label small text-muted">제목</label>
            <div class="p-2 border rounded bg-light fw-bold"><c:out value="${dto.title}"/></div>
          </div>

          <div class="mb-4">
            <label class="form-label small text-muted">내용</label>
            <div class="p-3 border rounded bg-white" style="min-height: 250px; white-space: pre-wrap;"><c:out value="${dto.content}"/></div>
          </div>

          <div class="d-flex justify-content-end gap-2 border-top pt-3">
            <button type="button" class="btn btn-primary btn-modify px-4">수정</button>
            <button type="button" class="btn btn-outline-secondary btn-list px-4">목록</button>
          </div>
        </div>
      </div>
    </div>
  </div>

  <div class="row footer">
    <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>
  document.querySelector(".btn-modify").addEventListener("click", function (e) {
    self.location = `/board/modify?bno=${dto.bno}&${pageRequestDTO.link}`
  }, false)
  document.querySelector(".btn-list").addEventListener("click", function (e) {
    self.location = `/board/list?${pageRequestDTO.link}`
  }, false)
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>


================================================
FILE: src/main/webapp/WEB-INF/views/board/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="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Register Board</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container-fluid { max-width: 1000px; }
        .card { border: none; }
    </style>
</head>
<body class="bg-light">

<div class="container-fluid" style="margin-top: 30px;">
    <div class="row">
        <div class="col-lg-10 mx-auto">
            <nav class="navbar navbar-expand-lg bg-white border-bottom rounded shadow-sm">
                <div class="container-fluid">
                    <a class="navbar-brand fw-bold" href="/board/list">TRIP PARTNER</a>
                    <div class="collapse navbar-collapse" id="navbarNav">
                    </div>
                </div>
            </nav>
        </div>
    </div>


    <div class="row mt-3">
        <div class="col-lg-10 mx-auto">
            <div class="card shadow-sm border-0">
                <div class="card-header bg-white border-bottom py-3 fw-bold">
                    새 글 등록
                </div>
                <div class="card-body">
                    <form action="/board/register" method="post">
                        <div class="row mb-3">
                            <div class="col-md-8">
                                <label class="form-label small fw-bold text-muted">제목</label>
                                <input type="text" name="title" class="form-control" placeholder="제목을 입력하세요" required>
                            </div>
                            <div class="col-md-4">
                                <label class="form-label small fw-bold text-muted">작성자</label>
                                <input type="text" name="writer" class="form-control" placeholder="성함" required>
                            </div>
                        </div>

                        <div class="row mb-3">
                            <div class="col-md-6">
                                <label class="form-label small fw-bold text-muted">카테고리</label>
                                <select name="category" class="form-select">
                                    <option value="배낭여행">배낭여행</option>
                                    <option value="자유여행">자유여행</option>
                                    <option value="패키지">패키지</option>
                                    <option value="맛집탐방">맛집탐방</option>
                                    <option value="기타">기타</option>
                                </select>
                            </div>
                            <div class="col-md-6">
                                <label class="form-label small fw-bold text-muted">여행지 (장소)</label>
                                <input type="text" name="location" class="form-control" placeholder="상세 장소" required>
                            </div>
                        </div>

                        <div class="row mb-3">
                            <div class="col-md-3">
                                <label class="form-label small fw-bold text-muted">여행 날짜</label>
                                <input type="date" name="travelDate" class="form-control" required>
                            </div>
                            <div class="col-md-3">
                                <label class="form-label small fw-bold text-muted">최대 모집 인원</label>
                                <input type="number" name="maxPeople" class="form-control" min="1" value="4">
                            </div>
                            <div class="col-md-3">
                                <label class="form-label small fw-bold text-muted">현재 참여 인원</label>
                                <input type="number" name="currentPeople" class="form-control" min="1" value="1">
                            </div>
                            <div class="col-md-3">
                                <label class="form-label small fw-bold text-muted">모집 상태</label>
                                <select name="status" class="form-select">
                                    <option value="1" selected>모집중</option>
                                    <option value="0">마마감</option>
                                </select>
                            </div>
                        </div>

                        <div class="mb-4">
                            <label class="form-label small fw-bold text-muted">상세 내용</label>
                            <textarea name="content" class="form-control" rows="10" placeholder="여행 일정을 상세히 적어주세요.
(여행 날짜가 2일 이상이거나 기타 카테고리를 선택한 경우 내용을 자세히 입력해주세요)" required></textarea>

                        </div>

                        <div class="d-flex justify-content-end gap-2 border-top pt-3">
                            <button type="button" class="btn btn-primary btn-register px-4">등록</button>
                            <button type="button" class="btn btn-secondary btn-reset">초기화</button>
                            <button type="button" class="btn btn-outline-secondary btn-list px-4">목록</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <div class="row footer">
        <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.3/dist/js/bootstrap.bundle.min.js"></script>

<script>
    const formObj = document.querySelector("form")

    document.querySelector(".btn-register").addEventListener("click", function(e) {
        formObj.action = "/board/register"
        formObj.method = "post"
        formObj.submit()
    }, false);

    document.querySelector(".btn-reset").addEventListener("click", function(e){
        formObj.reset()
    }, false)

    document.querySelector(".btn-list").addEventListener("click", function (e) {
        self.location = "/board/list?${pageRequestDTO.link}"
    }, false)
</script>
</body>
</html>


================================================
FILE: src/test/java/com/example/spring_framework/mapper/BoardMapperTests.java
================================================
[Binary file]


================================================
FILE: src/test/java/com/example/spring_framework/service/BoardServiceTests.java
================================================
package com.example.spring_framework.service;

import com.example.spring_framework.dto.BoardDTO;
import com.example.spring_framework.dto.PageRequestDTO;
import com.example.spring_framework.dto.PageResponseDTO;
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;

@Log4j2
@ExtendWith(SpringExtension.class)
// root-context.xml 경로는 프로젝트 폴더 구조(WEB-INF/root-context.xml 또는 WEB-INF/spring/root-context.xml)에 맞게 맞추세요.
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {
        log.info("--- 게시글 등록 테스트 ---");

        BoardDTO boardDTO = BoardDTO.builder()
                .title("서비스 게시글 등록 테스크 제목")
                .content("서비스 테스트 내용입니다... ")
                .writer("user service")
                .category("기타")
                .location("서울시")
                .travelDate(java.time.LocalDate.now())
                .maxPeople(4)
                .currentPeople(2)
                .status("1")
                .build();

        boardService.register(boardDTO);
    }

    @Test
    public void testPaging() {
        log.info("--- 페이징 및 목록 조회 테스트 ---");

        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .build();

        PageResponseDTO<BoardDTO> responseDTO = boardService.getList(pageRequestDTO);

        // 전체 개수 확인
        log.info("Total: " + responseDTO.getTotal());

        // 목록 출력
        responseDTO.getDtoList().forEach(boardDTO -> log.info(boardDTO));
    }

    @Test
    public void testGetOne() {
        log.info("--- 단일 게시물 조회 테스트 ---");

        Long bno = 1L;
        BoardDTO boardDTO = boardService.getOne(bno);

        log.info(boardDTO);
    }
}


java 프레임워크 기반 프로그래밍

리스트(list) 페이지 구현
검색 및 필터링 구현
페이지네이션 구현
수정(modify) 구현
삭제(remove) 구현
조회(read) 구현
등록(register) 구현



modify에서 다시 list로 돌아돌 때는 기존의 검색 했던 내용을 그대로 남겨두기 때문에 수정을 했을때 이상해 질 수 있음.
그래서 처음으로 돌아가 페이지 1로 돌아가고 검색 한 것도 사라지게 함



read, remove, register는 원래 있던 페이지 정보를 받아서 list로 돌아가는 버튼을 누르면 원래 페이지로 돌아가도록 함(pageRequestDTO.getLink() 사용)

단, register에서는 등록 했을 경우 1페이지로 가서 등록한 것을 보여주지만 list로 돌아갈 경우 원래 있던 페이지로 돌아감



추후 추가 해볼 내용

검색 했을때 결과가 없다면 목록 화면에 검색 결과가 없습니다 라는 문구를 띄워주고
modify를 통해 수정 했을 경우 1페이지로 돌아가는 것이 아닌 검색 내용을 저장해서
수정한 내용이 검색 필터에 걸려서 다른 내용일 경우 안나오고 필터에 걸리지 않고
그대로 유지된다면 나오게 바꾸면 좋을거 같다.



Remove(삭제) 후의 페이지 처리:



예를 들어, 5페이지의 마지막 남은 게시글 하나를 삭제했을 때
그대로 5페이지로 돌아가게 하면 **"검색 결과가 없습니다"**라는 텅 빈 화면을 보게 됨
해결 아이디어: 삭제 후 getCount()를 다시 체크해서, 현재 페이지에 보여줄 데이터가 없다면 

현재 페이지 - 1로 이동시키는 로직을 추가

 

더보기

<View>
register (등록)

 

list (등록 후)

 

 

list (검색, 필터)

 

read

 

modify (수정 및 삭제)

 

list (수정 결과 확인)

 

 

list (삭제 후 화면)