Skip to content

Feat/130 fr int 06 generate follow up questions#138

Merged
gudals2040 merged 4 commits intodevfrom
feat/130-FR-INT-06-generate-follow-up-questions
Mar 11, 2026
Merged

Feat/130 fr int 06 generate follow up questions#138
gudals2040 merged 4 commits intodevfrom
feat/130-FR-INT-06-generate-follow-up-questions

Conversation

@c-wonjun
Copy link
Collaborator

관련 이슈

작업 내용

  • [FR-INT-06, 07] AI 맞춤형 프롬프트 동적 주입
  1. DB에서 사용자의 이력서(Resume) 내용과 채용 공고(Job Posting) 요구사항을 조회하여 Gemini 프롬프트 컨텍스트에 동적으로 주입하는 로직 구현.
  2. 지원자의 경험을 검증하고 직무 적합성을 파악하는 날카로운 꼬리 질문 생성 파이프라인 완성.
  • [FR-INT-08] 면접 결과 리포트 조회 API 구현
  1. GET /api/interviews/{sessionId}/report 엔드포인트 및 InterviewReportResponse DTO 생성.
  2. 세션 상태가 정상적으로 완료된(DONE) 경우에만 리포트 데이터를 반환하도록 비즈니스 검증 로직(InterviewReportService) 추가.
  • 보안 강화 (IDOR 방어)
  1. 세션 소유권 검증 로직을 validateAndGetSessionOwnership 전용 메서드로 분리하여, 타인의 면접 세션(스트리밍, 종료, 리포트 조회)에 접근할 수 없도록 권한 제어 적용.

변경 사항 및 주의 사항

  • Repository 조회 메서드 변경: 권한 없는 데이터 접근을 막기 위해 JobPostingRepository 및 ResumeRepository에서 데이터를 조회할 때 findByIdAndMemberId를 사용하도록 설계
  • AI 프롬프트 흐름 변경: ConversationManager에서 이력서와 공고 내용을 추출한 뒤 InterviewRequestDto에 담아 GeminiService로 전달하도록 파라미터 구조가 변경됨
  • 프론트엔드 연동 주의사항: 리포트 조회 API 호출 시, 해당 세션이 DONE 상태가 아니거나 본인의 세션이 아닐 경우 403 FORBIDDEN 에러를 반환. 프론트엔드 예외 처리 시 참고 바랍니다.

체크 리스트

  • PR 제목 규칙을 준수했습니다
  • 관련 이슈를 연결했습니다
  • 본문 내용을 명확하게 작성했습니다
  • 정상 작동을 로컬 환경에서 검증했습니다

연관 이슈

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 AI 면접 시스템의 핵심 기능을 확장하고 보안을 강화하는 데 중점을 둡니다. 지원자의 이력서와 채용 공고 정보를 AI 프롬프트에 동적으로 주입하여 더욱 정교하고 개인화된 꼬리 질문을 생성할 수 있게 되었으며, 면접 완료 후 결과를 확인할 수 있는 리포트 조회 API를 도입했습니다. 또한, 사용자 세션에 대한 IDOR 취약점을 방어하고 프롬프트 파일 로드 시 Path Traversal 공격을 방지하는 등 전반적인 시스템 보안 수준을 향상했습니다.

Highlights

  • AI 맞춤형 프롬프트 동적 주입 기능 구현: 이력서 내용과 채용 공고 요구사항을 DB에서 조회하여 Gemini AI 프롬프트에 동적으로 주입함으로써, 지원자의 경험과 직무 적합성을 검증하는 꼬리 질문을 생성하는 파이프라인을 완성했습니다.
  • 면접 결과 리포트 조회 API 추가: GET /api/interviews/{sessionId}/report 엔드포인트를 신설하고 InterviewReportResponse DTO를 정의하여, 면접 세션이 'DONE' 상태로 완료된 경우에만 면접 결과를 조회할 수 있도록 비즈니스 로직을 구현했습니다.
  • IDOR(Insecure Direct Object Reference) 방어 강화: 면접 세션 소유권 검증 로직을 validateAndGetSessionOwnership 전용 메서드로 분리하여, 타인의 면접 세션(스트리밍, 종료, 리포트 조회)에 접근할 수 없도록 권한 제어를 적용했습니다.
  • Repository 조회 메서드 보안 강화: JobPostingRepository 및 ResumeRepository에서 데이터를 조회할 때 findByIdAndMemberId 메서드를 사용하도록 변경하여, 권한 없는 데이터 접근을 원천적으로 방지했습니다.
  • AI 프롬프트 전달 구조 개선: ConversationManager에서 이력서와 공고 내용을 추출한 후 InterviewRequestDto에 담아 GeminiService로 전달하도록 파라미터 구조를 변경하여 AI 프롬프트 흐름을 명확히 했습니다.
  • 프롬프트 파일 로드 보안 강화: Path Traversal 공격 방어를 위해 GeminiService의 loadPromptFile 메서드에 허용 목록(Allow-list) 기반의 파일 이름 검증 로직을 추가했습니다.
Changelog
  • src/main/java/com/aibe/team2/domain/interview/controller/InterviewController.java
    • InterviewReportResponse DTO 및 InterviewReportService 의존성을 추가했습니다.
    • streamTextInterview 메서드에서 sessionId 대신 검증된 InterviewSession 객체를 ConversationManager로 전달하도록 수정했습니다.
    • 면접 결과 리포트 조회를 위한 GET /{sessionId}/report 엔드포인트를 추가하고, 소유권 및 세션 상태 검증 로직을 포함했습니다.
    • validateAndGetSessionOwnership 메서드 호출 시 주석을 업데이트했습니다.
  • src/main/java/com/aibe/team2/domain/interview/dto/InterviewReportResponse.java
    • 면접 결과 리포트 데이터를 담는 새로운 DTO를 생성했습니다.
  • src/main/java/com/aibe/team2/domain/interview/dto/InterviewRequestDto.java
    • AI 프롬프트에 동적으로 주입될 resumeContent와 jobDescription 필드를 추가했습니다.
  • src/main/java/com/aibe/team2/domain/interview/service/ConversationManager.java
    • ResumeRepository와 JobPostingRepository 의존성을 추가했습니다.
    • startTextStreaming 메서드 시그니처를 InterviewSession 객체를 받도록 변경하고, 이력서 및 채용 공고 내용을 조회하여 InterviewRequestDto에 포함하도록 로직을 구현했습니다.
    • startTextStreaming 메서드에 @distributedlock 어노테이션을 추가했습니다.
    • 스트리밍 중 에러 발생 시 세션 상태를 ABORTED로 변경하고 로그를 추가했습니다.
  • src/main/java/com/aibe/team2/domain/interview/service/GeminiService.java
    • streamQuestion 메서드에서 이력서 및 채용 공고 컨텍스트를 최종 프롬프트에 동적으로 포함하도록 수정했습니다.
    • loadPromptFile 메서드에 Path Traversal 방어를 위한 허용 목록 기반 파일 이름 검증 로직을 추가하고, 에러 로깅을 개선했습니다.
  • src/main/java/com/aibe/team2/domain/interview/service/InterviewReportService.java
    • 면접 리포트 조회 로직을 담당하는 새로운 서비스를 추가하여 세션이 'DONE' 상태일 때만 리포트를 제공하도록 구현했습니다.
  • src/main/java/com/aibe/team2/domain/jobposting/repository/JobPostingRepository.java
    • IDOR 방어를 위해 findByIdAndMemberId 메서드를 추가했습니다.
  • src/main/resources/static/index.html
    • 테스트용 RESUME_ID 값을 1에서 3으로 변경했습니다.
Activity
  • InterviewController의 streamTextInterview, startVoiceInterview, endInterview 메서드에서 세션 소유권 검증 로직을 전용 private 메서드로 위임하는 변경이 여러 차례의 리뷰를 통해 반영되었습니다.
  • streamTextInterview 메서드에서 sessionId 대신 검증이 완료된 InterviewSession 객체를 ConversationManager로 전달하도록 리뷰 의견이 반영되었습니다.
  • GeminiService의 loadPromptFile 메서드에 Path Traversal 방어를 위한 허용 목록 검증 로직이 보안 리뷰를 통해 추가되었습니다.
  • ConversationManager에서 스트리밍 중 에러 발생 시 세션 상태를 ABORTED로 전환하고 로그를 남기도록 개선되었습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR implements AI-customized follow-up question generation and an interview result report inquiry API. While it introduces important functionality and includes some security improvements like Path Traversal protection, a significant IDOR vulnerability exists due to the current method of verifying session ownership via a user-supplied memberId parameter. Furthermore, the AI prompt construction is vulnerable to prompt injection. For the report inquiry API, consider using more appropriate HTTP status codes. To enhance maintainability, hardcoded strings in the Gemini prompt generation logic should be externalized. Addressing the IDOR issue with a robust authentication framework is a high priority, especially for the newly added report service.

Comment on lines +105 to +110
public ResponseEntity<InterviewReportResponse> getInterviewReport(
@PathVariable Long sessionId,
@RequestParam Long memberId) {

// 1. 소유권 검증: 내 면접이 맞는지 확인 (IDOR 취약점 방어)
InterviewSession session = validateAndGetSessionOwnership(sessionId, memberId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The getInterviewReport endpoint (and others using validateAndGetSessionOwnership) is vulnerable to Insecure Direct Object Reference (IDOR). The memberId used for ownership verification is taken directly from the request parameters (@RequestParam Long memberId) without being verified against the actual authenticated user. An attacker can access any user's interview report by providing that user's memberId and the corresponding sessionId. As noted in the code comments, this should be replaced with a secure authentication mechanism (e.g., @AuthenticationPrincipal from Spring Security) to ensure the memberId corresponds to the logged-in user.

References
  1. When accessing resources via an identifier (e.g., sessionId), always verify that the resource belongs to the currently authenticated user to prevent unauthorized access.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다.

Comment on lines +31 to +43
if (session.getResumeId() != null) {
resumeTitle = resumeRepository.findById(session.getResumeId())
.map(Resume::getTitle)
.orElse("삭제된 이력서");
}

// 채용 공고 제목 조회
String jobTitle = null;
if (session.getJobPostingId() != null) {
jobTitle = jobPostingRepository.findById(session.getJobPostingId())
.map(JobPosting::getJobTitle)
.orElse("삭제된 공고");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

리포트 생성 시 이력서와 채용 공고 제목을 조회할 때 findById()를 사용하고 있습니다. 이 경우, 면접 세션(session)의 소유권은 검증되었지만, 해당 세션에 연결된 이력서(resumeId)나 채용 공고(jobPostingId)가 정말 해당 사용자의 소유인지 검증하지 않아 IDOR(Insecure Direct Object Reference) 취약점이 발생할 수 있습니다.
ConversationManager에서 구현된 것처럼, memberId를 함께 사용하여 resumeRepository.findByIdAndMemberId()jobPostingRepository.findByIdAndMemberId()를 호출하여 소유권을 반드시 검증해야 합니다.

Suggested change
if (session.getResumeId() != null) {
resumeTitle = resumeRepository.findById(session.getResumeId())
.map(Resume::getTitle)
.orElse("삭제된 이력서");
}
// 채용 공고 제목 조회
String jobTitle = null;
if (session.getJobPostingId() != null) {
jobTitle = jobPostingRepository.findById(session.getJobPostingId())
.map(JobPosting::getJobTitle)
.orElse("삭제된 공고");
}
if (session.getResumeId() != null) {
resumeTitle = resumeRepository.findByIdAndMemberId(session.getResumeId(), session.getMemberId())
.map(Resume::getTitle)
.orElse("삭제된 이력서");
}
// 채용 공고 제목 조회
String jobTitle = null;
if (session.getJobPostingId() != null) {
jobTitle = jobPostingRepository.findByIdAndMemberId(session.getJobPostingId(), session.getMemberId())
.map(JobPosting::getJobTitle)
.orElse("삭제된 공고");
}
References
  1. When accessing resources via an identifier (e.g., sessionId), always verify that the resource belongs to the currently authenticated user to prevent unauthorized access.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다

Comment on lines +64 to +65
String finalPrompt = String.format("%s%s%s\n\n%s\n\n[Candidate Answer]\n%s",
atmospherePrompt, resumeContext, jobContext, constraints, request.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The construction of finalPrompt is vulnerable to Prompt Injection. User-supplied input from request.getMessage() is directly concatenated into the prompt string. An attacker could craft a malicious response to manipulate the LLM's instructions, bypass constraints, or potentially leak the system prompt. It is recommended to use structured prompting techniques or sanitize user input to prevent the LLM from interpreting user data as instructions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다

Comment on lines +116 to +119
} catch (IllegalStateException e) {
// 상태가 DONE이 아닐 경우 403 Forbidden 에러 반환
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

IllegalStateException이 발생했을 때 HttpStatus.FORBIDDEN (403)을 반환하도록 구현하셨습니다. 하지만 403은 인증된 사용자가 리소스에 접근할 권한이 없을 때 사용하는 상태 코드입니다. 이 경우, 사용자는 자신의 면접 리포트에 접근할 권한이 있지만, 면접이 아직 완료되지 않아 리소스의 상태가 요청을 처리하기에 적절하지 않은 상황입니다.
이러한 "상태 충돌" 상황에는 HttpStatus.CONFLICT (409)가 더 적합한 상태 코드입니다. API의 의미를 더 명확하게 전달하기 위해 상태 코드를 변경하는 것을 고려해 보세요.

Suggested change
} catch (IllegalStateException e) {
// 상태가 DONE이 아닐 경우 403 Forbidden 에러 반환
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
}
} catch (IllegalStateException e) {
// 상태가 DONE이 아닐 경우 409 Conflict 에러 반환
throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage());
}

Comment on lines +51 to +61
// [FR-INT-06] 이력서 기반 꼬리 질문 생성을 위한 컨텍스트 동적 주입
String resumeContext = "";
if (request.getResumeContent() != null && !request.getResumeContent().isBlank()) {
resumeContext = "\n\n[Candidate's Resume]\n다음은 지원자의 자기소개서 내용입니다. 이를 바탕으로 지원자의 경험을 묻는 꼬리 질문을 생성하세요.\n" + request.getResumeContent();
}

// [FR-INT-07] 채용 공고 기반 맞춤형 질문 생성을 위한 컨텍스트 동적 주입
String jobContext = "";
if (request.getJobDescription() != null && !request.getJobDescription().isBlank()) {
jobContext = "\n\n[Job Posting Requirements]\n다음은 지원자가 지원한 채용 공고의 상세 내용(요구 역량 및 주요 업무)입니다. 이를 바탕으로 직무 적합성을 검증하는 질문을 생성하세요.\n" + request.getJobDescription();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이력서와 채용 공고 컨텍스트를 프롬프트에 주입하기 위한 문자열이 코드에 직접 하드코딩되어 있습니다. 프롬프트는 비즈니스 로직의 중요한 부분이므로, 다른 프롬프트 파일(NORMAL.txt, constraints.txt)처럼 외부 .txt 파일로 분리하여 관리하는 것이 유지보수성 측면에서 더 좋습니다.
예를 들어, resume_context.txtjob_context.txt 파일을 만들어 프롬프트 템플릿을 저장하고, String.format을 사용해 동적으로 내용을 채워넣는 방식을 고려해볼 수 있습니다.

@gudals2040 gudals2040 merged commit a29797c into dev Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants