카테고리 없음

[TaskApp] Task 체크 기능 구현 (완료 / 비완료 과업 변경하기)

MoveForward 2024. 6. 25. 13:05

[구현 기능 설명]

"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를 변경한다.

 

 

[완성 화면]

체크 기능이 정상적으로 작동하는 것을 확인할 수 있다.