Java/Spring Boot

Spring Boot 기초(comment) - AWS 풀스택 과정 80일차

awspspgh 2024. 11. 19. 09:32
목차
1. comment
2. security

 

1. comment

detail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>Boot Detail page</h1>
    <hr>

    <th:block th:with="bvo=${bdto.bvo}">
    <form action="/board/modify" method="post" id="modForm" enctype="multipart/form-data">
        <div class="mb-3">
            <label for="r" class="form-label">Created At</label>
            <input type="text" class="form-control" name="regDate" id="r" th:value="${bvo.regDate}" readonly>
        </div><div class="mb-3">
            <input type="hidden" class="form-control" name="bno" id="n" th:value="${bvo.bno}">
        </div>
        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" class="form-control" name="title" id="title" th:value="${bvo.title}" readonly>
        </div>
        <div class="mb-3">
            <label for="writer" class="form-label">Writer</label>
            <input type="text" class="form-control" name="writer" id="writer" th:value="${bvo.writer}" readonly>
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">Content</label>
            <textarea class="form-control" name="content" id="content" rows="3" readonly>[[${bvo.content}]]</textarea>
        </div>
        <!-- file print line -->
        <div class="mb-3">
            <ul class="list-group">
                <li th:each="fvo:${bdto.flist}" class="list-group-item">
                    <div th:if="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <img th:src="@{|/upload/${fvo.saveDir}/${fvo.uuid}_${fvo.fileName}|}" alt="img" />
                    </div>
                    <div th:unless="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <!-- icon -->
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paperclip" viewBox="0 0 16 16">
                            <path d="M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5a.5.5 0 0 1 1 0v7a.5.5 0 0 0 1 0V3a1.5 1.5 0 1 0-3 0v9a2.5 2.5 0 0 0 5 0V5a.5.5 0 0 1 1 0v7a3.5 3.5 0 1 1-7 0z"/>
                        </svg>
                    </div>
                    <div class="ms-2 me-auto">
                        <div class="fw-bold">[[${fvo.fileName}]]</div>
                        [[${fvo.regDate}]]
                    </div>
                    <span class="badge text-bg-success rounded-pill">[[${fvo.fileSize}]]Bytes</span>
                    <button type="button" th:data-uuid="${fvo.uuid}" class="btn btn-outline-danger bnt-nm file-x" disabled>x</button>
                </li>
            </ul>
        </div>
        <!-- file 추가 라인 -->
        <div class="mb-3">
            <input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
        </div>
        <button type="button" id="trigger" class="btn btn-primary" disabled>File Upload</button> <br>
        <button type="button" id="listBtn" class="btn btn-primary">List</button>
        <!-- de-tail page에서 modify 상태로 변경하기 위한 버튼 -->
        <button type="button" id="modBtn" class="btn btn-warning">Modify</button>
        <a th:href="@{/board/delete(bno=${bvo.bno})}">
            <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
        </a>
    </form>
        <!-- comment line -->
        <!-- post -->
        <div class="input-group mb-3">
            <span class="input-group-text" id="cmtWriter">nick</span>
            <input type="text" id="cmtText" class="form-control" placeholder="Add Comment..." aria-label="Username" aria-describedby="basic-addon1">
            <button type="button" id="cmtAddBtn" class="btn btn-secondary">post</button>
        </div>
        <!-- spread -->
        <ul class="list-group list-group-flush" id="cmtListArea">
            <li class="list-group-item">
                <div class="ms-2 me-auto">
                    <div class="fw-bold">writer</div>
                    Content
                </div>
                <span class="badge text-bg-primary rounded-pill">regDate</span>
            </li>
        </ul>
        <!-- moreBtn-->
        <div>
            <button type="button" id="moreBtn" data-page="1" class="btn btn-dark" style="visibility:hidden">MORE + </button>
        </div>
        <!-- modal -->
        <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h1 class="modal-title fs-5" id="cmtWriterMod">${authNick }</h1>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <input type="text" class="form-control" id="cmtTextMod">
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                        <button type="button" id="cmtModBtn" class="btn btn-primary">changes</button>
                    </div>
                </div>
            </div>
        </div>

    <script th:inline="javascript">
        const bnoVal = [[${bvo.bno}]]
        console.log(bnoVal);
    </script>
    </th:block>

    <script th:src="@{/js/boardDetail.js}"></script>
    <script th:src="@{/js/boardRegister.js}"></script>
    <script th:src="@{/js/boardComment.js}"></script>

    <script>
        spreadCommentList(bnoVal);
    </script>

</div>

 

◈ boardComment.js

console.log("boardComment.js in");
console.log(bnoVal);

document.getElementById('cmtAddBtn').addEventListener('click',()=>{
    const cmtText = document.getElementById('cmtText');
    if(cmtText == null || cmtText == ''){
        alert('댓글 내용을 입력해주세요.')
        cmtText.focus();
        return false;
    }
    const cmtData = {
        bno : bnoVal,
        writer : document.getElementById('cmtWriter').innerText,
        content : cmtText.value
    };
    postCommentToServer(cmtData).then(result => {
        if(result > 0){
            alert("댓글 등록 성공");
            cmtText.value="";
        }
        // 댓글 뿌리기 호출
        spreadCommentList(bnoVal);
    })
});

function spreadCommentList(bnoVal, page=1){
    getCommentListFromServer(bnoVal, page).then(result =>{
        console.log(result);
        const ul = document.getElementById('cmtListArea');
        if(result.cmtList.length > 0){
            if(page == 1){
                ul.innerHTML=""; // 기존 샘플값 비우기.
            }
            for(let cvo of result.cmtList){
                let li = `<li data-cno = "${cvo.cno}" class="list-group-item">`;
                li += `<div class="ms-2 me-auto">`;
                li += `<div class="fw-bold">${cvo.writer}</div>`;
                li += `${cvo.content}`;
                li += `</div>`;
                li += `<span class="badge text-bg-primary rounded-pill">${cvo.regDate}</span>`;
                li += `<button type="button" class="btn btn-outline-warning btn-sm mod" data-bs-toggle="modal" data-bs-target="#myModal">%</button>`;
                li += `<button type="button" class="btn btn-outline-danger btn-sm del">X</button>`;
                li += `</li>`;
                ul.innerHTML += li;
            }

            // page 처리
            let moreBtn= document.getElementById('moreBtn');
            // 전체 페이지가 현재 페이지보다 크다면... (나와야 할 페이지가 존재한다면...)
            if(result.pgvo.pageNo < result.realEndPage){
                moreBtn.style.visibility = "visible"; // 표시
                moreBtn.dataset.page = page + 1;
            }else{
                moreBtn.style.visibility = "hidden"; // 숨김
            }

        }else{
            let li = `<li class="list-group-item">Comment List Empty</li>`;
            ul.innerHTML = li;
        }
    });
};

document.addEventListener('click',(e)=> {
    if(e.target.id == 'moreBtn'){
        let page = parseInt(e.target.dataset.page);
        spreadCommentList(bnoVal, page);
    }
    if(e.target.classList.contains('del')){
        let cno = e.target.closest('li').dataset.cno;
        removeCommentToServer(cno).then(result => {
            if(result > 0){
                alert("삭제 성공!!");
                spreadCommentList(bnoVal);
            }
        })
    }
    if(e.target.classList.contains('mod')){
        let li = e.target.closest('li');
        let modWriter = li.querySelector(".fw-bold").innerText;
        document.getElementById('cmtWriterMod').innerText = modWriter;
        let cmtText =li.querySelector('.fw-bold').nextSibling; // 쿼리 값의 같은 부모의 다른 형제 값
        document.getElementById('cmtTextMod').value = cmtText.nodeValue; // 그냥 넣으면 Object로 입력
        
        // 수정 버튼 id = cmtModBtn dataset
        document.getElementById('cmtModBtn').setAttribute("data-cno", li.dataset.cno);
    }
    if(e.target.id == 'cmtModBtn'){
        let cmtModData={
            cno : e.target.dataset.cno,
            content : document.getElementById('cmtTextMod').value
        }
        console.log(cmtModData);

        // 비동기 처리
        updateCommentToServer(cmtModData).then(result =>{
            if(result > 0){
                alert("수정 성공!!");
            }
            // 모달창 닫기
            document.querySelector('.btn-close').click();
            spreadCommentList(bnoVal);
        });
    }
});

async function updateCommentToServer(cmtModData) {
    try{
        const url = "/comment/update"
        const config ={
            method : "put",
            headers : {
                'content-type':'application/json; charset=utf-8'
            },
            body : JSON.stringify(cmtModData)
        }
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch(error){
        console.log(error);
    }

}

async function removeCommentToServer(cno) {
    try{
        const url = "/comment/remove/"+cno;
        const config={
            method:'delete'
        }
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch(error){
        console.log(error);
    }
}

async function postCommentToServer(cmtData) {
    try {
        const url = "/comment/post";
        const config = {
            method: "post",
            headers:{
                'Content-Type':'application/json; charset=utf-8'
            },
            body: JSON.stringify(cmtData)
        };
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch (error) {
        console.log(error);
    }
}

async function getCommentListFromServer(bnoVal, page) {
    try{
        const resp = await fetch("/comment/list/"+bnoVal+"/"+page);
        const result = await resp.json();
        return result;
    } catch(error){
        console.log(error);
    }
}

 

◈ PagingHandler.java

package com.ezen.spring.handler;

import com.ezen.spring.domain.CommentVO;
import com.ezen.spring.domain.PagingVO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.xml.stream.events.Comment;
import java.util.List;

@Getter
@Setter
@ToString
public class PagingHandler {
    private int startPage;
    private int endPage;
    private int realEndPage;
    private boolean prev, next;

    private int totalCount;
    private PagingVO pgvo;

    private List<CommentVO> cmtList;

    public PagingHandler(PagingVO pgvo, int totalCount){
        this.pgvo = pgvo;
        this.totalCount = totalCount;

        // 1~10 => 10 / 11~20 => 20 / 21~30 => 30
        // (현재 페이지번호 / 10) 올림 => * 10
        this.endPage = (int)Math.ceil(pgvo.getPageNo() / 10.0)*10;
        this.startPage = endPage - 9;

        this.realEndPage = (int)Math.ceil(totalCount / (double)pgvo.getQty());

        if(realEndPage < endPage){
            this.endPage = realEndPage;
        }

        this.prev = this.startPage > 1; // 1 11 21
        this.next = this.endPage < realEndPage;
    }

    public PagingHandler(PagingVO pgvo, int totalCount, List<CommentVO> cmtList){
        this(pgvo, totalCount);
        this.cmtList = cmtList;
    }

}

 

◈ CommentVO.java

package com.ezen.spring.domain;

import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class CommentVO {
    private long cno;
    private long bno;
    private String writer;
    private String content;
    private String regDate;
}

 

◈ CommentController.java

package com.ezen.spring.controller;

import com.ezen.spring.domain.CommentVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.handler.PagingHandler;
import com.ezen.spring.service.CommentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@Slf4j
@RequestMapping("/comment/*")
@RestController
public class CommentController {
    private final CommentService csv;

    @PostMapping("/post")
    @ResponseBody
    public String post(@RequestBody CommentVO commentVO){
        log.info(">>> commentVO >> {}", commentVO);
        int isOk = csv.post(commentVO);
        return isOk > 0 ? "1" : "0";
    }

    @GetMapping("/list/{bno}/{page}")
    @ResponseBody
    public PagingHandler list(@PathVariable("bno")long bno, @PathVariable("page") int page){
        PagingVO pgvo = new PagingVO(page, 5);
        PagingHandler ph = csv.getList(bno, pgvo);
        log.info(">>> ph >>> {}", ph);
        return ph;
    }

    @DeleteMapping("/remove/{cno}")
    @ResponseBody
    public String remove(@PathVariable("cno") long cno){
        int isOk = csv.remove(cno);
        return isOk > 0 ? "1" : "0";
    }

    @PutMapping("/update")
    @ResponseBody
    public String update(@RequestBody CommentVO commentVO){
        int isOk = csv.update(commentVO);
        return isOk > 0 ? "1" : "0";
    }
}

 

◈ CommentService.java

package com.ezen.spring.service;

import com.ezen.spring.domain.CommentVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.handler.PagingHandler;

public interface CommentService {
    int post(CommentVO commentVO);

    PagingHandler getList(long bno, PagingVO pgvo);

    int remove(long cno);

    int update(CommentVO commentVO);
}

 

◈ CommentServiceImpl.java

package com.ezen.spring.service;

import com.ezen.spring.domain.CommentVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.handler.PagingHandler;
import com.ezen.spring.repository.CommentMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Slf4j
@Service
public class CommentServiceImpl implements CommentService{
    private final CommentMapper commentMapper;

    @Override
    public int post(CommentVO commentVO) {
        return commentMapper.post(commentVO);
    }

    @Transactional
    @Override
    public PagingHandler getList(long bno, PagingVO pgvo) {
        int totalCount = commentMapper.getTotalCount(bno);
        List<CommentVO> cmtList = commentMapper.getList(bno,pgvo);
        PagingHandler ph = new PagingHandler(pgvo, totalCount, cmtList);
        return ph;
    }

    @Override
    public int remove(long cno) {
        return commentMapper.remove(cno);
    }

    @Override
    public int update(CommentVO commentVO) {
        return commentMapper.update(commentVO);
    }
}

 

◈ CommentMapper.java

package com.ezen.spring.repository;

import com.ezen.spring.domain.CommentVO;
import com.ezen.spring.domain.PagingVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface CommentMapper {
    int post(CommentVO commentVO);

    int getTotalCount(long bno);

    List<CommentVO> getList(@Param("bno") long bno,@Param("pgvo") PagingVO pgvo);

    int remove(long cno);

    int update(CommentVO commentVO);
}

 

◈ commentMapper.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.ezen.spring.repository.CommentMapper">
    <insert id = "post">
        insert into comment(bno, writer, content) values(#{bno}, #{writer}, #{content})
    </insert>
    <select id="getTotalCount" resultType="int">
        select count(cno) from comment where bno = #{bno}
    </select>
    <select id="getList" resultType="com.ezen.spring.domain.CommentVO">
        select * from comment where bno = #{bno} order by cno desc limit #{pgvo.startIndex}, #{pgvo.qty}
    </select>
    <delete id="remove">
        delete from comment where cno = #{cno}
    </delete>
    <update id="update">
        update comment set content=#{content}, reg_date = now() where cno = #{cno}
    </update>
</mapper>

 

출력

 

2. security

 build.gradle (spring)

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.11'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.ezen'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
	implementation 'org.apache.tika:tika-core:2.4.1'
	implementation 'org.apache.tika:tika-parsers:2.4.1'
	implementation 'net.coobird:thumbnailator:0.4.17'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 sqlFile.txt

create table board(
bno bigint auto_increment,
title varchar(200) not null,
writer varchar(200) not null,
content text,
reg_date datetime default now(),
primary key(bno));

create table file(
uuid varchar(256) not null,
save_dir varchar(256) not null,
file_name varchar(200) not null,
file_type tinyint(1) default 0,
bno bigint not null,
file_size bigint,
reg_date datetime default now(),
primary key(uuid));

create table comment(
cno bigint auto_increment,
bno bigint not null,
writer varchar(200),
content text,
reg_date datetime default now(),
primary key(cno));

create table user(
email varchar(200),
pwd varchar(256),
nick_name varchar(200),
reg_date datetime default now(),
last_login datetime default now(),
primary key(email));

create table auth(
id int auto_increment,
email varchar(200),
auth varchar(50),
primary key(id)
foreign key(email) references user(email));

 

 SecurityConfig.java

package com.ezen.spring.config;

import com.ezen.spring.security.CustomUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    /* springSecurity6 => bcEncoder => createDelegationPasswordEncoder */

    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    // SecurityFilterChain 객체로 설정
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http.csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize ->
                    authorize.requestMatchers("/user/list").hasAnyRole("ADMIN")
                            .requestMatchers("/","/index","/js/**","/upload/**","/user/login", "/dist/**",
                                    "/user/signup","/board/list", "/board/detail", "/comment/list/**").permitAll()
                            .anyRequest().authenticated()
                )
                .formLogin(login -> login
                        .usernameParameter("email")
                        .passwordParameter("pwd")
                        .loginPage("/user/login")
                        .defaultSuccessUrl("/board/list").permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/user/logout")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .logoutSuccessUrl("/")
                )
                .build();
    }
    // userDetailService : spring에서 만든 클래스와 같은 객체
    @Bean
    UserDetailsService userDetailsService(){
        return new CustomUserService(); // security 패키지에 클래스로 생성
    }

    // authenticationManager 객체
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

 

 AuthVO.java

package com.ezen.spring.domain;

import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class AuthVO {
    private int id;
    private String email;
    private String auth;
}

 

◈ UserVO.java

package com.ezen.spring.domain;

import lombok.*;

import java.util.List;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserVO {
    private String email;
    private String pwd;
    private String nickName;
    private String regDate;
    private String lastLogin;
    private List<AuthVO> authList;
}

 

◈ CustomUserService.java

package com.ezen.spring.security;

import com.ezen.spring.domain.UserVO;
import com.ezen.spring.repository.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Slf4j
public class CustomUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // userName 주고 UserVO 객체 리턴 (authList 같이)
        log.info(">>> username {}",username);
        UserVO userVO = userMapper.selectEmail(username);

        log.info(">>> userVO {}",userVO);
        userVO.setAuthList(userMapper.selectAuths(username));
        // UserDetails return
        return new AuthUser(userVO);
    }
}

 

◈ UserController.java

package com.ezen.spring.controller;

import com.ezen.spring.domain.UserVO;
import com.ezen.spring.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Slf4j
@RequestMapping("/user/*")
@RequiredArgsConstructor
@Controller
public class UserController {
    private final UserService usv;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/signup")
    public String signup(){
        return "/user/register";
    }

    @PostMapping("/signup")
    public String singup(UserVO userVO){
        userVO.setPwd(passwordEncoder.encode(userVO.getPwd()));
        int isOk = usv.insert(userVO);
        return "/index";
    }

    @GetMapping("/login")
    public String login(){
        return "/user/login";
    }

}

 

◈ UserService.java

package com.ezen.spring.service;

import com.ezen.spring.domain.UserVO;

public interface UserService {

    int insert(UserVO userVO);
}

 

◈ UserServiceImpl.java

package com.ezen.spring.service;

import com.ezen.spring.domain.UserVO;
import com.ezen.spring.repository.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService{
    private final UserMapper userMapper;

    @Transactional
    @Override
    public int insert(UserVO userVO) {
        int isOk = userMapper.insert(userVO);
        if(isOk > 0){
            userMapper.authInsert(userVO.getEmail());
        }
        return isOk;
    }
}

 

◈ UserMapper.java

package com.ezen.spring.repository;

import com.ezen.spring.domain.AuthVO;
import com.ezen.spring.domain.UserVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    int insert(UserVO userVO);

    void authInsert(String email);

    UserVO selectEmail(String username);

    List<AuthVO> selectAuths(String username);
}

 

◈ userMapper.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.ezen.spring.repository.UserMapper">
    <insert id="insert">
        insert into user(email, pwd, nick_name)
        value(#{email}, #{pwd}, #{nickName})
    </insert>
    <insert id="authInsert">
        insert into auth(email, auth)
        values(#{email}, 'ROLE_USER')
    </insert>
    <select id="selectEmail" resultType="com.ezen.spring.domain.UserVO">
        select * from user where email = #{email}
    </select>
    <select id="selectAuths" resultType="com.ezen.spring.domain.AuthVO">
        select * from auth where email = #{email}
    </select>
</mapper>

 

◈ register.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>User Sign Up Page</h1>
    <hr>
    <form action="/user/signup" method="post">
        <div class="mb-3">
            <label for="e" class="form-label">Email</label>
            <input type="text" class="form-control" name="email" id="e" placeholder="email...">
        </div>
        <div class="mb-3">
            <label for="p" class="form-label">Password</label>
            <input type="text" class="form-control" name="pwd" id="p" placeholder="password...">
        </div>
        <div class="mb-3">
            <label for="n" class="form-label">Nickname</label>
            <input type="text" class="form-control" name="nickName" id="n" placeholder="nickname...">
        </div>
<button type="submit" class="btn btn-primary" id="regBtn">register</button>
    </form>
</div>

 

login.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>User Login Page</h1>
    <form action="/user/login" method="post">
        <div class="mb-3">
            <label for="e" class="form-label">Email</label>
            <input type="text" class="form-control" name="email" id="e" placeholder="email...">
        </div>
        <div class="mb-3">
            <label for="p" class="form-label">Password</label>
            <input type="text" class="form-control" name="pwd" id="p" placeholder="password...">
        </div>
        <button type="submit" class="btn btn-primary" id="regBtn">register</button>
    </form>
</div>

 

 header.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<!-- th:fragment="이름" : 레이아웃에서 사용할 조각 -->
<div th:fragment="header" class="container">
  <nav class="navbar navbar-expand-lg bg-body-tertiary">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Boot</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <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="/board/register">BoardRegister</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/board/list">BoardList</a>
          </li><li class="nav-item">
            <a class="nav-link" href="/user/signup">SignUp</a>
          </li><li class="nav-item">
            <a class="nav-link" href="/user/login">Login</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
  <h5>Header Area</h5>
</div>

 

출력