Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'io.spring.dependency-management' version '1.1.7'
//id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}

group = 'com'
Expand All @@ -26,6 +27,13 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'

// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down Expand Up @@ -55,6 +63,36 @@ dependencies {
implementation 'at.favre.lib:bcrypt:0.10.2'
}

/*
* queryDSL 설정 추가
*/
// querydsl에서 사용할 경로 설정
//def querydslDir = "$buildDir/generated/querydsl"
//
//
//// JPA 사용 여부와 사용할 경로를 설정
////querydsl {
//// jpa = true
//// querydslSourcesDir = querydslDir
////}
//
//// build 시 사용할 sourceSet 추가
//sourceSets {
// main.java.srcDir querydslDir
//}
//
//tasks.withType(JavaCompile).configureEach {
// options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
//}
//
//// querydsl 이 compileClassPath 를 상속하도록 설정
//configurations {
// compileOnly {
// extendsFrom annotationProcessor
// }
// querydsl.extendsFrom compileClasspath
//}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.taskflow.domain.dashboard.controller;

import com.taskflow.domain.dashboard.dto.TaskStatisticsResponse;
import com.taskflow.domain.dashboard.dto.TodayTaskSummaryResponse;
import com.taskflow.domain.dashboard.service.DashboardService;
import com.taskflow.global.common.ApiResponse;
import com.taskflow.global.response.success.StatisticSuccess;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DashboardController {

private final DashboardService dashboardService;


public DashboardController(DashboardService dashboardService) {
this.dashboardService = dashboardService;
}

//통계값 조회 API
@GetMapping("/dashboard/statistics")
public ResponseEntity<ApiResponse<TaskStatisticsResponse>> getStatistics() {
return ResponseEntity.status(HttpStatus.OK).
body(ApiResponse.success(StatisticSuccess.STATISTIC_SUCCESS.getMessage(),dashboardService.getTaskStatistics()));
}

//오늘의 태스크 요약
@GetMapping("/dashboard/my-tasks/today")
public ResponseEntity<ApiResponse<TodayTaskSummaryResponse>> getMyTodayTasks(@RequestParam Long assigneeId) {
return ResponseEntity.status(HttpStatus.OK).
// body(ApiResponse.success(TaskSuccess.TASK_READ_SUCCESS.getMessage(), response));
body(ApiResponse.success("태스크요약 성공", dashboardService.getTodayTaskSummary(assigneeId)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.taskflow.domain.dashboard.dto;

import com.taskflow.domain.task.entity.Task;

import java.time.LocalDateTime;

public record TaskSimpleResponse(Long id, String title, String priority, LocalDateTime dueDate) {

public static TaskSimpleResponse from(Task task) {
return new TaskSimpleResponse(
task.getId(),
task.getTitle(),
task.getPriority().name(),
task.getDueDate()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.taskflow.domain.dashboard.dto;

public record TaskStatisticsResponse(
long totalCount,
long todoCount,
long inProgressCount,
long doneCount,
double completionRate,
long overdueCount
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.taskflow.domain.dashboard.dto;

import java.util.List;

public record TodayTaskSummaryResponse(
List<TaskSimpleResponse> todoTasks,
List<TaskSimpleResponse> inProgressTasks
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.taskflow.domain.dashboard.repository;


import com.taskflow.domain.dashboard.dto.TaskStatisticsResponse;
import com.taskflow.domain.task.entity.Task;
import com.taskflow.domain.task.enums.TaskStatus;

import java.util.List;

public interface TaskRepositoryCustom {
TaskStatisticsResponse fetchTaskStatistics();
List<Task> findTodayTasksByStatusAndAssignee(TaskStatus status, Long assigneeId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.taskflow.domain.dashboard.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.taskflow.domain.dashboard.dto.TaskStatisticsResponse;

import com.taskflow.domain.task.entity.QTask;
import com.taskflow.domain.task.entity.Task;
import com.taskflow.domain.task.enums.TaskStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@RequiredArgsConstructor
@Repository
public class TaskRepositoryImpl implements TaskRepositoryCustom {
private final JPAQueryFactory queryFactory;


@Override
public TaskStatisticsResponse fetchTaskStatistics() {
QTask task = QTask.task;
LocalDateTime now = LocalDateTime.now();

long total = queryFactory.select(task.count())
.from(task)
.where(task.isDeleted.isFalse())
.fetchOne();

long todo = queryFactory.select(task.count())
.from(task)
.where(task.isDeleted.isFalse()
.and(task.status.eq(TaskStatus.valueOf("TODO"))))
.fetchOne();

long inProgress = queryFactory.select(task.count())
.from(task)
.where(task.isDeleted.isFalse()
.and(task.status.eq(TaskStatus.valueOf("IN_PROGRESS"))))
.fetchOne();

long done = queryFactory.select(task.count())
.from(task)
.where(task.isDeleted.isFalse()
.and(task.status.eq(TaskStatus.valueOf("DONE"))))
.fetchOne();

long overdue = queryFactory.select(task.count())
.from(task)
.where(task.isDeleted.isFalse()
.and(task.status.in(TaskStatus.TODO, TaskStatus.IN_PROGRESS))
.and(task.dueDate.before(now)))
.fetchOne();

double completionRate = total == 0 ? 0.0 : Math.round((done * 10000.0 / total)) / 100.0;

return new TaskStatisticsResponse(total, todo, inProgress, done, completionRate, overdue);
}

@Override
public List<Task> findTodayTasksByStatusAndAssignee(TaskStatus status, Long assigneeId) {
QTask task = QTask.task;

return queryFactory
.selectFrom(task)
.where(
task.assignee.id.eq(assigneeId),
task.status.eq(status),
task.isDeleted.isFalse(),
task.createdAt.between(LocalDate.now().atStartOfDay(), LocalDateTime.now())
)
.orderBy(task.priority.desc()) // HIGH > MEDIUM > LOW
.fetch();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.taskflow.domain.dashboard.service;

import com.taskflow.domain.dashboard.dto.TaskSimpleResponse;
import com.taskflow.domain.dashboard.dto.TaskStatisticsResponse;
import com.taskflow.domain.dashboard.dto.TodayTaskSummaryResponse;
import com.taskflow.domain.task.entity.Task;
import com.taskflow.domain.task.enums.TaskStatus;
import com.taskflow.domain.task.repository.TaskRepository;
import com.taskflow.global.exception.dashboard.StastisticException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DashboardService {

private final TaskRepository taskRepository;

public DashboardService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

public TaskStatisticsResponse getTaskStatistics() {

TaskStatisticsResponse dto = taskRepository.fetchTaskStatistics();

if (dto.totalCount() == 0) {
throw new StastisticException("태스크 통계를 불러오는 중 오류 발생", HttpStatus.INTERNAL_SERVER_ERROR);
}

return dto;
}

public TodayTaskSummaryResponse getTodayTaskSummary(Long assigneeId) {
List<Task> todos = taskRepository.findTodayTasksByStatusAndAssignee(TaskStatus.TODO, assigneeId);
List<Task> inProgress = taskRepository.findTodayTasksByStatusAndAssignee(TaskStatus.IN_PROGRESS, assigneeId);

return new TodayTaskSummaryResponse(
todos.stream().map(TaskSimpleResponse::from).toList(),
inProgress.stream().map(TaskSimpleResponse::from).toList()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class Member extends BaseEntity {
private UserRole userRole;

@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT false")
@Builder.Default
private Boolean is_deleted = false;

public void softDelete() {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/taskflow/domain/task/entity/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,8 @@ public void delete() {
this.isDeleted = true;
this.deletedAt = LocalDateTime.now();
}

}

}

Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.taskflow.domain.task.enums;



public enum TaskPriority {
LOW, MEDIUM, HIGH
}

/**
* 일정 우선순위를 정의하는 열거형
* 업무 중요도에 따라 LOW, MEDIUM, HIGH 세 가지 값으로 분류
*/
public enum TaskPriority {
LOW, MEDIUM, HIGH
}

4 changes: 4 additions & 0 deletions src/main/java/com/taskflow/domain/task/enums/TaskStatus.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.taskflow.domain.task.enums;


// 일정 상태 정의

/**
* 일정 상태를 정의하는 열거형
* 각 일정의 진행 상태를 나타내는 값으로 구성
*/

public enum TaskStatus {
TODO, IN_PROGRESS, DONE
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.taskflow.domain.task.repository;


import com.taskflow.domain.dashboard.repository.TaskRepositoryCustom;

import com.taskflow.domain.member.entity.Member;
import com.taskflow.domain.task.entity.Task;
import com.taskflow.domain.task.enums.TaskStatus;
Expand All @@ -13,8 +16,12 @@
/**
* Task 엔티티에 대한 데이터베이스 접근을 담당하는 JPA 레포지토리 인터페이스
*/

public interface TaskRepository extends JpaRepository<Task, Long>, TaskRepositoryCustom {

public interface TaskRepository extends JpaRepository<Task, Long> {


/**
* 삭제되지 않은 전체 일정 조회
*/
Expand Down Expand Up @@ -59,4 +66,8 @@ public interface TaskRepository extends JpaRepository<Task, Long> {
* 상태, 담당자, 제목 키워드 기준으로 삭제되지 않은 일정 조회
*/
List<Task> findAllByStatusAndAssigneeAndTitleContainingAndIsDeletedFalse(TaskStatus status, Member assignee, String title);

}

}

20 changes: 20 additions & 0 deletions src/main/java/com/taskflow/global/config/QuerydslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.taskflow.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

//영속성을 트랜잭션 범위에서 생성하여 관리하는 어노테이션
@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Loading