Skip to content

Commit 83001c4

Browse files
Merge pull request #52 from TaskFlow-CLAP/CLAP-47
CLAP-47 작업 데이터 스케쥴러로 엘라스틱서치에 저장
2 parents 2dece58 + a40eb15 commit 83001c4

25 files changed

+614
-5
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ repositories {
2626
dependencies {
2727
implementation 'org.springframework.boot:spring-boot-starter-web'
2828

29+
//ElasticSearch
30+
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
31+
2932
// validator
3033
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.1'
3134

src/main/java/clap/server/TaskflowApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class TaskflowApplication {
810

911
public static void main(String[] args) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package clap.server.adapter.inbound.web.statistics;
2+
3+
import clap.server.application.port.inbound.statistics.FindCategoryTaskRequestUsecase;
4+
import clap.server.application.port.inbound.statistics.FindPeriodTaskProcessUsecase;
5+
import clap.server.application.port.inbound.statistics.FindPeriodTaskRequestUsecase;
6+
import clap.server.application.port.inbound.statistics.FindSubCategoryTaskRequestUsecase;
7+
import clap.server.common.annotation.architecture.WebAdapter;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
13+
import java.util.Map;
14+
15+
@WebAdapter
16+
@RequiredArgsConstructor
17+
public class FindStatisticsController {
18+
private final FindPeriodTaskRequestUsecase findPeriodTaskRequestUsecase;
19+
private final FindPeriodTaskProcessUsecase findPeriodTaskProcessUsecase;
20+
private final FindCategoryTaskRequestUsecase findCategoryTaskRequestUsecase;
21+
private final FindSubCategoryTaskRequestUsecase findSubCategoryTaskRequestUsecase;
22+
// private final ManagerTaskProcessUsecase managerTaskProcessUsecase;
23+
24+
@GetMapping(value = "/task/statistics/task-requests-by-period")
25+
public ResponseEntity<Map<String, Long>> aggregatePeriodTaskRequest(@RequestParam String period) {
26+
return ResponseEntity.ok(findPeriodTaskRequestUsecase.aggregatePeriodTaskRequest(period));
27+
}
28+
29+
@GetMapping("/task/statistics/task-processed-by-period")
30+
public ResponseEntity<Map<String, Long>> aggregatePeriodTaskProcess(@RequestParam String period) {
31+
return ResponseEntity.ok(findPeriodTaskProcessUsecase.aggregatePeriodTaskProcess(period));
32+
}
33+
@GetMapping("/task/statistics/task-requests-by-category")
34+
public ResponseEntity<Map<String, Long>> aggregateCategoryTaskRequest(@RequestParam String period) {
35+
return ResponseEntity.ok(findCategoryTaskRequestUsecase.aggregateCategoryTaskRequest(period));
36+
}
37+
38+
@GetMapping("/task/statistics/task-requests-by-subcategory")
39+
public ResponseEntity<Map<String, Long>> aggregateSubCategoryTaskRequest(@RequestParam String period, @RequestParam String mainCategory) {
40+
return ResponseEntity.ok(findSubCategoryTaskRequestUsecase.aggregateSubCategoryTaskRequest(period, mainCategory));
41+
}
42+
// @GetMapping("/task/statistics/tasks-processed-by-manager")
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package clap.server.adapter.outbound.infrastructure.elastic;
2+
3+
import co.elastic.clients.elasticsearch._types.aggregations.CalendarInterval;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
7+
@Getter
8+
@RequiredArgsConstructor
9+
public enum PeriodConfig {
10+
DAY(1, CalendarInterval.Hour, 11, 19),
11+
WEEK(14, CalendarInterval.Day, 0, 10);
12+
13+
private final long daysToSubtract;
14+
private final CalendarInterval calendarInterval;
15+
private final int substringStart;
16+
private final int substringEnd;
17+
18+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package clap.server.adapter.outbound.infrastructure.elastic;
2+
3+
import clap.server.adapter.outbound.infrastructure.elastic.document.TaskDocument;
4+
import clap.server.adapter.outbound.infrastructure.elastic.repository.TaskElasticRepository;
5+
import clap.server.application.port.outbound.task.TaskDocumentPort;
6+
import clap.server.common.annotation.architecture.InfrastructureAdapter;
7+
import clap.server.domain.model.task.Task;
8+
import co.elastic.clients.elasticsearch._types.aggregations.AggregationBuilders;
9+
import co.elastic.clients.elasticsearch._types.aggregations.MultiBucketBase;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregations;
12+
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
13+
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
14+
15+
import java.time.LocalDate;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.TreeMap;
19+
import java.util.stream.Collectors;
20+
21+
@InfrastructureAdapter
22+
@RequiredArgsConstructor
23+
public class TaskDocumentAdapter implements TaskDocumentPort {
24+
private final TaskElasticRepository taskElasticRepository;
25+
private final ElasticsearchOperations elasticsearchOperations;
26+
27+
@Override
28+
public void saveStatistics(List<Task> statistics) {
29+
taskElasticRepository.saveAll(statistics.stream().map(TaskDocument::new).toList());
30+
}
31+
32+
@Override
33+
public Map<String, Long> findPeriodTaskRequestByPeriod(String period) {
34+
PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase());
35+
36+
NativeQuery query = buildPeriodTaskRequestQuery(periodConfig);
37+
return getPeriodTaskResults(executeQuery(query), periodConfig);
38+
}
39+
40+
@Override
41+
public Map<String, Long> findPeriodTaskProcessByPeriod(String period) {
42+
PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase());
43+
44+
NativeQuery query = buildPeriodTaskProcessQuery(periodConfig);
45+
return getPeriodTaskResults(executeQuery(query), periodConfig);
46+
}
47+
48+
@Override
49+
public Map<String, Long> findCategoryTaskRequestByPeriod(String period) {
50+
PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase());
51+
52+
NativeQuery query = buildCategoryTaskRequestQuery(periodConfig);
53+
return getCategoryTaskResults(executeQuery(query));
54+
}
55+
56+
@Override
57+
public Map<String, Long> findSubCategoryTaskRequestByPeriod(String period, String mainCategory) {
58+
PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase());
59+
60+
NativeQuery query = buildSubCategoryTaskRequestQuery(periodConfig, mainCategory);
61+
return getCategoryTaskResults(executeQuery(query));
62+
}
63+
64+
private NativeQuery buildPeriodTaskRequestQuery(PeriodConfig config) {
65+
return NativeQuery.builder()
66+
.withQuery(q -> q
67+
.range(r -> r
68+
.date(d -> d
69+
.field("created_at")
70+
.gte(String.valueOf(LocalDate.now().minusDays(config.getDaysToSubtract()))))))
71+
.withAggregation("period_task", AggregationBuilders.dateHistogram()
72+
.field("created_at")
73+
.calendarInterval(config.getCalendarInterval())
74+
.build()._toAggregation())
75+
.withMaxResults(0)
76+
.build();
77+
}
78+
79+
private NativeQuery buildPeriodTaskProcessQuery(PeriodConfig config) {
80+
NativeQuery rangeQuery = NativeQuery.builder()
81+
.withQuery(q -> q
82+
.range(r -> r
83+
.date(d -> d
84+
.field("created_at")
85+
.gte(String.valueOf(LocalDate.now().minusDays(config.getDaysToSubtract())))))).build();
86+
NativeQuery statusQuery = NativeQuery.builder()
87+
.withQuery(q -> q
88+
.term(v -> v
89+
.field("status")
90+
.value("completed"))).build();
91+
92+
return NativeQuery.builder()
93+
.withQuery(q -> q
94+
.bool(b -> b
95+
.must(rangeQuery.getQuery(), statusQuery.getQuery()))
96+
)
97+
.withAggregation("period_task", AggregationBuilders.dateHistogram()
98+
.field("created_at")
99+
.calendarInterval(config.getCalendarInterval())
100+
.build()._toAggregation())
101+
.withMaxResults(0)
102+
.build();
103+
}
104+
105+
private NativeQuery buildCategoryTaskRequestQuery(PeriodConfig config) {
106+
return NativeQuery.builder()
107+
.withQuery(q -> q
108+
.range(r -> r
109+
.date(d -> d
110+
.field("created_at")
111+
.gte(String.valueOf(LocalDate.now().minusDays(config.getDaysToSubtract()))))))
112+
.withAggregation("category_task", AggregationBuilders.terms()
113+
.field("main_category")
114+
.build()._toAggregation())
115+
.withMaxResults(0)
116+
.build();
117+
}
118+
119+
private NativeQuery buildSubCategoryTaskRequestQuery(PeriodConfig config, String mainCategory) {
120+
NativeQuery rangeQuery = NativeQuery.builder()
121+
.withQuery(q -> q
122+
.range(r -> r
123+
.date(d -> d
124+
.field("created_at")
125+
.gte(String.valueOf(LocalDate.now().minusDays(config.getDaysToSubtract())))))).build();
126+
NativeQuery categoryQuery = NativeQuery.builder()
127+
.withQuery(q -> q
128+
.term(v -> v
129+
.field("main_category")
130+
.value(mainCategory))).build();
131+
132+
return NativeQuery.builder()
133+
.withQuery(q -> q
134+
.bool(b -> b
135+
.must(rangeQuery.getQuery(), categoryQuery.getQuery()))
136+
)
137+
.withAggregation("category_task", AggregationBuilders.terms()
138+
.field("sub_category")
139+
.build()._toAggregation())
140+
.withMaxResults(0)
141+
.build();
142+
}
143+
144+
private ElasticsearchAggregations executeQuery(NativeQuery query) {
145+
return (ElasticsearchAggregations) elasticsearchOperations
146+
.search(query, TaskDocument.class)
147+
.getAggregations();
148+
}
149+
150+
private Map<String, Long> getPeriodTaskResults(ElasticsearchAggregations aggregations, PeriodConfig config) {
151+
return new TreeMap<>(
152+
aggregations.get("period_task")
153+
.aggregation()
154+
.getAggregate()
155+
.dateHistogram()
156+
.buckets()
157+
.array()
158+
.stream()
159+
.collect(Collectors.toMap(
160+
bucket -> bucket.keyAsString().substring(
161+
config.getSubstringStart(),
162+
config.getSubstringEnd()
163+
),
164+
MultiBucketBase::docCount
165+
))
166+
);
167+
}
168+
169+
private Map<String, Long> getCategoryTaskResults(ElasticsearchAggregations aggregations) {
170+
return new TreeMap<>(
171+
aggregations.get("category_task")
172+
.aggregation()
173+
.getAggregate()
174+
.sterms()
175+
.buckets()
176+
.array()
177+
.stream()
178+
.collect(Collectors.toMap(
179+
bucket -> bucket.key().stringValue(),
180+
MultiBucketBase::docCount
181+
))
182+
);
183+
}
184+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package clap.server.adapter.outbound.infrastructure.elastic.document;
2+
3+
import clap.server.domain.model.task.Task;
4+
import jakarta.persistence.Id;
5+
import lombok.Getter;
6+
import org.springframework.data.elasticsearch.annotations.Document;
7+
import org.springframework.data.elasticsearch.annotations.Field;
8+
import org.springframework.data.elasticsearch.annotations.Mapping;
9+
10+
import java.time.LocalDate;
11+
12+
@Document(indexName = "task")
13+
@Mapping(mappingPath = "elastic/task-mapping.json")
14+
@Getter
15+
public class TaskDocument {
16+
@Id
17+
private Long id;
18+
@Field(name="task_code")
19+
private String taskCode;
20+
@Field(name="main_category")
21+
private String mainCategory;
22+
@Field(name="sub_category")
23+
private String subCategory;
24+
@Field(name="status")
25+
private String status;
26+
@Field(name="processor")
27+
private String processor;
28+
@Field(name="created_at")
29+
private LocalDate createdAt;
30+
31+
public TaskDocument(Task taskEntity) {
32+
this.id = taskEntity.getTaskId();
33+
this.taskCode = taskEntity.getTaskCode();
34+
this.mainCategory = taskEntity.getCategory().getMainCategory().getName();
35+
this.subCategory = taskEntity.getCategory().getName();
36+
this.status = taskEntity.getTaskStatus().name().toLowerCase();
37+
this.processor = taskEntity.getProcessor().getMemberInfo().getNickname();
38+
this.createdAt = taskEntity.getCreatedAt().toLocalDate();
39+
}
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package clap.server.adapter.outbound.infrastructure.elastic.repository;
2+
3+
import clap.server.adapter.outbound.infrastructure.elastic.document.TaskDocument;
4+
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
5+
6+
public interface TaskElasticRepository extends ElasticsearchRepository<TaskDocument, Long> {
7+
}

src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus;
55
import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper;
66
import clap.server.adapter.outbound.persistense.repository.member.MemberRepository;
7-
import clap.server.application.port.outbound.member.LoadMemberPort;
87
import clap.server.application.port.outbound.member.CommandMemberPort;
8+
import clap.server.application.port.outbound.member.LoadMemberPort;
99
import clap.server.common.annotation.architecture.PersistenceAdapter;
1010
import clap.server.domain.model.member.Member;
1111
import lombok.RequiredArgsConstructor;

src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import clap.server.application.port.outbound.task.CommandTaskPort;
1010
import clap.server.application.port.outbound.task.LoadTaskPort;
1111
import clap.server.common.annotation.architecture.PersistenceAdapter;
12-
1312
import clap.server.domain.model.task.Task;
1413
import lombok.RequiredArgsConstructor;
1514
import org.springframework.data.domain.Page;
1615
import org.springframework.data.domain.Pageable;
1716

17+
import java.time.LocalDateTime;
18+
import java.util.List;
1819
import java.util.Optional;
1920

2021

@@ -42,4 +43,10 @@ public Page<FindTaskListResponse> findAllByRequesterId(Long requesterId, Pageabl
4243
.map(taskPersistenceMapper::toDomain);
4344
return taskList.map(TaskMapper::toFindTaskListResponse);
4445
}
46+
47+
@Override
48+
public List<Task> findYesterdayTaskByDate(LocalDateTime now) {
49+
return taskRepository.findYesterdayTaskByUpdatedAtIsBetween(now.minusDays(1), now)
50+
.stream().map(taskPersistenceMapper::toDomain).toList();
51+
}
4552
}

src/main/java/clap/server/adapter/outbound/persistense/entity/member/MemberEntity.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,13 @@ public class MemberEntity extends BaseTimeEntity {
3232
private boolean isReviewer;
3333

3434
@ManyToOne(fetch = FetchType.LAZY)
35-
@JoinColumn(name = "department_id", nullable = false)
35+
@JoinColumn(name = "department_id")
3636
private DepartmentEntity department;
3737

3838
@Column(nullable = false)
3939
@Enumerated(EnumType.STRING)
4040
private MemberRole role;
4141

42-
@Column(nullable = false)
4342
private String departmentRole;
4443

4544
@Enumerated(EnumType.STRING)

0 commit comments

Comments
 (0)