링크 참고.
th:href="@{/projects/new}"
=> 타임리프의 문법에서는 th:href, th:src 주소에 "@{/}" 를 붙인다.
jsp에서의 <=request.contextPath> 와 같은 의미.
localhost:8080까지의 주소를 의미함. 주소가 바뀌더라도 자동으로 바뀔 수 있도록 설정한 것.
<a class="btn btn-primary" th:href="@{/employees/new}" type="button">새 직원 추가</a>
<a class="btn btn-primary" th:href="@{/projects/new}" type="button">새 프로젝트 추가</a>
project와 employee 테이블을 1:N 관계로 만듦.
아래의 두 클래스에 추가.
- Employee -
외래키 project_id 추가.
// N:1 관계. 직원은 여러명에 프로젝트는 하나.
// cascade는 수정, 삭제시 연결동작, fetch는 검색 시 연관된 참조데이터를 한번에 가져오느냐(EAGER) 필요한데이터만 가져오느냐(LAZY)
@ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
fetch = FetchType.LAZY)
@JoinColumn(name = "project_id") // 추가되는 테이블명 (외래키 열)
private Project project;
- Project -
하나의 프로젝트에 여러명의 직원 연결.
@OneToMany(mappedBy = "project") // 프로젝트 테이블에 맵핑
private List<Employee> employees;
Project테이블과 Employee테이블의 관계를 만들어줌.
Employee테이블의 외래키 project_id로 Project테이블을 참조함.
- Fetch Type
EAGER : 연관된(참조) 데이터까지 한번에 다 가져옴
LAZY : 우선 필요한 데이터만 가져오고 나중에 필요하면 참조 데이터 가져옴
- cascade
CascadeType.ALL: 모든 Cascade를 적용
CascadeType.PERSIST: 엔티티를 저장save할때, 연관된 엔티티도 함께 저장
CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X
CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침
참고
두 클래스 모두 각 추가된 필드에 대한 Getter/Setter메서드 만들기.
http://localhost:8080/h2.console 에서 테이블추가 확인. (JDBC URL은 jdbc:h2:mem:testdb 입력)
- new-project.html -
직원테이블의 전체를 출력하고 그 중에서 고르는 직원선택 태그를 추가한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layouts :: Head"></head>
<body>
<nav th:replace="layouts :: Navbar"></nav>
<div class="container">
<form action="/projects/save" method="post" th:object="${project}">
<div class="row my-2">
<input class="form-control" type="text" th:field="*{name}" placeholder="프로젝트 이름" />
</div>
<div class="row my-2">
<select class="form-select" th:field="*{stage}">
<option th:value="시작전">시작전</option>
<option th:value="진행중">진행중</option>
<option th:value="완료">완료</option>
</select>
</div>
<div class="row my-2">
<textarea class="form-control" th:field="*{description}" placeholder="프로젝트 설명"></textarea>
</div>
<!-- 직원 선택 태그 추가 -->
<div class="row my-2">
<p>직원들을 선택</p>
<!-- 선택태그의 매핑은 employee임. ${project.employees} = *{employees} -->
<select class="form-select" th:field="*{employees}" multiple>
<!-- 직원 전체를 옵션으로 출력 -->
<option th:each=" : ${employee : empList}" th:value="${employee.employeeId}" th:text="${employee.firstName}"></option>
</select>
</div>
<button class="btn btn-primary" type="submit">새 프로젝트</button>
</form>
</div>
<footer th:replace="layouts :: footer"></footer>
</body>
</html>
empList를 만들어줘야함.
- ProjectController -
필드변수 선언 후 /new 에 내용추가.
@Autowired
private EmployeeRepository employeeRepository;
...
@GetMapping("/new")
public String newProjectForm(Model model) {
Project p = new Project();
model.addAttribute("project", p);
List<Employee> empList = employeeRepository.findAll();
model.addAttribute("empList", empList);
return "projects/new-project";
}
@PostMapping("/save") // ids는 employee의 id들을 의미함
public String createProject(Project project, @RequestParam("employees") List<Long> ids) {
projectRepository.save(project); // project객체를 DB의 테이블에 저장
return "redirect:/projects/"; // post-redirect-get 패턴(/new > /save > /new)
}
@PostMapping("/save") // ids는 employee의 id들을 의미함
public String createProject(Project project, @RequestParam("employees") List<Long> ids) {
projectRepository.save(project); // project객체를 DB의 테이블에 저장
// 프로젝트 생성 html에서 올라온 직원ID들을 입력하여 직원리스트를 가져와 직원테이블의 각각의 직원에 프로젝트를 입력
Iterable<Employee> selectEmployees = employeeRepository.findAllById(ids);
for (Employee emp : selectEmployees) {
emp.setProject(project); // 각각의 직원객체에 프로젝트를 입력하고
employeeRepository.save(emp); // DB에 다시 저장
}
return "redirect:/projects/"; // post-redirect-get 패턴(/new > /save > /new)
}
앞서만든 1:N관계를 N:N으로 수정.
N:N은 바로 관계를 맺을 수 없으며, 제 3의 테이블을 만들어 참조함.
아래의 두 클래스에서 이전에 선언한 필드변수를 수정함.
- Employee -
테이블을 만들어 join시킴. get/set메서드 새로만들기.
// N:N 관계에서는 테이블을 만들어 만든 테이블에 id를 넣고 다른 테이블의 id도 입력한다
@ManyToMany(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
fetch = FetchType.LAZY)
@JoinTable(name = "project_employee", joinColumns = @JoinColumn(name = "employee_id"),
inverseJoinColumns = @JoinColumn(name="project_id"))
private List<Project> projects;
- Project -
@ManyToMany(cascade = { CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST,
CascadeType.REFRESH }, fetch = FetchType.LAZY)
@JoinTable(name = "project_employee", joinColumns = @JoinColumn(name = "project_id"), inverseJoinColumns = @JoinColumn(name = "employee_id"))
private List<Employee> employees;
- ProjectController -
@PostMapping("/save")
public String createProject(Project project) {
projectRepository.save(project); // project객체를 DB의 테이블에 저장
return "redirect:/projects/"; // post-redirect-get 패턴(/new > /save > /new)
}
직원 3명 입력 후 1번, 3번 직원만 새 프로젝트에 투입했을때의 DB.
PRG (Post-Redirect-Get) 패턴을 사용하지 않으면 새로고침을 했을 때 반복적으로 post가 될 수 있다.
이를 방지하기위해 Post와 Get 사이에 Redirect를 해줌으로써 안정성을 높힌다.
Get의 경우 Post와 달리 여러번 반복되어도 무관하다.
프로젝트 실행 시 자동으로 메인 메서드에서 DB에 데이터를 입력하도록작성.
- PmaApplication -
@SpringBootApplication
public class PmaApplication {
@Autowired
private EmployeeRepository empRepo;
@Autowired
private ProjectRepository proRepo;
public static void main(String[] args) {
SpringApplication.run(PmaApplication.class, args);
}
// 프로젝트 실행 시 자동으로 DB에 데이터를 입력하는 코드
@Bean
CommandLineRunner runner() {
return args-> {
Employee emp1 = new Employee("길동", "홍", "hong@gmail.com");
Employee emp2 = new Employee("라니", "고", "go@gmail.com");
Employee emp3 = new Employee("스티븐", "킹", "king@gmail.com");
Employee emp4 = new Employee("날두", "호", "ho@gmail.com");
Employee emp5 = new Employee("펭수", "김", "kim@gmail.com");
Employee emp6 = new Employee("피터", "팬", "pen@gmail.com");
Employee emp7 = new Employee("순신", "이", "lee@gmail.com");
Employee emp8 = new Employee("감찬", "강", "kang@gmail.com");
Employee emp9 = new Employee("유신", "김", "yousin@gmail.com");
Project pro1 = new Project("대형 프로젝트", "시작전", "할일이 많음");
Project pro2 = new Project("새 직원 인사", "완료", "필요한 부서의 새 직원 고용");
Project pro3 = new Project("오피스 리모델링", "진행중", "오래된 오피스 환경을 새것처럼 리모델링");
Project pro4 = new Project("회사 보안 강화", "진행중", "출 입문 인증 지문센서 추가");
// project 에 직원들을 추가하고, employee 객체에 프로젝트들을 추가한다.
// project에 새 메서드 작성
pro1.addEmployee(emp1);
pro1.addEmployee(emp2);
pro2.addEmployee(emp3);
pro3.addEmployee(emp1);
pro4.addEmployee(emp1);
pro4.addEmployee(emp3);
// 다른 방법
// emp1.setProjects(Arrays.asList(pro1, pro3, pro4));
// emp2.setProjects(Arrays.asList(pro1));
// emp3.setProjects(Arrays.asList(pro2, pro4));
// DB에 저장
empRepo.save(emp1);
empRepo.save(emp2);
empRepo.save(emp3);
empRepo.save(emp4);
empRepo.save(emp5);
empRepo.save(emp6);
empRepo.save(emp7);
empRepo.save(emp8);
empRepo.save(emp9);
proRepo.save(pro1);
proRepo.save(pro2);
proRepo.save(pro3);
proRepo.save(pro4);
};
}
}
- Project -
// 프로젝트 객체에서 직원을 더하는 메서드. employees가 null이면 새 ArrayList를 만들어 매개변수로 받은 객체(직원)를 리스트에 추가한다.
public void addEmployee(Employee emp) {
if (employees == null) {
employees = new ArrayList<>();
}
employees.add(emp);
}
이후 Project클래스와 Emplyoee클래스에서 @ManyToMany(cascade = {...}, ...)
중 CascadeType.PERSIST 제거.(안해주면 에러발생함)
실행 시 데이터가 입력된 것을 확인가능.
각 테이블마다 id가 따로 적용되야하는데 전체 레코그를 통틀어 id가 순서대로 생성됨.
이를 수정하기 위해 Project, Employee 클래스의 id를 수정.
=> 이제는 DB에서 id컬럼을 만들어 순서값이 따로 매겨진다.
- Employee -
@Id // 기본키를 명시
@GeneratedValue(strategy = GenerationType.IDENTITY) // Id를 자동생성
private Long employeeId;
- Project -
@Id // Id가 기본키임을 알림
@GeneratedValue(strategy = GenerationType.IDENTITY) // id를 자동으로 생성할것을 알림
private Long projectId; // 프로젝트 아이디 (CamelCase => DB project_id)
h2 DB에서 확인할때 사용할 sql문
SELECT * FROM EMPLOYEE;
SELECT * FROM PROJECT;
SELECT * FROM PROJECT_EMPLOYEE;
앞서 main메서드로 입력하기 위해 PmaApplication클래스에서 작성한 코드는 모두 삭제한다.
위치에 data.sql 파일을 넣기
aplication.properties
맨 아래행에 추가
# data.sql 파일이 있을 시 실행한다
spring.jpa.defer-datasource-initialization=true
이후 실행 시 정상적으로 데이터가 들어간 것을 확인가능함.
정적파일은 static폴더에 추가한다.
- layouts.html -
css 링크태그를 bootstrap아래 추가.
<link rel="stylesheet" th:href="@{/css/style.css}" />
- home.html -
프로젝트 진행상황부분을 수정. 7:3으로 맞춘 후 2만큼은 남김.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layouts :: Head"></head>
<body>
<nav th:replace="layouts :: Navbar"></nav>
<div class="container">
<h3>프로젝트 진행상황</h3>
<div class="row">
<div class="col-md-7">
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>프로젝트 이름</th>
<th>현재 진행상태</th>
</tr>
</thead>
<tbody>
<!-- 타임리프의 반복문 -->
<tr th:each="project : ${projectList}">
<td th:text="${project.name}"></td>
<td th:text="${project.stage}"></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-3 ms-5">
<canvas id="myChart"></canvas>
</div>
</div>
</div>
<br />
<div class="container">
<h3>직원 현황</h3>
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>성</th>
<th>이름</th>
<th>이메일</th>
</tr>
</thead>
<tbody>
<tr th:each="employee : ${employeeList}">
<td th:text="${employee.lastName}"></td>
<td th:text="${employee.firstName}"></td>
<td th:text="${employee.email}"></td>
</tr>
</tbody>
</table>
</div>
<footer th:replace="layouts :: footer"></footer>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script th:src="@{/js/chart.js}"></script>
</body>
</html>
- chart.js -
const data = {
labels: ['시작전', '진행중', '완료'],
datasets: [
{
label: 'My First Dataset',
data: [3, 2, 1],
backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'],
hoverOffset: 4,
},
],
};
const config = {
type: 'pie',
data: data,
};
const myChart = new Chart(document.getElementById('myChart'), config);
반영이 완료된 형태.
- home.html -
...
<!-- 타임리프의 반복문 -->
<tr th:each="project : ${projectList}">
<td th:text="${project.name}"></td>
<td class="stage" th:text="${project.stage}"></td>
</tr>
...
- chart.js -
const stages = document.querySelectorAll('.stage');
let s1 = 0;
let s2 = 0;
let s3 = 0;
// 각 stage의 텍스트컨텐츠(내용)이 셋 중 어느것에 해당하느냐에 따라 값을 +1 해줌. ===은 값과 타입까지 모두 체크함.
stages.forEach((stage) => {
if (stage.textContent === '시작전') s1++;
else if (stage.textContent === '진행중') s2++;
else if (stage.textContent === '완료') s3++;
});
const data = {
labels: ['시작전', '진행중', '완료'],
datasets: [
{
label: 'My First Dataset',
data: [s1, s2, s3],
backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'],
hoverOffset: 4,
},
],
};
...아래는 동일하므로 생략...
진행상태가 반영이 된 모습.