spring boot 3.1.6, model mapper, gradle, jpa 예제

오세훈·2023년 12월 14일
0

spring-boot

목록 보기
6/7

properties

server.port=8085

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/team16
spring.datasource.username=root
spring.datasource.password=1234

logging.level.com.pro06=info
logging.level.org.hibernate.type.descriptor.sql=trace

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

modelMapper

package com.pro06.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // model mapper
    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setFieldMatchingEnabled(true)
                .setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }
}

DTO

package com.pro06.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;

// 강좌 테이블
// 여기에 나중에 선생님 관련 컬럼 하나 추가
// validation을 이용해 size, notnull 을 써도 되고 아니면 
// column을 이용해 length랑 null able 지정해줘도 됨

@Getter @Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class CourseDto extends BaseDto {

    private Integer no;         // 강좌 번호

    @Size(max = 20)
    @NotBlank
    private String id;          // 작성자(관리자)

    @Size(max = 10)
    @NotBlank
    private String level;      // 학년

    @Size(max = 100)
    @NotBlank
    private String title;       // 제목

    @Size(max = 2000)
    @NotNull
    private String content;     // 내용

    @NotNull
    private Integer cnt;        // 조회수

    @NotNull
    private Integer peo;        // 수강인원

    @NotBlank
    private Integer peo_max;    // 최대 수강인원
}

Entity

package com.pro06.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

// 강좌 테이블
// 여기에 나중에 선생님 관련 컬럼 하나 추가
// validation을 이용해 size, notnull 을 써도 되고 아니면 
// column을 이용해 length랑 null able 지정해줘도 됨

@Entity
@Getter
@Table(name="course")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor

// column에 defualt 값을 설정할 때에 밑의 두개를 같이 써줘야 한다.
@DynamicInsert
@DynamicUpdate
public class Course extends BaseEntity{

    @Id
    @Column(name = "no")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer no;         // 강좌 번호

    @Column(length = 20, nullable = false)
    private String id;          // 작성자(관리자)

    @Column(length = 10, nullable = false)
    private String level;      // 학년

    @Column(length = 100, nullable = false)
    private String title;       // 제목

    @Column(length = 2000, nullable = false)
    private String content;     // 내용

    @ColumnDefault("0")
    private Integer cnt;        // 조회수

    @ColumnDefault("0")
    private Integer peo;        // 수강인원

    @Column(nullable = false)
    private Integer peo_max;    // 최대 수강인원

    public void peoUp(){
        this.peo = this.peo + 1;
    }
		
		// 수정할 때 사용할 메서드
    public void change(String id, String level,
                       String title, String content) {
        this.id = id;
        this.level = level;
        this.title = title;
        this.content = content;
    }

}

Repository

package com.pro06.repository.course;

import com.pro06.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CourseRepository extends JpaRepository<Course, Integer> {
    
    // 수강생이 다 차지 않은 강좌만 불러오기
    @Query("select c from Course c where c.peo < c.peo_max")
    List<Course> courseList();
}

Service

package com.pro06.service.course;

import com.pro06.dto.CourseDto;
import com.pro06.entity.Course;
import com.pro06.repository.course.CourseRepository;
import jakarta.transaction.Transactional;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
@Transactional
public class CourseServiceImpl {
    @Autowired
    private CourseRepository courseRepository;

    @Autowired
    private ModelMapper modelMapper;
    
    // 강좌 등록
    public void courseInsert(CourseDto courseDto) {
        Course course = modelMapper.map(courseDto, Course.class);
        courseRepository.save(course);
    }

    // 강좌 수정
    public void courseUpdate(CourseDto courseDto) {
        Optional<Course> course = courseRepository.findById(courseDto.getNo());
        Course res = course.orElseThrow();
        res.change(courseDto.getId(), courseDto.getLevel(),
                courseDto.getTitle(), courseDto.getContent());
        courseRepository.save(res);
    }

    // 강좌 삭제
    public void courseDelete(Integer no) {
        courseRepository.deleteById(no);
    }
    
    // 어드민 강좌 목록 불러오기
    public List<CourseDto> admCourseList() {
        List<Course> lst = courseRepository.findAll();
        List<CourseDto> courseList = lst.stream().map(course ->
                modelMapper.map(course, CourseDto.class))
                .collect(Collectors.toList());
        return courseList;
    }

    // 강좌 목록 불러오기
    public List<CourseDto> courseList() {
        List<Course> lst = courseRepository.courseList();
        List<CourseDto> courseList = lst.stream().map(course ->
                modelMapper.map(course, CourseDto.class))
                .collect(Collectors.toList());
        return courseList;
    }
    
    // 강좌 상세 보기
    public CourseDto getCourse(Integer no) {
        Optional<Course> course = courseRepository.findById(no);
        CourseDto courseDto = modelMapper.map(course, CourseDto.class);
        return courseDto;
    }

    // 강좌 수강신청 수강생 +1
    public void setCoursePeo(Integer no) {
        Optional<Course> course = courseRepository.findById(no);
        Course res = course.orElseThrow();
        res.peoUp();
        courseRepository.save(res);
    }
}

Controller

package com.pro06.controller.course;

import com.pro06.dto.CourseDto;
import com.pro06.dto.LectureDto;
import com.pro06.dto.MyCourseDto;
import com.pro06.service.UserService;
import com.pro06.service.course.CourseServiceImpl;
import com.pro06.service.course.LectureServiceImpl;
import com.pro06.service.course.MyCourseServiceImpl;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;

// 해당 컨트롤러에서 사용한 log.error들은 싹다 alert로 나중에 바꿔야함

// 강좌 컨트롤
@Controller
@Log4j2
@RequestMapping("/course/*")
public class CourseController {

    @Autowired
    private CourseServiceImpl courseService;

    @Autowired
    private LectureServiceImpl lectureService;

    @Autowired
    private MyCourseServiceImpl myCourseService;

    @Autowired
    private UserService userService;
    
    // 강좌 목록
    @GetMapping("list")
    public String courseList(Principal principal, Model model) {
        List<CourseDto> courseList = courseService.courseList();

        List<String> checkList = new ArrayList<>();
        
        // 로그인한 사람이면 강좌마다 수강신청 했는지 안했는지 검사
        if(principal != null) {
            String id = principal.getName();
            for (CourseDto course: courseList) {
                Integer cnt = myCourseService.getMyCourseCnt(id, course.getNo());
                if(cnt > 0) {
                    checkList.add("y");
                } else {
                    checkList.add("n");
                }
            }
        }

        model.addAttribute("checkList", checkList);
        model.addAttribute("courseList", courseList);
        return "course/courseList";
    }
    
    // 강좌 상세
    @GetMapping("detail")
    public String courseDetail(@RequestParam("no") Integer no, Model model) throws IOException {

        // 강좌 상세
        CourseDto course = courseService.getCourse(no);
        model.addAttribute("course", course);
        
        // 강의 목록
        List<LectureDto> lectureList = lectureService.lectureCnoList(no);
        model.addAttribute("lectureList", lectureList);
        return "course/courseDetail";
    }

    // 강좌 수강신청
    @GetMapping("apply")
    public String courseApply(Principal principal, @RequestParam("cno") Integer cno) {

        if(principal == null) {
            log.error("로그인을 해야 수강신청을 할 수 있습니다.");
            return "redirect:/";
        }

        String id = principal.getName();

        Integer ck = myCourseService.getMyCourseCnt(id, cno);

        if(ck > 0) {
            log.error("이미 해당 강의를 수강하고 있습니다.");
            return "redirect:/";
        }
        
        // 수강 신청 하기전에 인원이 다 찼는지 안 찼는지 확인
        CourseDto course1 = courseService.getCourse(cno);

        if(course1.getPeo() >= course1.getPeo_max() ) {
            log.error("이미 수강생이 다 찼습니다.");
            return "redirect:/";
        }

        CourseDto course = new CourseDto();
        course.setNo(cno);

        MyCourseDto myCourse = new MyCourseDto();
        myCourse.setId(id);
        myCourse.setCourse(course);

        courseService.setCoursePeo(cno);
        myCourseService.myCourseInsert(myCourse);

        return "redirect:/"; // 인덱스 이동
    }
}

view, thymeleaf

detail

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>강좌 상세</title>
    <th:block th:replace="include/head :: head" ></th:block>
</head>
<body>
<th:block th:replace="include/header :: header" ></th:block>
<section class="table-list">
    <div class="container">
        <h2>강좌상세</h2>
        <ul>
            <li>강좌 이름 : [[${course.title}]]</li>
            <li>작성자 : [[${course.id}]]</li>
            <li>현재수강인원 : [[${course.peo}]]</li>
            <li>최대수강인원 : [[${course.peo_max}]]</li>
        </ul>

        <h3>강의목록</h3>
        <div class="search_from" style="text-align: center">
            <select name="select_filter" class="select_filter">
                <option value="1">제목</option>
            </select>
            <input type="text" name="search_filter" class="search_filter">
        </div>
        <table id="myTable" class="List-table">
            <thead>
            <tr>
                <th>No</th>
                <th>제목</th>
            </tr>
            </thead>
            <tbody>
            <th:block th:each="lecture, status : ${lectureList}">
                <tr>
                    <td th:text="${status.count}"></td>
                    <td th:text="${lecture.title}"></td>
                </tr>
            </th:block>
            </tbody>
        </table>
    </div>
</section>
<th:block th:replace="include/footer :: footer" ></th:block>
</body>
</html>

list

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>강좌목록</title>
    <th:block th:replace="include/head :: head" ></th:block>
</head>
<body>
<th:block th:replace="include/header :: header" ></th:block>
<section class="table-list">
    <div class="container">
        <h2>강좌목록</h2>
        <div class="search_from">
            <select name="select_filter" class="select_filter">
                <option value="1">제목</option>
                <option value="2">학년</option>
                <option value="4">등록일</option>
            </select>
            <input type="text" name="search_filter" class="search_filter">
        </div>
        <table id="myTable" class="List-table">
            <thead>
            <tr>
                <th>No</th>
                <th>제목</th>
                <th>학년</th>
                <th>수강현황</th>
                <th>등록일</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
            <th:block th:each="course, status : ${courseList}">
                <tr>
                    <td th:text="${status.count}"></td>
                    <td><a th:href="@{/course/detail(no=${course.no})}" th:text="${course.title}"></a></td>
                    <td th:text="${course.level}"></td>
                    <td th:text="${course.peo} + '/' + ${course.peo_max}"></td>
                    <td th:text="${#temporals.format(course.getCreatedTime(), 'yyyy-MM-dd hh:mm')}"></td>
                    <td>
                        <th:block sec:authorize="isAuthenticated()">
                            <th:block th:if="${checkList.get(status.index)} eq 'n'">
                                <a th:href="@{/course/apply(cno=${course.no})}">수강신청</a>
                            </th:block>
                        </th:block>
                    </td>
                </tr>
            </th:block>
            </tbody>
        </table>
    </div>
</section>
<th:block th:replace="include/footer :: footer" ></th:block>
</body>
</html>

insert

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>강좌등록</title>
    <th:block th:replace="/include/adminhead :: head"></th:block>
</head>
<body>
<div class="container-scroller">
    <div class="container-fluid page-body-wrapper">
        <th:block th:replace="/include/adminheader :: header"></th:block>
        <div class="main-panel">
            <div class="content-wrapper">
                <h1>관리자페이지</h1>
                <h2>강좌등록</h2>
                <div class="card p-3">
                    <form th:action="@{/admin/courseInsert}" method="post" class="text-center mb-3 was-validated">
                        <div class="form-group">
                            <label for="id" class="form-label">아이디</label>
                            <input type="text" name="id" id="id" class="form-control" th:value="${id}" readonly>
                        </div>

                        <div class="form-group">
                            <label for="level" class="form-label">학년</label>
                            <select name="level" id="level" class="custom-select">
                                <option value="초등" selected>초등</option>
                                <option value="중학">중학</option>
                                <option value="수능">수능</option>
                            </select>
                        </div>

                        <!-- form-control을 적용한 input 바로 밑에
                            valid-feedback, invalid-feedback를 적용 시켜주면 주어진 pattern에 따라 검사를 진행한다. -->
                        <div class="form-group">
                            <label for="title" class="form-label" >제목</label>
                            <input type="text" name="title" id="title" class="form-control"
                                   pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]*$" maxlength="100" required>
                            <div class="valid-feedback">
                                형식에 알맞습니다.
                            </div>
                            <div class="invalid-feedback">
                                한글, 숫자, 영어대소문자만 가능합니다.
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="content" class="form-label">내용</label>
                            <input type="text" name="content" id="content" class="form-control"
                                   pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]*$" maxlength="1000" required>
                            <div class="valid-feedback">
                                형식에 알맞습니다.
                            </div>
                            <div class="invalid-feedback">
                                한글, 숫자, 영어대소문자만 가능합니다.
                            </div>
                        </div>

                        <!-- 수강 최대 인원은 1~99 명까지 가능 -->
                        <div class="form-group">
                            <label for="peo_max" class="form-label">최대 수강인원</label>
                            <input type="text" name="peo_max" id="peo_max" class="form-control" value="10"
                                   pattern="[1-9]|[1-9][0-9]" maxlength="2" required>
                            <div class="valid-feedback">
                                형식에 알맞습니다.
                            </div>
                            <div class="invalid-feedback">
                                1 ~ 99명 까지만 가능합니다.
                            </div>
                        </div>

                        <div class="form-group">
                            <input type="submit" class="btn btn-primary" value="등록">
                            <input type="reset" class="btn btn-danger" value="초기화">
                        </div>
                    </form>
                </div>
            </div>

            <footer class="footer">
                <div class="d-sm-flex justify-content-center justify-content-sm-between">
                    <span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2021.  Premium <a href="https://www.bootstrapdash.com/" target="_blank">Bootstrap admin template</a> from BootstrapDash. All rights reserved.</span>
                    <span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made with <i class="ti-heart text-danger ml-1"></i></span>
                </div>
            </footer>
        </div>
    </div>
</div>
</body>
</html>

update

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>강좌수정</title>
    <th:block th:replace="/include/adminhead :: head"></th:block>
</head>
<body>
<div class="container-scroller">
    <div class="container-fluid page-body-wrapper">
        <th:block th:replace="/include/adminheader :: header"></th:block>
        <div class="main-panel">
            <div class="content-wrapper">
                <h1>관리자페이지</h1>
                <h2>강좌수정</h2>
                <div class="card p-3">
                    <form th:action="@{/admin/courseUpdate}" method="post" class="text-center mb-3 was-validated">
                        <div class="form-group">
                            <input type="hidden" name="no" id="no" th:value="${dto.no}">
                        </div>

                        <div class="form-group">
                            <label for="id">아이디</label>
                            <input type="text" name="id" id="id" class="form-control" th:value="${dto.id}" readonly>
                        </div>

                        <div class="form-group">
                            <label for="level">학년</label>
                            <select name="level" id="level" class="custom-select">
                                <th:block th:if="${dto.level eq '초등'}">
                                    <option value="초등" selected>초등</option>
                                </th:block>
                                <th:block th:if="${dto.level ne '초등'}">
                                    <option value="초등">초등</option>
                                </th:block>

                                <th:block th:if="${dto.level eq '중학'}">
                                    <option value="중학" selected>중학</option>
                                </th:block>
                                <th:block th:if="${dto.level ne '중학'}">
                                    <option value="중학">중학</option>
                                </th:block>

                                <th:block th:if="${dto.level eq '수능'}">
                                    <option value="수능" selected>수능</option>
                                </th:block>
                                <th:block th:if="${dto.level ne '수능'}">
                                    <option value="수능">수능</option>
                                </th:block>
                            </select>
                        </div>

                        <div class="form-group">
                            <label for="title">제목</label>
                            <input type="text" name="title" id="title" class="form-control" pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]*$"
                                   maxlength="100" th:value="${dto.title}" required>
                            <div class="valid-feedback">
                                형식에 알맞습니다.
                            </div>
                            <div class="invalid-feedback">
                                한글, 숫자, 영어대소문자만 가능합니다.
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="content">내용</label>
                            <input type="text" name="content" id="content" class="form-control" pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]*$"
                                   maxlength="1000" th:value="${dto.content}" required>
                            <div class="valid-feedback">
                                형식에 알맞습니다.
                            </div>
                            <div class="invalid-feedback">
                                한글, 숫자, 영어대소문자만 가능합니다.
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="peo_max">최대 수강인원</label>
                            <input type="text" name="peo_max" id="peo_max" value="10" class="form-control" maxlength="2" readonly>
                        </div>

                        <div>
                            <input type="submit" class="btn btn-primary" value="등록">
                            <input type="reset" class="btn btn-danger" value="초기화">
                        </div>
                    </form>
                </div>
            </div>

            <footer class="footer">
                <div class="d-sm-flex justify-content-center justify-content-sm-between">
                    <span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2021.  Premium <a href="https://www.bootstrapdash.com/" target="_blank">Bootstrap admin template</a> from BootstrapDash. All rights reserved.</span>
                    <span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made with <i class="ti-heart text-danger ml-1"></i></span>
                </div>
            </footer>
        </div>
    </div>
</div>
</body>
</html>
profile
자바 풀 스택 주니어 개발자

0개의 댓글