[구현 기능 설명]
"Task 테이블" 옆에 체크 표시를 생성하고, 체크 표시를 통해 '완료 / 비완료 과업'을 구분한다.
151, 149, 147, 141, 139 번의 박스를 클릭할 경우, 체크된 박스 이미지로 변경된다.
[Task.class - Entity]
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Task extends BaseEntity{
/**
* task_id(PK)
* title
* content
* isChecked
* member_id(FK)
*/
@Id @GeneratedValue
@Column(name = "task_id")
private Long id;
private String title; //일정명
private String content; //설명
private Boolean isChecked = false; //체크(과업 완료) 여부
//Task 의 주인
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id") //FK Column
private Member member;
/*중략...*/
//체크 여부 변경 (isChecked)
public void updateIsChecked() {
//isChecked == false -> ture
if (!this.isChecked) {
this.isChecked = true;
}
//isChecked == true -> false
else {
this.isChecked = false;
}
}
}
[TaskRepository.class]
public interface TaskRepository extends JpaRepository<Task, Long> {
Page<Task> findByMember(Member member, Pageable pageable);
Page<Task> findByMemberAndIsChecked(Member member, Boolean isChecked, Pageable pageable);
List<Task> findByMember(Member member); //전체 Task 목록
List<Task> findByMemberAndIsChecked(Member member, Boolean isChecked); //전체 Task 체크 목록
}
[TaskService.class]
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TaskService {
private final TaskRepository taskRepository;
/*중략 ...*/
/**
* Task 체크여부 수정
*/
@Transactional
public void taskCheck(Long taskId) {
//1. Task 찾기
Task task = taskRepository.findById(taskId)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 Task 입니다."));
task.updateIsChecked();
}
/**
* finishedTaskList
* - "isChecked == true" 인 TaskList
* - "isChecked == false" 인 TaskList
*/
public Page<Task> getIsCheckedTaskList(Member member, Boolean isChecked ,Pageable pageable) {
return taskRepository.findByMemberAndIsChecked(member, isChecked, pageable);
}
//전체 과업 수
public Integer totalTaskNum(Member member) {
return taskRepository.findByMember(member).size();
}
//완료 과업 수
public Integer checkTaskNum(Member member, Boolean isChecked) {
return taskRepository.findByMemberAndIsChecked(member, isChecked).size();
}
}
[TaskController.class]
@RequestMapping("/tasks")
@Controller
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
private final MemberService memberService;
// 현재 로그인된 회원의 task list
@GetMapping("")
public String home(@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
Model model) {
//task create form 넘기기
model.addAttribute("taskForm", new TaskRequestDto());
//현재 로그인된 username
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("signedMember", authentication.getName());
//현재 로그인된 member
Member member = memberService.findByUsername(authentication.getName());
Page<Task> tasks = taskService.taskList(member, pageable);
Page<TaskResponseDto> list = tasks.map(TaskResponseDto::new);
//task dto list
model.addAttribute("tasks", list);
model.addAttribute("previous", pageable.previousOrFirst().getPageNumber()); //이전 페이지 정보
model.addAttribute("next", pageable.next().getPageNumber()); //다음 페이지 정보
model.addAttribute("hasPrevious", list.hasPrevious()); //이전 페이지 존재 여부
model.addAttribute("hasNext", list.hasNext()); //다음 페이지 존재 여부
//페이지 번호
/** 페이지 블록 계산
* currentPage = 5
* User : 5 , Spring : 4
* startPage = 1
* endPage = 5
* <= 1 2 3 4 5 =>
*/
int currentPage = pageable.getPageNumber() + 1; //현재 페이지 정보(User side)
model.addAttribute("current", currentPage);
int blockSize = 5;
int startPage = ((currentPage - 1) / blockSize) * blockSize + 1; //블럭 시작 페이지
int endPage = Math.min(startPage + blockSize - 1, list.getTotalPages()); //블럭 마지막 페이지
model.addAttribute("startPage", startPage);
model.addAttribute("endPage", endPage);
// 달성 Task / 전체 Task
model.addAttribute("checkTaskNum", taskService.checkTaskNum(member, true));
model.addAttribute("totalTaskNum", taskService.totalTaskNum(member));
return "tasks/task-list";
}
//task 생성
@PostMapping("")
public String createTask(@ModelAttribute("taskForm") @Validated(TaskRequestDto.Create.class) TaskRequestDto dto,
BindingResult result, Model model) {
//유효성 검사 오류 시, 에러 처리 로직
if (result.hasErrors()) {
//에러 메시지 반환
List<String> errorMessage = result.getAllErrors().stream()
.map(objectError -> objectError.getDefaultMessage())
.collect(Collectors.toList());
model.addAttribute("errorMessage", errorMessage);
return "tasks/task-list";
}
//작성자 설정
Member member = memberService.findByUsername(memberService.getCurrentUsername());
dto.setMember(member);
taskService.createTask(dto);
return "redirect:/tasks";
}
}
[Thyemleaf]
<!--task-list.html-->
<body>
<!--header-->
<header class="header" th:replace="~{fragments/header :: header}"></header>
<div class="task-container">
<!--task 작성란-->
<form class="task-form" th:action="@{/tasks}" th:object="${taskForm}" method="post">
<div>
<h2>Task 작성</h2>
<!--task title-->
<div class="task-box">
<input id="task-title-input" th:field="*{title}" type="text" name="task-title" required="">
<label for="task-title-input">Task Title</label>
</div>
<!--task content-->
<div class="task-box">
<textarea id="task-content-textarea" th:field="*{content}" name="task-content" ></textarea>
<label for="task-content-textarea">Task Content</label>
</div>
<button type="submit" style="background: green;">SAVE TASK</button>
</div>
</form>
<!--task 목록-->
<div class="task-table">
<h2>과제 목록</h2>
<p th:text="'전체 과업:' + ${totalTaskNum}">전체 과업</p>
<p th:text="'달성 과업:' + ${checkTaskNum}">달성 과업</p>
<div class="progress-container">
<div class="progress-bar" th:style="'width: ' + (${checkTaskNum} / ${totalTaskNum} * 100) + '%'">
<span th:text="${checkTaskNum} / ${totalTaskNum} * 100 + '%'">75%</span>
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}">id</td>
<td>
<a th:text="${task.title}" th:href="@{|/tasks/${task.id}|}">title</a>
</td>
<td>
<form th:action="@{'/tasks/' + ${task.id} + '/checking'}" method="post" style="display: inline;">
<input type="hidden" th:name="id" th:value="${task.id}">
<button type="submit" style="background: none; border: none; padding: 0;">
<img th:src="${task.isChecked} ? '/img/check-icon.png' : '/img/empty-circle.png'" style="width:24px; height:24px; cursor: pointer;" />
</button>
</form>
</td>
</tr>
</tbody>
</table>
<div class="pagination-box">
<ul class="pagination">
<li class="page-item">
<a th:if="${hasPrevious}" th:href="@{/tasks(page=${previous})}"
role="button" class="page-link">이전</a>
<a th:if="${!hasPrevious}" th:href="@{/tasks(page=${previous})}"
role="button" class="page-link disabled">이전</a>
</li>
<li class="page-item" th:each="pageNum : ${#numbers.sequence(startPage, endPage)}"
th:classappend="${pageNum == current} ? active : ''">
<a th:href="@{/tasks(page=${pageNum - 1})}" th:text="${pageNum}"
role="button" class="page-link">페이지 번호</a>
</li>
<li class="page-item">
<a th:if="${hasNext}" th:href="@{/tasks(page=${next})}"
role="button" class="page-link">다음</a>
<a th:if="${!hasNext}" th:href="@{/tasks(page=${next})}"
role="button" class="page-link disabled">다음</a>
</li>
</ul>
</div>
</div>
</div>
<!--footer-->
<footer class="footer" th:replace="~{fragments/footer :: footer}"></footer>
</body>
이 부분이 중요하다.
<form th:action="@{'/tasks/' + ${task.id} + '/checking'}" method="post" style="display: inline;">
<input type="hidden" th:name="id" th:value="${task.id}">
<button type="submit" style="background: none; border: none; padding: 0;">
<img th:src="${task.isChecked} ? '/img/check-icon.png' : '/img/empty-circle.png'" style="width:24px; height:24px; cursor: pointer;" />
</button>
</form>
삼항 연산자를 이용하여, "task.isChecked" 가 'true' 이면, 'check-icon.png' 이고,
'false' 이면, 'empty-circle.png' 가 표시된다.
<form> 을 통해, isChecked 를 변경하는 post 요청을 보내어 isCheck를 변경한다.
[완성 화면]
체크 기능이 정상적으로 작동하는 것을 확인할 수 있다.