한 일

  • 타임리프 주소작성
  • 테이블을 1 : N 관계로 만들기
  • 테이블을 N : N 관계로 만들기
  • PRG 패턴
  • 테이블에 기본데이터 추가하기
  • css와 js추가하기
  • chart 추가하기

참고. properties 파일의 한글이 깨질때

링크 참고.


타임리프 주소작성

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>


테이블을 1 : N 관계로 만들기

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


테이블을 N : N 관계로 만들기

앞서만든 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 패턴

PRG (Post-Redirect-Get) 패턴

PRG (Post-Redirect-Get) 패턴을 사용하지 않으면 새로고침을 했을 때 반복적으로 post가 될 수 있다.
이를 방지하기위해 Post와 Get 사이에 Redirect를 해줌으로써 안정성을 높힌다.
Get의 경우 Post와 달리 여러번 반복되어도 무관하다.


테이블에 기본데이터 추가하기

1. java main메서드에서 실행과 동시에 추가하기

프로젝트 실행 시 자동으로 메인 메서드에서 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;

2. sql파일로 입력하기

앞서 main메서드로 입력하기 위해 PmaApplication클래스에서 작성한 코드는 모두 삭제한다.


위치에 data.sql 파일을 넣기

aplication.properties
맨 아래행에 추가

# data.sql 파일이 있을 시 실행한다
spring.jpa.defer-datasource-initialization=true

이후 실행 시 정상적으로 데이터가 들어간 것을 확인가능함.


css와 js추가하기


정적파일은 static폴더에 추가한다.

- layouts.html -
css 링크태그를 bootstrap아래 추가.

<link rel="stylesheet" th:href="@{/css/style.css}" />

chart 추가하기

- 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,
    },
  ],
};
	...아래는 동일하므로 생략...


진행상태가 반영이 된 모습.


디자인 패턴 참고링크

profile
천 리 길도 가나다라부터

0개의 댓글

Powered by GraphCDN, the GraphQL CDN