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
@@ -0,0 +1,52 @@
package com.example.skillboost.interview.controller;

import com.example.skillboost.interview.dto.InterviewFeedbackRequest;
import com.example.skillboost.interview.dto.InterviewFeedbackResponse;
import com.example.skillboost.interview.dto.InterviewStartRequest;
import com.example.skillboost.interview.dto.InterviewStartResponse;
import com.example.skillboost.interview.service.InterviewFeedbackService;
import com.example.skillboost.interview.service.InterviewService;
import com.example.skillboost.interview.service.SpeechToTextService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

@Profile({"local", "prod"})
@RestController
@RequestMapping("/api/interview")
@RequiredArgsConstructor
public class InterviewController {

private final InterviewService interviewService;
private final InterviewFeedbackService feedbackService;
private final SpeechToTextService speechToTextService;

// 1) 면접 시작 + 질문 생성
@PostMapping("/start")
public ResponseEntity<InterviewStartResponse> start(@RequestBody InterviewStartRequest request) {
InterviewStartResponse response = interviewService.startInterview(request);
return ResponseEntity.ok(response);
}

// 2) (텍스트 기반) 전체 답변 평가
@PostMapping("/feedback")
public ResponseEntity<InterviewFeedbackResponse> feedback(
@RequestBody InterviewFeedbackRequest request
) {
InterviewFeedbackResponse response = feedbackService.createFeedback(request);
return ResponseEntity.ok(response);
}

// 3) 🔊 음성 → 텍스트(STT)만 담당
@PostMapping("/stt")
public ResponseEntity<Map<String, String>> stt(
@RequestPart("audio") MultipartFile audioFile
) {
String text = speechToTextService.transcribe(audioFile);
return ResponseEntity.ok(Map.of("text", text));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewAnswerDto {

// 어떤 질문에 대한 답변인지 구분용
private Long questionId;

// 질문 타입 (기술 / 인성)
private QuestionType type;

// 실제 질문 텍스트
private String question;

// STT로 변환된 지원자의 답변 텍스트
private String answerText;

// 답변에 사용된 시간(초) - 지금은 0으로 둬도 되고, 나중에 프론트에서 계산해서 넣어도 됨
private int durationSec;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewFeedbackRequest {

// 선택적이지만 있으면 리포팅/로깅에 도움 됨
private String sessionId;

// AI 평가용 전체 질문/답변 리스트
private List<InterviewAnswerDto> answers;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor // ← 필요시를 대비한 기본 생성자
@AllArgsConstructor
public class InterviewFeedbackResponse {

// 전체 점수 (0 ~ 100)
private int overallScore;

// 전체 답변에 대한 요약 한 문단
private String summary;

// 각 질문별 점수 + 피드백 리스트
private List<QuestionFeedbackDto> details;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewQuestionDto {

// 세션 내 질문 번호 (1 ~ 5)
private Long id;

// TECH / BEHAV
private QuestionType type;

// 질문 텍스트
private String text;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor // JSON 역직렬화용 필수
@AllArgsConstructor // 생성자 자동 생성
public class InterviewStartRequest {

// GitHub 레포 주소
private String repoUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor // JSON 역직렬화 대비용
@AllArgsConstructor
@Builder // startInterview()에서 builder로 만들기 좋아짐
public class InterviewStartResponse {

// 세션 고유 ID (STT / 답변 제출 시 반드시 필요)
private String sessionId;

// 질문당 제한 시간(초) - 기본 60초
private int durationSec;

// AI 생성 기술 질문 + 인성 질문 총 5개
private List<InterviewQuestionDto> questions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// src/main/java/com/example/skillboost/interview/dto/QuestionFeedbackDto.java
package com.example.skillboost.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class QuestionFeedbackDto {

private Long questionId;
private String questionText; // ✅ 질문 내용 추가
private int score;
private String feedback;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.skillboost.interview.dto;

public enum QuestionType {
TECH, // 기술 질문
BEHAV // 인성 질문
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.skillboost.interview.model;

import com.example.skillboost.interview.dto.InterviewQuestionDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor // 세션 저장 시 역직렬화 대비
@AllArgsConstructor
@Builder
public class InterviewSession implements Serializable {

private String sessionId; // 세션 고유 ID
private String repoUrl; // 레포 주소
private LocalDateTime createdAt; // 세션 생성 시간
private List<InterviewQuestionDto> questions; // 질문 리스트

public static InterviewSession create(String sessionId, String repoUrl, List<InterviewQuestionDto> questions) {
return InterviewSession.builder()
.sessionId(sessionId)
.repoUrl(repoUrl)
.questions(questions)
.createdAt(LocalDateTime.now())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.example.skillboost.interview.service;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class GeminiClient {

private final WebClient.Builder webClientBuilder;

@Value("${gemini.api.key}")
private String apiKey;

@Value("${gemini.model}")
private String model;

private WebClient webClient() {
return webClientBuilder
.baseUrl("https://generativelanguage.googleapis.com/v1beta")
.build();
}

/**
* 단순 텍스트 프롬프트 요청 → 첫 번째 candidate의 text 반환
*/
public String generateText(String prompt) {

Map<String, Object> body = Map.of(
"contents", List.of(
Map.of("parts", List.of(
Map.of("text", prompt)
))
)
);

GeminiResponse response = null;

try {
response = webClient()
.post()
.uri("/models/" + model + ":generateContent?key=" + apiKey)
.bodyValue(body)
.retrieve()
.bodyToMono(GeminiResponse.class)
.onErrorResume(ex -> {
log.error("Gemini API 호출 실패: {}", ex.getMessage());
return Mono.empty();
})
.block();

} catch (Exception e) {
log.error("Gemini 요청 중 서버 오류", e);
return ""; // 완전 실패 시 빈 문자열
}

if (response == null || response.candidates == null || response.candidates.isEmpty()) {
log.warn("Gemini 응답이 비어 있음");
return "";
}

// 첫 후보 꺼내기
GeminiCandidate first = response.candidates.get(0);

if (first.content == null || first.content.parts == null || first.content.parts.isEmpty()) {
log.warn("Gemini content.parts 없음");
return "";
}

String text = first.content.parts.get(0).text;
return text != null ? text.trim() : "";
}

// =============================
// 내부 응답 DTO
// =============================

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
private static class GeminiResponse {
private List<GeminiCandidate> candidates;
}

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
private static class GeminiCandidate {
private GeminiContent content;
}

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
private static class GeminiContent {
private List<GeminiPart> parts;
}

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
private static class GeminiPart {
@JsonProperty("text")
private String text;
}
}
Loading