diff --git a/src/main/java/com/example/nexus/app/datacenter/service/DataCenterService.java b/src/main/java/com/example/nexus/app/datacenter/service/DataCenterService.java index 9d2cd42f..fb81f553 100644 --- a/src/main/java/com/example/nexus/app/datacenter/service/DataCenterService.java +++ b/src/main/java/com/example/nexus/app/datacenter/service/DataCenterService.java @@ -3,8 +3,9 @@ import com.example.nexus.app.datacenter.controller.dto.response.datacenter.*; import com.example.nexus.app.datacenter.controller.dto.response.datacenter.InsightsResponse.FeedbackItemDto; import com.example.nexus.app.datacenter.controller.dto.response.datacenter.QualityFeedbackResponse.ProblemLocationDto; -import com.example.nexus.app.datacenter.domain.ParticipantFeedback; -import com.example.nexus.app.datacenter.repository.ParticipantFeedbackRepository; +import com.example.nexus.app.feedback.domain.Feedback; +import com.example.nexus.app.feedback.domain.BugType; +import com.example.nexus.app.feedback.repository.FeedbackRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,7 +25,7 @@ @Slf4j public class DataCenterService { - private final ParticipantFeedbackRepository feedbackRepository; + private final FeedbackRepository feedbackRepository; private final KeywordAnalyzer keywordAnalyzer; /** @@ -39,12 +40,12 @@ public DataCenterResponse getDataCenterData(Long postId, int days) { LocalDateTime startDate = endDate.minusDays(days); // 기간 내 피드백 조회 - List feedbacks = feedbackRepository.findByPostIdAndDateRange( + List feedbacks = feedbackRepository.findByPostIdAndDateRange( postId, startDate, endDate); // 전주 데이터 (비교용) LocalDateTime lastWeekStart = startDate.minusDays(7); - List lastWeekFeedbacks = feedbackRepository.findByPostIdAndDateRange( + List lastWeekFeedbacks = feedbackRepository.findByPostIdAndDateRange( postId, lastWeekStart, startDate); return DataCenterResponse.builder() @@ -60,8 +61,8 @@ public DataCenterResponse getDataCenterData(Long postId, int days) { * 요약 카드 데이터 생성 */ private DataCenterSummaryResponse buildSummary(Long postId, - List feedbacks, - List lastWeekFeedbacks, + List feedbacks, + List lastWeekFeedbacks, int days) { long totalParticipants = feedbacks.size(); long lastWeekParticipants = lastWeekFeedbacks.size(); @@ -73,13 +74,13 @@ private DataCenterSummaryResponse buildSummary(Long postId, // 평균 만족도 double avgSatisfaction = feedbacks.stream() .filter(f -> f.getOverallSatisfaction() != null) - .mapToInt(ParticipantFeedback::getOverallSatisfaction) + .mapToInt(Feedback::getOverallSatisfaction) .average() .orElse(0.0); double lastWeekAvgSatisfaction = lastWeekFeedbacks.stream() .filter(f -> f.getOverallSatisfaction() != null) - .mapToInt(ParticipantFeedback::getOverallSatisfaction) + .mapToInt(Feedback::getOverallSatisfaction) .average() .orElse(0.0); @@ -131,19 +132,19 @@ private DataCenterSummaryResponse buildSummary(Long postId, /** * 전반 평가 데이터 생성 */ - private OverallEvaluationResponse buildOverallEvaluation(List feedbacks) { + private OverallEvaluationResponse buildOverallEvaluation(List feedbacks) { // 평균 점수 계산 - double avgSatisfaction = calculateAverage(feedbacks, ParticipantFeedback::getOverallSatisfaction); - double avgRecommendation = calculateAverage(feedbacks, ParticipantFeedback::getRecommendationIntent); - double avgReuse = calculateAverage(feedbacks, ParticipantFeedback::getReuseIntent); + double avgSatisfaction = calculateAverage(feedbacks, Feedback::getOverallSatisfaction); + double avgRecommendation = calculateAverage(feedbacks, Feedback::getRecommendationIntent); + double avgReuse = calculateAverage(feedbacks, Feedback::getReuseIntent); // 분포 계산 Map satisfactionDist = calculateDistribution(feedbacks, - ParticipantFeedback::getOverallSatisfaction); + Feedback::getOverallSatisfaction); Map recommendationDist = calculateDistribution(feedbacks, - ParticipantFeedback::getRecommendationIntent); + Feedback::getRecommendationIntent); Map reuseDist = calculateDistribution(feedbacks, - ParticipantFeedback::getReuseIntent); + Feedback::getReuseIntent); return OverallEvaluationResponse.builder() .averageSatisfaction(Math.round(avgSatisfaction * 10) / 10.0) @@ -158,10 +159,11 @@ private OverallEvaluationResponse buildOverallEvaluation(List feedbacks) { - // 불편 요소 Top 3 + private QualityFeedbackResponse buildQualityFeedback(List feedbacks) { + // 불편 요소 Top 3 (Feedback.mostInconvenient를 문자열로 변환) Map inconvenientElements = feedbacks.stream() - .flatMap(f -> f.getInconvenientElements().stream()) + .filter(f -> f.getMostInconvenient() != null) + .map(f -> f.getMostInconvenient().getDescription()) .collect(Collectors.groupingBy(e -> e, Collectors.counting())) .entrySet().stream() .sorted(Map.Entry.comparingByValue().reversed()) @@ -182,11 +184,12 @@ private QualityFeedbackResponse buildQualityFeedback(List f // 만족도 점수 분포 (비율) Map satisfactionScoreDist = calculatePercentageDistribution(feedbacks, - ParticipantFeedback::getOverallSatisfaction); + Feedback::getOverallSatisfaction); - // 문제 유형 비중 + // 문제 유형 비중 (Feedback.bugTypes를 문자열로 변환) Map problemTypeProportions = feedbacks.stream() - .flatMap(f -> f.getProblemTypes().stream()) + .flatMap(f -> f.getBugTypes().stream()) + .map(BugType::getDescription) .collect(Collectors.groupingBy(t -> t, Collectors.counting())) .entrySet().stream() .collect(Collectors.toMap( @@ -198,9 +201,9 @@ private QualityFeedbackResponse buildQualityFeedback(List f // 주요 문제 발생 위치 (Top 5) List topProblemLocations = feedbacks.stream() - .filter(f -> f.getProblemLocation() != null && !f.getProblemLocation().isEmpty()) + .filter(f -> f.getBugLocation() != null && !f.getBugLocation().isEmpty()) .collect(Collectors.groupingBy( - ParticipantFeedback::getProblemLocation, + Feedback::getBugLocation, Collectors.counting() )) .entrySet().stream() @@ -209,8 +212,9 @@ private QualityFeedbackResponse buildQualityFeedback(List f .map(e -> { // 해당 위치의 주요 문제 유형 찾기 String mainProblemType = feedbacks.stream() - .filter(f -> e.getKey().equals(f.getProblemLocation())) - .flatMap(f -> f.getProblemTypes().stream()) + .filter(f -> e.getKey().equals(f.getBugLocation())) + .flatMap(f -> f.getBugTypes().stream()) + .map(BugType::getDescription) .findFirst() .orElse("오류"); @@ -243,12 +247,13 @@ private QualityFeedbackResponse buildQualityFeedback(List f /** * 기능별 사용성 평가 데이터 생성 */ - private UsabilityEvaluationResponse buildUsabilityEvaluation(List feedbacks) { - double functionalityScore = calculateAverage(feedbacks, ParticipantFeedback::getFunctionalityScore); - double comprehensibilityScore = calculateAverage(feedbacks, ParticipantFeedback::getComprehensibilityScore); - double loadingSpeedScore = calculateAverage(feedbacks, ParticipantFeedback::getLoadingSpeedScore); - double responseTimingScore = calculateAverage(feedbacks, ParticipantFeedback::getResponseTimingScore); - double stabilityScore = calculateAverage(feedbacks, ParticipantFeedback::getStabilityScore); + private UsabilityEvaluationResponse buildUsabilityEvaluation(List feedbacks) { + double functionalityScore = calculateAverage(feedbacks, Feedback::getFunctionalityScore); + double comprehensibilityScore = calculateAverage(feedbacks, Feedback::getComprehensibilityScore); + double loadingSpeedScore = calculateAverage(feedbacks, Feedback::getSpeedScore); + double responseTimingScore = calculateAverage(feedbacks, Feedback::getResponseTimingScore); + // Feedback에는 stabilityScore가 없으므로 0.0으로 설정 + double stabilityScore = 0.0; return UsabilityEvaluationResponse.builder() .functionalityScore(Math.round(functionalityScore * 10) / 10.0) @@ -262,37 +267,37 @@ private UsabilityEvaluationResponse buildUsabilityEvaluation(List feedbacks) { - // 좋았던 점 피드백 + private InsightsResponse buildInsights(List feedbacks) { + // 좋았던 점 피드백 (Feedback.goodPoints) List positiveFeedbacks = feedbacks.stream() - .filter(f -> f.getPositiveFeedback() != null && !f.getPositiveFeedback().isEmpty()) + .filter(f -> f.getGoodPoints() != null && !f.getGoodPoints().isEmpty()) .map(f -> FeedbackItemDto.builder() .feedbackId(f.getId()) - .summary(keywordAnalyzer.summarize(f.getPositiveFeedback())) - .fullContent(f.getPositiveFeedback()) - .emoji(keywordAnalyzer.selectEmoji(f.getPositiveFeedback(), true)) + .summary(keywordAnalyzer.summarize(f.getGoodPoints())) + .fullContent(f.getGoodPoints()) + .emoji(keywordAnalyzer.selectEmoji(f.getGoodPoints(), true)) .build()) .collect(Collectors.toList()); - // 개선 제안 피드백 + // 개선 제안 피드백 (Feedback.improvementSuggestions) List improvementSuggestions = feedbacks.stream() - .filter(f -> f.getImprovementSuggestion() != null && !f.getImprovementSuggestion().isEmpty()) + .filter(f -> f.getImprovementSuggestions() != null && !f.getImprovementSuggestions().isEmpty()) .map(f -> FeedbackItemDto.builder() .feedbackId(f.getId()) - .summary(keywordAnalyzer.summarize(f.getImprovementSuggestion())) - .fullContent(f.getImprovementSuggestion()) - .emoji(keywordAnalyzer.selectEmoji(f.getImprovementSuggestion(), false)) + .summary(keywordAnalyzer.summarize(f.getImprovementSuggestions())) + .fullContent(f.getImprovementSuggestions()) + .emoji(keywordAnalyzer.selectEmoji(f.getImprovementSuggestions(), false)) .build()) .collect(Collectors.toList()); // 키워드 분석 (긍정 피드백 + 개선 제안 모두 분석) List allTexts = new ArrayList<>(); feedbacks.forEach(f -> { - if (f.getPositiveFeedback() != null) { - allTexts.add(f.getPositiveFeedback()); + if (f.getGoodPoints() != null && !f.getGoodPoints().isEmpty()) { + allTexts.add(f.getGoodPoints()); } - if (f.getImprovementSuggestion() != null) { - allTexts.add(f.getImprovementSuggestion()); + if (f.getImprovementSuggestions() != null && !f.getImprovementSuggestions().isEmpty()) { + allTexts.add(f.getImprovementSuggestions()); } }); @@ -320,8 +325,8 @@ private double calculateChangeRate(double current, double previous) { /** * 평균 계산 */ - private double calculateAverage(List feedbacks, - java.util.function.Function getter) { + private double calculateAverage(List feedbacks, + java.util.function.Function getter) { return feedbacks.stream() .map(getter) .filter(Objects::nonNull) @@ -333,8 +338,8 @@ private double calculateAverage(List feedbacks, /** * 분포 계산 (1~5점별 개수) */ - private Map calculateDistribution(List feedbacks, - java.util.function.Function getter) { + private Map calculateDistribution(List feedbacks, + java.util.function.Function getter) { Map distribution = feedbacks.stream() .map(getter) .filter(Objects::nonNull) @@ -351,8 +356,8 @@ private Map calculateDistribution(List feedb /** * 비율 분포 계산 (%) */ - private Map calculatePercentageDistribution(List feedbacks, - java.util.function.Function getter) { + private Map calculatePercentageDistribution(List feedbacks, + java.util.function.Function getter) { Map countDist = calculateDistribution(feedbacks, getter); long total = feedbacks.size(); diff --git a/src/main/java/com/example/nexus/app/feedback/repository/FeedbackRepository.java b/src/main/java/com/example/nexus/app/feedback/repository/FeedbackRepository.java index bbe466f8..880a112a 100644 --- a/src/main/java/com/example/nexus/app/feedback/repository/FeedbackRepository.java +++ b/src/main/java/com/example/nexus/app/feedback/repository/FeedbackRepository.java @@ -1,6 +1,7 @@ package com.example.nexus.app.feedback.repository; import com.example.nexus.app.feedback.domain.Feedback; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -23,5 +24,12 @@ public interface FeedbackRepository extends JpaRepository { @Query("SELECT COUNT(f) FROM Feedback f WHERE f.participation.post.id = :postId") Long countByPostId(@Param("postId") Long postId); + + @Query("SELECT f FROM Feedback f JOIN FETCH f.participation p WHERE p.post.id = :postId " + + "AND f.createdAt >= :startDate AND f.createdAt < :endDate ORDER BY f.createdAt DESC") + List findByPostIdAndDateRange( + @Param("postId") Long postId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/example/nexus/app/feedback/service/FeedbackService.java b/src/main/java/com/example/nexus/app/feedback/service/FeedbackService.java index 8734e46a..3d9df191 100644 --- a/src/main/java/com/example/nexus/app/feedback/service/FeedbackService.java +++ b/src/main/java/com/example/nexus/app/feedback/service/FeedbackService.java @@ -135,6 +135,13 @@ public FeedbackResponse submitFeedback(FeedbackSubmitRequest request, Long userI feedbackDraftRepository.findByParticipationId(participation.getId()) .ifPresent(feedbackDraftRepository::delete); + if (participation.isApproved()) { + participation.complete(); + participationRepository.save(participation); + log.info("피드백 제출로 인한 참여 완료 처리: participationId={}, status={}", + participation.getId(), participation.getStatus()); + } + log.info("피드백 제출 완료: feedbackId={}, participationId={}, userId={}", savedFeedback.getId(), participation.getId(), userId); diff --git a/src/main/java/com/example/nexus/app/global/config/AuditorAwareImpl.java b/src/main/java/com/example/nexus/app/global/config/AuditorAwareImpl.java index 4dec3172..ac7721d8 100644 --- a/src/main/java/com/example/nexus/app/global/config/AuditorAwareImpl.java +++ b/src/main/java/com/example/nexus/app/global/config/AuditorAwareImpl.java @@ -1,10 +1,9 @@ package com.example.nexus.app.global.config; -import com.example.nexus.app.global.code.status.ErrorStatus; -import com.example.nexus.app.global.exception.GeneralException; import com.example.nexus.app.global.oauth.domain.CustomUserDetails; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.AuditorAware; +import org.springframework.lang.NonNull; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -16,6 +15,7 @@ public class AuditorAwareImpl implements AuditorAware { @Override + @NonNull public Optional getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -23,7 +23,7 @@ public Optional getCurrentAuditor() { if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getPrincipal())) { log.warn("인증되지 않은 사용자 - Optional.empty() 반환"); -// throw new GeneralException(ErrorStatus.UNAUTHORIZED); + return Optional.empty(); } // CustomUserDetails에서 직접 userId 가져오기