Skip to content

Commit 131fe51

Browse files
authored
Merge pull request #27 from SWUNITZEL/feat/#11-report-nonverbal-communication
[FET] 비언어적 커뮤니케이션 결과 분석 보고서 API
2 parents b6e6d59 + 4c1ee31 commit 131fe51

10 files changed

Lines changed: 325 additions & 0 deletions

File tree

src/main/java/com/swunitzel/fiterview/apiPayload/code/status/ErrorStatus.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public enum ErrorStatus implements BaseErrorCode {
1818
// 생활기록부 오류
1919
_SCHOOL_RECORD_NOT_FOUND(HttpStatus.BAD_REQUEST, "SCHOOL-RECORD403", "사용자의 생활기록부 분석 정보가 존재하지 않습니다"),
2020

21+
// 인터뷰 관련 오류
22+
_INTERVIEW_NOT_FOUND(HttpStatus.BAD_REQUEST, "INTERVIEW404", "인터뷰를 찾을 수 없습니다"),
23+
2124
;
2225

2326
private final HttpStatus httpStatus;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.swunitzel.fiterview.apiPayload.exception.handler;
2+
3+
import com.swunitzel.fiterview.apiPayload.code.BaseErrorCode;
4+
import com.swunitzel.fiterview.apiPayload.exception.GeneralException;
5+
6+
public class InterviewHandler extends GeneralException {
7+
public InterviewHandler(BaseErrorCode code) {
8+
super(code);
9+
}
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.swunitzel.fiterview.controller;
2+
3+
import com.swunitzel.fiterview.apiPayload.ApiResponse;
4+
import com.swunitzel.fiterview.dto.ReportDto;
5+
import com.swunitzel.fiterview.dto.SchoolRecordResponseDto;
6+
import com.swunitzel.fiterview.jwt.CustomUserDetails;
7+
import com.swunitzel.fiterview.services.ReportService;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
@RequiredArgsConstructor
17+
@RequestMapping("/report")
18+
public class ReportController {
19+
20+
private final ReportService reportService;
21+
22+
@GetMapping("/{interviewId}/nonverbal-communication")
23+
public ApiResponse<ReportDto.NonverbalCommunicationReportDto> getNonVerbalCommunicationReport(@PathVariable(name = "interviewId") String interviewId) {
24+
ReportDto.NonverbalCommunicationReportDto reportDto = reportService.getNonverbalCommunicationReport(interviewId);
25+
return ApiResponse.onSuccess(reportDto);
26+
}
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.swunitzel.fiterview.converter;
2+
3+
import com.swunitzel.fiterview.domain.Interview;
4+
import com.swunitzel.fiterview.dto.ReportDto;
5+
6+
public class ReportConverter {
7+
8+
public static ReportDto.NonverbalCommunicationReportDto toDto(Interview interview) {
9+
return ReportDto.NonverbalCommunicationReportDto.builder()
10+
.totalScore(toTotalScoreDto(interview))
11+
.avgShoulderTiltCount(interview.getAvgShoulderTiltCount())
12+
.avgTurnLeftCount(interview.getAvgTurnLeftCount())
13+
.avgTurnRightCount(interview.getAvgTurnRightCount())
14+
.build();
15+
}
16+
17+
public static ReportDto.NonverbalCommunicationReportTotalScoreDto toTotalScoreDto(Interview interview) {
18+
return ReportDto.NonverbalCommunicationReportTotalScoreDto.builder()
19+
.avgPostureScore(interview.getAvgPostureScore())
20+
.avgFacialScore(interview.getAvgFacialScore())
21+
.avgGazeScore(interview.getAvgGazeScore())
22+
.build();
23+
}
24+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.swunitzel.fiterview.domain;
2+
3+
import lombok.*;
4+
import org.springframework.data.annotation.Id;
5+
import org.springframework.data.mongodb.core.mapping.Document;
6+
import org.springframework.data.mongodb.core.mapping.Field;
7+
8+
9+
@Document(collection = "answers")
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class Answer {
15+
@Id
16+
private String id;
17+
18+
@Field("interview_id")
19+
private String interviewId;
20+
21+
@Field("question_id")
22+
private String questionId;
23+
24+
private String keyword;
25+
26+
private String answer;
27+
28+
private String summary;
29+
30+
@Field("aiAnalysis_comment")
31+
private String aiAnalysisComment;
32+
33+
@Field("improved_answer")
34+
private String improvedAnswer;
35+
36+
@Field("blind_rule_adherence")
37+
private String blindRuleAdherence;
38+
39+
@Field("smile_ratio")
40+
private float smileRatio;
41+
42+
@Field("gaze_down_count")
43+
private int gazeDownCount;
44+
45+
@Field("gaze_points")
46+
private String gazePoints;
47+
48+
@Field("shoulder_tilt_count")
49+
private int shoulderTiltCount;
50+
51+
@Field("turn_left_count")
52+
private int turnLeftCount;
53+
54+
@Field("turn_right_count")
55+
private int turnRightCount;
56+
57+
@Field("video_url")
58+
private String videoUrl;
59+
60+
@Field("speaking_speed")
61+
private float speakingSpeed;
62+
63+
@Field("pitch_mean")
64+
private float pitchMean;
65+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.swunitzel.fiterview.domain;
2+
3+
import lombok.*;
4+
import org.springframework.data.annotation.Id;
5+
import org.springframework.data.mongodb.core.mapping.Document;
6+
import org.springframework.data.mongodb.core.mapping.Field;
7+
8+
@Document(collection = "interview")
9+
@Getter
10+
@Setter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class Interview {
14+
15+
@Id
16+
private String id;
17+
18+
@Field("avg_posture_score")
19+
private float avgPostureScore;
20+
21+
@Field("avg_facial_score")
22+
private float avgFacialScore;
23+
24+
@Field("avg_gaze_score")
25+
private float avgGazeScore;
26+
27+
@Field("avg_shoulder_tilt_count")
28+
private float avgShoulderTiltCount;
29+
30+
@Field("avg_turn_left_count")
31+
private float avgTurnLeftCount;
32+
33+
@Field("avg_turn_right_count")
34+
private float avgTurnRightCount;
35+
36+
public Interview updateTotalScore(float avgPostureScore, float avgFacialScore, float avgGazeScore,
37+
float avgShoulderTiltCount, float avgTurnLeftCount, float avgTurnRightCount) {
38+
this.avgPostureScore = avgPostureScore;
39+
this.avgFacialScore = avgFacialScore;
40+
this.avgGazeScore = avgGazeScore;
41+
this.avgShoulderTiltCount = avgShoulderTiltCount;
42+
this.avgTurnLeftCount = avgTurnLeftCount;
43+
this.avgTurnRightCount = avgTurnRightCount;
44+
return this;
45+
}
46+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.swunitzel.fiterview.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
public class ReportDto {
9+
10+
@Builder
11+
@Getter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public static class NonverbalCommunicationReportTotalScoreDto{
15+
private float avgPostureScore;
16+
private float avgFacialScore;
17+
private float avgGazeScore;
18+
19+
}
20+
21+
@Builder
22+
@Getter
23+
@NoArgsConstructor
24+
@AllArgsConstructor
25+
public static class NonverbalCommunicationReportDto{
26+
private NonverbalCommunicationReportTotalScoreDto totalScore;
27+
private float avgShoulderTiltCount;
28+
private float avgTurnLeftCount;
29+
private float avgTurnRightCount;
30+
31+
}
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.swunitzel.fiterview.repository;
2+
3+
import com.swunitzel.fiterview.domain.Answer;
4+
import org.springframework.data.mongodb.repository.MongoRepository;
5+
6+
import java.util.List;
7+
8+
public interface AnswerRepository extends MongoRepository<Answer, String> {
9+
10+
List<Answer> findAllByInterviewId(String interviewId);
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.swunitzel.fiterview.repository;
2+
3+
import com.swunitzel.fiterview.domain.Interview;
4+
import org.springframework.data.mongodb.core.aggregation.ArrayOperators;
5+
import org.springframework.data.mongodb.repository.MongoRepository;
6+
7+
public interface InterviewRepository extends MongoRepository<Interview, String> {
8+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.swunitzel.fiterview.services;
2+
3+
import com.swunitzel.fiterview.apiPayload.code.status.ErrorStatus;
4+
import com.swunitzel.fiterview.apiPayload.exception.handler.InterviewHandler;
5+
import com.swunitzel.fiterview.converter.ReportConverter;
6+
import com.swunitzel.fiterview.domain.Answer;
7+
import com.swunitzel.fiterview.domain.Interview;
8+
import com.swunitzel.fiterview.dto.ReportDto;
9+
import com.swunitzel.fiterview.repository.AnswerRepository;
10+
import com.swunitzel.fiterview.repository.InterviewRepository;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.stereotype.Service;
13+
14+
import java.util.List;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class ReportService {
19+
20+
private final AnswerRepository answerRepository;
21+
private final InterviewRepository interviewRepository;
22+
23+
public ReportDto.NonverbalCommunicationReportDto getNonverbalCommunicationReport(String interviewId) {
24+
25+
List<Answer> answers = answerRepository.findAllByInterviewId(interviewId);
26+
27+
// 영상 분석 결과 지표 합계
28+
float totalPostureScore = 0f;
29+
float totalSmileScore = 0f;
30+
int totalGazeDownScore = 0;
31+
32+
// 자세 측정 카운드
33+
int totalShoulderTiltCount = 0;
34+
int totalTurnLeftCount = 0;
35+
int totalTurnRightCount = 0;
36+
37+
for (Answer answer : answers) {
38+
totalPostureScore += scorePosture(
39+
answer.getShoulderTiltCount(),
40+
answer.getTurnLeftCount(),
41+
answer.getTurnRightCount());
42+
totalSmileScore += scoreSmile(answer.getSmileRatio());
43+
totalGazeDownScore += scoreGaze(answer.getGazeDownCount());
44+
totalShoulderTiltCount += answer.getShoulderTiltCount();
45+
totalTurnLeftCount += answer.getTurnLeftCount();
46+
totalTurnRightCount += answer.getTurnRightCount();
47+
}
48+
49+
int answerCount = answers.size();
50+
51+
float avgPostureScore = totalPostureScore / answerCount;
52+
float avgFacialScore = totalSmileScore / answerCount;
53+
float avgGazeScore = totalGazeDownScore / answerCount;
54+
float avgShoulderTiltCount = totalShoulderTiltCount / answerCount;
55+
float avgTurnLeftCount = totalTurnLeftCount / answerCount;
56+
float avgTurnRightCount = totalTurnRightCount / answerCount;
57+
58+
// 인터뷰에 총첨 업데이트
59+
Interview interview = interviewRepository.findById(interviewId)
60+
.orElseThrow(() -> new InterviewHandler(ErrorStatus._INTERVIEW_NOT_FOUND));
61+
62+
Interview updatedInterview = interview.updateTotalScore(avgPostureScore, avgFacialScore, avgGazeScore,
63+
avgShoulderTiltCount, avgTurnLeftCount, avgTurnRightCount);
64+
65+
return ReportConverter.toDto(updatedInterview);
66+
}
67+
68+
int scoreGaze(int downCount) {
69+
if (downCount <= 3) {
70+
return 100;
71+
} else if (downCount <= 6) {
72+
return 90;
73+
} else if (downCount <= 10) {
74+
return 80;
75+
} else if (downCount <= 15) {
76+
return 70;
77+
} else {
78+
return 60;
79+
}
80+
}
81+
82+
float scorePosture(int shoulderTilt, int turnLeft, int turnRight) {
83+
84+
// 가중치 적용 (어깨기울기 2배, 고개 돌림 각각 1배)
85+
int weightedSum = shoulderTilt * 2 + turnLeft + turnRight;
86+
87+
return 100f - weightedSum * 1.5f;
88+
}
89+
90+
float scoreSmile(float smilRatio) {
91+
if (smilRatio >= 0.5f) {
92+
return 90 + (smilRatio - 0.5f) * 20; // 90~100점
93+
} else if (smilRatio >= 0.3f) {
94+
return 70 + (smilRatio - 0.3f) * 100; // 70~89점
95+
} else {
96+
return 50 * smilRatio / 0.3f; // 0~50점
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)