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 @@ -13,7 +13,6 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand Down Expand Up @@ -41,12 +40,11 @@ public ApiResponse<AnalysisResponse> analyze(
@GetMapping
public ApiResponse<AnalysisResponse> getAnalysis(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long mockApplyId,
@RequestParam(required = false) Integer sequence
@PathVariable Long mockApplyId
) {
return ApiResponse.onSuccess(
"자소서 분석 결과 조회에 성공했습니다.",
analysisService.getAnalysis(getAuthenticatedUser(userDetails), mockApplyId, sequence)
analysisService.getAnalysis(getAuthenticatedUser(userDetails), mockApplyId)
Comment on lines 40 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Spring MVC, if a controller method uses @GetMappingand does not declare@RequestParam("sequence"), does a request to the same path with ?sequence=2still match and ignore the extra query parameter by default? Also, does@GetMapping(params = "!sequence") prevent that request from matching?

💡 Result:

Yes: by default, if a controller method is mapped only with @GetMapping (matching by path and HTTP method) and it does NOT declare any request-parameter conditions via @RequestMapping(params=...) / @GetMapping(params=...), then an extra query parameter like?sequence=2 does not prevent the request from matching. Spring will still route to the handler method; @RequestParam affects only whether you can bind a specific parameter into a method argument, not whether unmapped extra query parameters block request mapping. [1][2] No: @GetMapping(params = "!sequence") does not “ignore”?sequence=2; instead it adds a request-mapping condition that requires the parameter sequence to be absent. Spring’s request-mapping “params” condition supports negation with the! form ("!myParam" means the specified parameter is not supposed to be present in the request). Therefore, a request to the same path with sequence=2 will NOT match a mapping declared with params="!sequence". [1][3][4] Example outcomes: - @GetMapping(path="/x") with no params condition: /x?sequence=2 still matches. - @GetMapping(path="/x", params="!sequence"): /x?sequence=2 does not match; only /x with no sequence parameter matches.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the referenced controllers
ls -R src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/ 2>/dev/null || true

# Show the relevant portions of AnalysisController
sed -n '1,200p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java | nl -ba | sed -n '1,220p'

# Search for sequence usage in the analysis controller package
rg -n "sequence" src/main/java/com/jobdri/jobdri_api/domain/analysis/controller -S || true

# Show JobPostingAnalysisController mapping and method(s) mentioning sequence
sed -n '1,240p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/JobPostingAnalysisController.java | nl -ba | sed -n '1,260p'

Repository: JobDri-Developer/BackEnd

Length of output: 276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant parts of AnalysisController
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java

# Search for "sequence" usage in the analysis controller package
rg -n "sequence" src/main/java/com/jobdri/jobdri_api/domain/analysis/controller -S || true

# Show relevant parts of JobPostingAnalysisController
sed -n '1,260p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/JobPostingAnalysisController.java

Repository: JobDri-Developer/BackEnd

Length of output: 5211


Reject legacy sequence query param on /api/mock-applies/{mockApplyId}/analysis GET to avoid silent wrong results

AnalysisController#getAnalysis maps GET /api/mock-applies/{mockApplyId}/analysis with @GetMapping but does not declare a sequence request parameter (no @RequestParam("sequence") and no params condition). A request to /api/mock-applies/{mockApplyId}/analysis?sequence=N will still match this handler, and sequence will be ignored, returning results based only on mockApplyId—silently breaking the expected “sequence-based” behavior clients may be using.

GET /api/job-postings/{jobPostingId}/analysis?sequence=N

exists in JobPostingAnalysisController, so fail fast on the legacy route by rejecting requests that include sequence.

🧭 Minimal guard to fail fast on legacy `sequence` calls
-    `@GetMapping`
+    `@GetMapping`(params = "!sequence")
     public ApiResponse<AnalysisResponse> getAnalysis(
             `@AuthenticationPrincipal` UserDetailsImpl userDetails,
             `@PathVariable` Long mockApplyId
     ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java`
around lines 40 - 47, The GET handler AnalysisController#getAnalysis currently
matches requests that include a legacy sequence query param and ignores it;
update the mapping to explicitly reject requests that include sequence by adding
params = "!sequence" to the `@GetMapping` on getAnalysis (e.g., `@GetMapping`(params
= "!sequence")), so requests with ?sequence=... won't match this handler and
will fail fast; keep the method signature and service call unchanged.

);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.jobdri.jobdri_api.domain.analysis.controller;

import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisResponse;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisService;
import com.jobdri.jobdri_api.global.apiPayload.ApiResponse;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import com.jobdri.jobdri_api.global.security.UserDetailsImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/job-postings/{jobPostingId}/analysis")
@Tag(name = "Analysis", description = "자소서 분석 API")
public class JobPostingAnalysisController {

private final AnalysisService analysisService;

@Operation(summary = "지원 회차별 자소서 분석 결과 조회", description = "공고 기준으로 지정한 지원 회차의 자소서 분석 결과를 조회합니다.")
@GetMapping
public ApiResponse<AnalysisResponse> getAnalysisBySequence(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long jobPostingId,
@RequestParam Integer sequence
) {
return ApiResponse.onSuccess(
"자소서 분석 결과 조회에 성공했습니다.",
analysisService.getAnalysisByJobPostingSequence(getAuthenticatedUser(userDetails), jobPostingId, sequence)
);
}

private com.jobdri.jobdri_api.domain.user.entity.User getAuthenticatedUser(UserDetailsImpl userDetails) {
if (userDetails == null || userDetails.getUser() == null) {
throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO, "인증 정보가 누락되었습니다.");
}
return userDetails.getUser();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository;
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
import com.jobdri.jobdri_api.domain.audit.annotation.AuditLogEvent;
import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting;
import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingService;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus;
import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository;
Expand Down Expand Up @@ -41,6 +43,7 @@ public class AnalysisService {
private final QuestionRepository questionRepository;
private final AnalysisRepository analysisRepository;
private final QuestionAnalysisRepository questionAnalysisRepository;
private final JobPostingService jobPostingService;
private final AnalysisAiClient analysisAiClient;
private final CreditService creditService;

Expand Down Expand Up @@ -88,29 +91,35 @@ public AnalysisResponse analyze(User user, Long mockApplyId) {
}

public AnalysisResponse getAnalysis(User user, Long mockApplyId) {
return getAnalysis(user, mockApplyId, null);
}

public AnalysisResponse getAnalysis(User user, Long mockApplyId, Integer sequence) {
MockApply mockApply = getOwnedMockApply(user, mockApplyId);
if (sequence != null) {
mockApply = resolveMockApplyBySequence(mockApply, sequence);
}
Long resolvedMockApplyId = mockApply.getId();
Analysis analysis = analysisRepository.findByMockApplyId(mockApply.getId())
.orElseThrow(() -> new GeneralException(
GeneralErrorCode.ANALYSIS_NOT_FOUND,
"해당 모의 서류 지원의 분석 결과를 찾을 수 없습니다. mockApplyId=" + mockApply.getId()
));
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
List<QuestionAnalysis> questionAnalyses =
questionAnalysisRepository.findAllByAnalysisIdOrderByQuestionIdAscIdAsc(analysis.getId());

return toResponse(mockApply, analysis, questions, questionAnalyses);
}

Analysis analysis = analysisRepository.findByMockApplyId(resolvedMockApplyId)
public AnalysisResponse getAnalysisByJobPostingSequence(User user, Long jobPostingId, int sequence) {
JobPosting jobPosting = jobPostingService.getOwnedJobPosting(user, jobPostingId);
MockApply mockApply = resolveMockApplyBySequence(jobPosting, sequence);
Analysis analysis = analysisRepository.findByMockApplyId(mockApply.getId())
.orElseThrow(() -> new GeneralException(
GeneralErrorCode.ANALYSIS_NOT_FOUND,
"해당 모의 서류 지원의 분석 결과를 찾을 수 없습니다. mockApplyId=" + resolvedMockApplyId
"해당 모의 서류 지원의 분석 결과를 찾을 수 없습니다. mockApplyId=" + mockApply.getId()
));
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(resolvedMockApplyId);
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
List<QuestionAnalysis> questionAnalyses =
questionAnalysisRepository.findAllByAnalysisIdOrderByQuestionIdAscIdAsc(analysis.getId());

return toResponse(mockApply, analysis, questions, questionAnalyses);
}

private MockApply resolveMockApplyBySequence(MockApply baseMockApply, int sequence) {
private MockApply resolveMockApplyBySequence(JobPosting jobPosting, int sequence) {
if (sequence < 1) {
throw new GeneralException(
GeneralErrorCode.INVALID_PARAMETER,
Expand All @@ -119,8 +128,8 @@ private MockApply resolveMockApplyBySequence(MockApply baseMockApply, int sequen
}

List<MockApply> mockApplies = mockApplyRepository.findAllByUserIdAndJobPostingIdOrderByIdAsc(
baseMockApply.getUser().getId(),
baseMockApply.getJobPosting().getId()
jobPosting.getUser().getId(),
jobPosting.getId()
);

int derivedSequence = 0;
Expand All @@ -137,8 +146,8 @@ private MockApply resolveMockApplyBySequence(MockApply baseMockApply, int sequen

throw new GeneralException(
GeneralErrorCode.MOCK_APPLY_NOT_FOUND,
"해당 순번의 모의 서류 지원을 찾을 수 없습니다. mockApplyId="
+ baseMockApply.getId()
"해당 순번의 모의 서류 지원을 찾을 수 없습니다. jobPostingId="
+ jobPosting.getId()
+ ", sequence="
+ sequence
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,8 @@ void getAnalysis() {
}

@Test
@DisplayName("sequence 쿼리값으로 같은 공고의 특정 회차 분석 결과를 조회한다")
void getAnalysisBySequence() {
@DisplayName("jobPosting 기준 sequence로 특정 회차 분석 결과를 조회한다")
void getAnalysisByJobPostingSequence() {
User user = saveUser("analysis-get-sequence@example.com");
JobPosting jobPosting = saveJobPosting(user);
MockApply firstMockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL));
Expand Down Expand Up @@ -499,7 +499,7 @@ void getAnalysisBySequence() {
analysisService.analyze(user, firstMockApply.getId());
AnalysisResponse saved = analysisService.analyze(user, secondMockApply.getId());

AnalysisResponse response = analysisService.getAnalysis(user, firstMockApply.getId(), 2);
AnalysisResponse response = analysisService.getAnalysisByJobPostingSequence(user, jobPosting.getId(), 2);

assertThat(response.analysisId()).isEqualTo(saved.analysisId());
assertThat(response.mockApplyId()).isEqualTo(secondMockApply.getId());
Expand All @@ -508,8 +508,8 @@ void getAnalysisBySequence() {
}

@Test
@DisplayName("sequence 쿼리값 조회는 저장된 지원 순번을 우선 사용한다")
void getAnalysisByStoredSequence() {
@DisplayName("jobPosting 기준 sequence 조회는 저장된 지원 순번을 우선 사용한다")
void getAnalysisByJobPostingStoredSequence() {
User user = saveUser("analysis-get-stored-sequence@example.com");
JobPosting jobPosting = saveJobPosting(user);
MockApply firstMockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL));
Expand Down Expand Up @@ -543,7 +543,7 @@ void getAnalysisByStoredSequence() {
analysisService.analyze(user, firstMockApply.getId());
AnalysisResponse saved = analysisService.analyze(user, secondMockApply.getId());

AnalysisResponse response = analysisService.getAnalysis(user, firstMockApply.getId(), 4);
AnalysisResponse response = analysisService.getAnalysisByJobPostingSequence(user, jobPosting.getId(), 4);

assertThat(response.analysisId()).isEqualTo(saved.analysisId());
assertThat(response.mockApplyId()).isEqualTo(secondMockApply.getId());
Expand All @@ -552,12 +552,16 @@ void getAnalysisByStoredSequence() {
}

@Test
@DisplayName("존재하지 않는 sequence로 분석 결과 조회 시 예외를 던진다")
void getAnalysisThrowsWhenSequenceDoesNotExist() {
@DisplayName("존재하지 않는 sequence로 jobPosting 분석 결과 조회 시 예외를 던진다")
void getAnalysisByJobPostingThrowsWhenSequenceDoesNotExist() {
User user = saveUser("analysis-get-sequence-missing@example.com");
MockApply mockApply = saveMockApply(user);

assertThatThrownBy(() -> analysisService.getAnalysis(user, mockApply.getId(), 2))
assertThatThrownBy(() -> analysisService.getAnalysisByJobPostingSequence(
user,
mockApply.getJobPosting().getId(),
2
))
.isInstanceOf(GeneralException.class)
.extracting("code")
.isEqualTo(GeneralErrorCode.MOCK_APPLY_NOT_FOUND);
Expand Down
Loading