Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Objects;

@Tag(name = "Analysis", description = "분석 리포트 API")
@RestController
Expand All @@ -38,8 +39,22 @@ public ResponseEntity<ApiResponse<AnalysisReportResponse>> getReport(
public ResponseEntity<ApiResponse<Void>> analysisComplete(
@RequestBody AnalysisCompleteRequest request) {
analysisService.saveResult(request);
slackNotifier.notify("✅ 분석 결과 저장 — videoId=" + request.getVideoId()
+ ", totalHits=" + request.getTotalHits());

String storedMode = videoRepository.findById(request.getVideoId())
.map(Video::getAnalysisMode)
.orElse(null);
String callbackMode = request.getAnalysisMode();
String modeCheck = callbackMode == null
? "NOT_REPORTED"
: Objects.equals(storedMode, callbackMode) ? "MATCH" : "MISMATCH";
String completionLog = "✅ 분석 결과 저장 — videoId=" + request.getVideoId()
+ ", storedMode=" + storedMode
+ ", callbackMode=" + callbackMode
+ ", modeCheck=" + modeCheck
+ ", strokeSchemes=" + request.getStrokeClassSchemes()
+ ", totalHits=" + request.getTotalHits();
System.out.println(completionLog);
slackNotifier.notify(completionLog);
return ResponseEntity.ok(ApiResponse.success("분석 결과 저장 완료", null));
}

Expand All @@ -56,7 +71,11 @@ public ResponseEntity<ApiResponse<Void>> analysisFail(
});

System.err.println("[분석 실패] videoId=" + videoId + ", error=" + error);
slackNotifier.notify("❌ 분석 실패 — videoId=" + videoId + "\n에러: " + error);
String storedMode = videoRepository.findById(videoId)
.map(Video::getAnalysisMode)
.orElse(null);
slackNotifier.notify("❌ 분석 실패 — videoId=" + videoId
+ ", storedMode=" + storedMode + "\n에러: " + error);
return ResponseEntity.ok(ApiResponse.success("실패 상태 업데이트 완료", null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class AnalysisCompleteRequest {
private List<HitData> hitsData;
private String skeletonVideoUrl;
private String minimapVideoUrl;
private String analysisMode;
private List<String> strokeClassSchemes;

@JsonProperty("rallyResults")
private List<RallyResultData> rallyResults;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public static class VideoInfo {
private String thumbnailUrl;
// 초 단위 정수 — 프론트에서 "M:SS" 포맷 변환 또는 표시 용도
private Integer durationSeconds;
// "pro" | "amateur". null이면 컬럼 추가 전 업로드된 기존 영상
private String analysisMode;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public class VideoUploadResponse {
private String title;
private String uploadDate;
private String status;
private String analysisMode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public class Video {
@Column(name = "duration_seconds")
private Integer durationSeconds;

// "pro" | "amateur". NULL은 컬럼 추가 전 업로드된 기존 영상이라 모드를 알 수 없다는 뜻이다.
@Column(name = "analysis_mode", length = 16)
private String analysisMode;

@Column(name = "video_status")
@Builder.Default
private String videoStatus = "PROCESSING";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,18 @@ public VideoUploadResponse uploadVideo(Long userId, MultipartFile videoFile,
: null)
.durationSeconds(
durationSeconds != null && durationSeconds > 0 ? durationSeconds : null)
.analysisMode(analysisMode)
.build();

Video saved = videoRepository.save(video);

slackNotifier.notify(String.format(
"📤 영상 업로드 — videoId=%d, 제목: %s, mode=%s, 크기: %.1fMB, 재생시간: %ds",
String uploadLog = String.format(
"📤 영상 업로드 — videoId=%d, 제목: %s, analysisMode=%s (DB 저장), 크기: %.1fMB, 재생시간: %ds",
saved.getVideoId(), title, analysisMode,
videoFile.getSize() / 1024.0 / 1024.0,
durationSeconds != null ? durationSeconds : 0));
durationSeconds != null ? durationSeconds : 0);
System.out.println(uploadLog);
slackNotifier.notify(uploadLog);

// AI 서버 호출
try {
Expand Down Expand Up @@ -157,8 +160,10 @@ public VideoUploadResponse uploadVideo(Long userId, MultipartFile videoFile,
aiServerUrl + "/analyze",
analyzeRequest,
String.class);
slackNotifier.notify("🚀 AI 분석 요청 전송 완료 — videoId=" + saved.getVideoId()
+ " → " + aiServerUrl);
String requestLog = "🚀 AI 분석 요청 전송 완료 — videoId=" + saved.getVideoId()
+ ", analysisMode=" + analysisMode + " → " + aiServerUrl;
System.out.println(requestLog);
slackNotifier.notify(requestLog);
} catch (Exception e) {
e.printStackTrace();
// AI 서버 호출 실패해도 업로드 자체는 성공으로 처리
Expand All @@ -173,6 +178,7 @@ public VideoUploadResponse uploadVideo(Long userId, MultipartFile videoFile,
.title(saved.getTitle())
.uploadDate(saved.getUploadDate().format(DateTimeFormatter.ISO_LOCAL_DATE))
.status(saved.getVideoStatus())
.analysisMode(saved.getAnalysisMode())
.build();
}

Expand Down Expand Up @@ -244,6 +250,7 @@ public VideoDetailResponse getVideoDetail(Long videoId) {
? s3Service.generatePresignedUrl(video.getThumbnailUrl())
: null)
.durationSeconds(video.getDurationSeconds())
.analysisMode(video.getAnalysisMode())
.build())
.matchSummary(matchSummary)
.timelineEvents(eventDtos)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.rallytrack.backend.domain.analysis.dto;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class AnalysisCompleteRequestContractTest {

private final ObjectMapper objectMapper = new ObjectMapper();

@Test
void readsAnalysisModeAndActualClassifierSchemesFromAiCallback() throws Exception {
AnalysisCompleteRequest request = objectMapper.readValue("""
{
"videoId": 10,
"analysisMode": "pro",
"strokeClassSchemes": ["9class"],
"totalHits": 6
}
""", AnalysisCompleteRequest.class);

assertThat(request.getAnalysisMode()).isEqualTo("pro");
assertThat(request.getStrokeClassSchemes()).containsExactly("9class");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.rallytrack.backend.domain.video.repository;

import com.rallytrack.backend.domain.user.entity.User;
import com.rallytrack.backend.domain.user.repository.UserRepository;
import com.rallytrack.backend.domain.video.entity.Video;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@ActiveProfiles("test")
class VideoAnalysisModePersistenceTest {

@Autowired
private VideoRepository videoRepository;

@Autowired
private UserRepository userRepository;

@Autowired
private EntityManager entityManager;

@Test
void persistsProAndAmateurModesSeparately() {
User user = userRepository.save(User.builder()
.email("mode-test@example.com")
.password("test")
.nickname("mode-test")
.build());

Long proId = videoRepository.save(Video.builder()
.title("pro video")
.analysisMode("pro")
.user(user)
.build()).getVideoId();
Long amateurId = videoRepository.save(Video.builder()
.title("amateur video")
.analysisMode("amateur")
.user(user)
.build()).getVideoId();

videoRepository.flush();
entityManager.clear();

assertThat(videoRepository.findById(proId).orElseThrow().getAnalysisMode())
.isEqualTo("pro");
assertThat(videoRepository.findById(amateurId).orElseThrow().getAnalysisMode())
.isEqualTo("amateur");
}
}