diff --git a/src/main/java/com/dnd/namuiwiki/common/exception/ApplicationErrorType.java b/src/main/java/com/dnd/namuiwiki/common/exception/ApplicationErrorType.java index bbc53462..35bd523a 100644 --- a/src/main/java/com/dnd/namuiwiki/common/exception/ApplicationErrorType.java +++ b/src/main/java/com/dnd/namuiwiki/common/exception/ApplicationErrorType.java @@ -61,8 +61,8 @@ public enum ApplicationErrorType { */ NOT_FOUND_SURVEY(HttpStatus.NOT_FOUND, "존재하지 않는 설문입니다."), QUESTION_ANSWER_COUNT_NOT_EQUAL(HttpStatus.INTERNAL_SERVER_ERROR, "문항과 답변의 개수가 일치하지 않습니다."), - INVALID_BORROWING_LIMIT(HttpStatus.BAD_REQUEST, "빌릴 수 있는 돈은 0 이상 10억 이하입니다."), + INVALID_SURVEY_OWNER(HttpStatus.CONFLICT, "설문 소유자가 아닙니다."), /** * Filter Error Type @@ -71,10 +71,10 @@ public enum ApplicationErrorType { @Getter - private HttpStatus httpStatus; + private final HttpStatus httpStatus; @Getter - private String message; + private final String message; public int getStatusCode() { return httpStatus.value(); diff --git a/src/main/java/com/dnd/namuiwiki/common/exception/AsyncExceptionHandler.java b/src/main/java/com/dnd/namuiwiki/common/exception/AsyncExceptionHandler.java new file mode 100644 index 00000000..b77d3607 --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/common/exception/AsyncExceptionHandler.java @@ -0,0 +1,20 @@ +package com.dnd.namuiwiki.common.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +import java.lang.reflect.Method; + +@Slf4j +public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + log.error("AsyncException message={}, declaringClass={}, methodName={}", ex.getMessage(), method.getDeclaringClass(), method.getName(), ex); + + for (Object param : params) { + log.error("Parameter value - {}, declaringClass={}, methodName={}", param, method.getDeclaringClass(), method.getName()); + } + } + +} diff --git a/src/main/java/com/dnd/namuiwiki/config/AsyncConfiguration.java b/src/main/java/com/dnd/namuiwiki/config/AsyncConfiguration.java new file mode 100644 index 00000000..878ed030 --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/config/AsyncConfiguration.java @@ -0,0 +1,38 @@ +package com.dnd.namuiwiki.config; + +import com.dnd.namuiwiki.common.exception.AsyncExceptionHandler; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@EnableAsync +@Configuration +public class AsyncConfiguration implements AsyncConfigurer { + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("Async-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return asyncExceptionHandler(); + } + + @Bean + public AsyncExceptionHandler asyncExceptionHandler() { + return new AsyncExceptionHandler(); + } + +} diff --git a/src/main/java/com/dnd/namuiwiki/config/CacheConfiguration.java b/src/main/java/com/dnd/namuiwiki/config/CacheConfiguration.java new file mode 100644 index 00000000..d19f1915 --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/config/CacheConfiguration.java @@ -0,0 +1,28 @@ +package com.dnd.namuiwiki.config; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@EnableCaching +@Configuration +public class CacheConfiguration { + + @Bean + CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + List caches = new ArrayList<>(); + caches.add(new ConcurrentMapCache("questions")); + caches.add(new ConcurrentMapCache("question")); + cacheManager.setCaches(caches); + return cacheManager; + } + +} diff --git a/src/main/java/com/dnd/namuiwiki/config/EventConfiguration.java b/src/main/java/com/dnd/namuiwiki/config/EventConfiguration.java new file mode 100644 index 00000000..ffe40db8 --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/config/EventConfiguration.java @@ -0,0 +1,19 @@ +package com.dnd.namuiwiki.config; + +import com.dnd.namuiwiki.domain.statistic.StatisticsService; +import com.dnd.namuiwiki.domain.survey.SurveyEventHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class EventConfiguration { + private final StatisticsService statisticsService; + + @Bean + public SurveyEventHandler surveyEventHandler() { + return new SurveyEventHandler(statisticsService); + } + +} diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepository.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepository.java new file mode 100644 index 00000000..0c048c6d --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepository.java @@ -0,0 +1,12 @@ +package com.dnd.namuiwiki.domain.dashboard; + +import com.dnd.namuiwiki.domain.survey.model.entity.Answer; +import com.dnd.namuiwiki.domain.survey.type.Period; +import com.dnd.namuiwiki.domain.survey.type.Relation; +import com.dnd.namuiwiki.domain.user.entity.User; + +import java.util.List; + +public interface DashboardCustomRepository { + void updateDashboard(User owner, Period period, Relation relation, List answers); +} diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepositoryImpl.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepositoryImpl.java new file mode 100644 index 00000000..1042b856 --- /dev/null +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardCustomRepositoryImpl.java @@ -0,0 +1,43 @@ +package com.dnd.namuiwiki.domain.dashboard; + +import com.dnd.namuiwiki.domain.dashboard.model.entity.Dashboard; +import com.dnd.namuiwiki.domain.survey.model.entity.Answer; +import com.dnd.namuiwiki.domain.survey.type.Period; +import com.dnd.namuiwiki.domain.survey.type.Relation; +import com.dnd.namuiwiki.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.List; + +@RequiredArgsConstructor +public class DashboardCustomRepositoryImpl implements DashboardCustomRepository { + private final MongoTemplate mongoTemplate; + + @Override + public void updateDashboard(User owner, Period period, Relation relation, List answers) { + Query query = Query.query(Criteria.where("user").is(owner) + .and("period").is(period) + .and("relation").is(relation)); + + Update update = new Update(); + + FindAndModifyOptions options = new FindAndModifyOptions().returnNew(true); + + for (Answer answer : answers) { + update.inc(String.format("statistics.statistics.%s.totalCount", answer.getQuestion().getId())); + + if (answer.getType().isOption()) { + update.inc(String.format("statistics.statistics.%s.legends.%s.count", + answer.getQuestion().getId(), answer.getAnswer().toString())); + } + } + + mongoTemplate.findAndModify(query, update, options, Dashboard.class); + } + +} diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardRepository.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardRepository.java index 0bcd641a..a16183d3 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardRepository.java +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/DashboardRepository.java @@ -8,6 +8,7 @@ import java.util.Optional; -public interface DashboardRepository extends MongoRepository { +public interface DashboardRepository extends MongoRepository, DashboardCustomRepository { Optional findByUserAndPeriodAndRelation(User user, Period period, Relation relation); + boolean existsByUserAndPeriodAndRelation(User user, Period period, Relation relation); } diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/BestWorthDashboardComponent.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/BestWorthDashboardComponent.java index 918651b3..e79f1674 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/BestWorthDashboardComponent.java +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/BestWorthDashboardComponent.java @@ -36,6 +36,7 @@ public void calculate(Statistics statistics) { for (Legend legend : legends) { if (totalCount == 0) { rank.add(new RatioDto(legend.getText(), 0)); + continue; } int percentage = (int) (legend.getCount() * 100 / totalCount); optionPercentage += percentage; diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/HappyDashboardComponent.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/HappyDashboardComponent.java index 2663c9c0..bd025e08 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/HappyDashboardComponent.java +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/HappyDashboardComponent.java @@ -36,6 +36,7 @@ public void calculate(Statistics statistics) { for (Legend legend : legends) { if (totalCount == 0) { rank.add(new RatioDto(legend.getText(), 0)); + continue; } int percentage = (int) (legend.getCount() * 100 / totalCount); optionPercentage += percentage; diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/SadDashboardComponent.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/SadDashboardComponent.java index 18af7fd7..c296d80b 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/SadDashboardComponent.java +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/SadDashboardComponent.java @@ -36,6 +36,7 @@ public void calculate(Statistics statistics) { for (Legend legend : legends) { if (totalCount == 0) { rank.add(new RatioDto(legend.getText(), 0)); + continue; } int percentage = (int) (legend.getCount() * 100 / totalCount); optionPercentage += percentage; diff --git a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/entity/Dashboard.java b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/entity/Dashboard.java index 8215334b..d2d92bc9 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/entity/Dashboard.java +++ b/src/main/java/com/dnd/namuiwiki/domain/dashboard/model/entity/Dashboard.java @@ -9,6 +9,7 @@ import lombok.Builder; import lombok.Getter; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.DocumentReference; @@ -17,6 +18,7 @@ @Getter @Builder @Document("dashboards") +@CompoundIndex(def = "{ 'user': 1, 'period': 1, 'relation': 1 }", unique = true) public class Dashboard extends BaseTimeEntity { @Id diff --git a/src/main/java/com/dnd/namuiwiki/domain/question/QuestionRepository.java b/src/main/java/com/dnd/namuiwiki/domain/question/QuestionRepository.java index b51a68ce..6bef69dd 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/question/QuestionRepository.java +++ b/src/main/java/com/dnd/namuiwiki/domain/question/QuestionRepository.java @@ -1,7 +1,21 @@ package com.dnd.namuiwiki.domain.question; import com.dnd.namuiwiki.domain.question.entity.Question; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.lang.NonNull; + +import java.util.List; +import java.util.Optional; public interface QuestionRepository extends MongoRepository { + + @NonNull + @Cacheable("questions") + List findAll(); + + @NonNull + @Cacheable("question") + Optional findById(@NonNull String id); + } diff --git a/src/main/java/com/dnd/namuiwiki/domain/question/entity/Question.java b/src/main/java/com/dnd/namuiwiki/domain/question/entity/Question.java index 6449ed9b..e2a2371d 100644 --- a/src/main/java/com/dnd/namuiwiki/domain/question/entity/Question.java +++ b/src/main/java/com/dnd/namuiwiki/domain/question/entity/Question.java @@ -28,7 +28,7 @@ public class Question extends BaseTimeEntity { private Long surveyOrder; private boolean reasonRequired; - @DocumentReference + @DocumentReference(collection = "options") private Map options; public Optional