Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8810f91
[Refactor] AnalysisResponseAssembler 분리 (#279)
shinae1023 Aug 13, 2026
fed0798
[Refactor] Analysis payload sanitization 전용 서비스 분리 (#279)
shinae1023 Aug 13, 2026
d83ff01
[Refactor] Analysis result validation/normalization 분리 (#279)
shinae1023 Aug 13, 2026
8d11a15
[Refactor] AnalysisResultPersistenceService 저장 책임만 남도록 축소 (#279)
shinae1023 Aug 13, 2026
dd17a1f
[Refactor] AnalysisService 공개 API 정리 (#279)
shinae1023 Aug 13, 2026
7c02ce8
[Fix] AnalysisService 공개 API 호환성 복구 (#279)
shinae1023 Aug 13, 2026
70b2ec0
[Fix] 코드리뷰반영
shinae1023 Aug 13, 2026
ed98d67
Merge pull request #280 from JobDri-Developer/refactor/#279-analysis
shinae1023 Aug 13, 2026
973031a
[Fix] 내부 worker API 키 검증을 공통 필터로 이전 (#281)
shinae1023 Aug 15, 2026
c19f95b
[Refactor] 내부 worker 컨트롤러의 중복 인증 로직 제거 (#281)
shinae1023 Aug 15, 2026
0286f2f
[Fix] 내부 worker API 인증 필터 테스트 추가 (#281)
shinae1023 Aug 15, 2026
3d0be2e
[Fix] 내부 worker API 키 검증을 공통 필터로 이전 (#281)
shinae1023 Aug 15, 2026
be590b9
Merge pull request #282 from JobDri-Developer/refactor/#281-internal-…
shinae1023 Aug 15, 2026
44400da
[Feat] Grafana Cloud 수집을 위한 운영 로그 및 메트릭 엔드포인트 구성
whc9999 Sep 7, 2026
0313dfd
[Fix] 운영 콘솔 로그에서 민감한 MDC 정보 노출 차단
whc9999 Sep 7, 2026
8ebec9a
Merge pull request #283 from JobDri-Developer/feat/grafana-cloud-obse…
whc9999 Sep 7, 2026
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
3 changes: 3 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ services:
APP_AUDIT_LOG_PATH: ${APP_AUDIT_LOG_PATH:-/var/log/spring-boot/audit}
ports:
- "${APP_PORT:-8080}:8080"
- "127.0.0.1:${API_METRICS_HOST_PORT:-19090}:9090"
volumes:
- spring-boot-logs:/var/log/spring-boot
depends_on:
Expand Down Expand Up @@ -70,6 +71,8 @@ services:
condition: service_started
rabbitmq:
condition: service_healthy
ports:
- "127.0.0.1:${WORKER_METRICS_HOST_PORT:-18000}:8000"
restart: unless-stopped

loki:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.jobdri.jobdri_api.domain.analysis.service.async.AnalysisWorkerBridgeService;
import com.jobdri.jobdri_api.domain.workerresult.dto.WorkerTaskResultResponse;
import com.jobdri.jobdri_api.global.apiPayload.ApiResponse;
import com.jobdri.jobdri_api.global.security.InternalApiKeyValidator;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -23,7 +22,6 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -34,32 +32,25 @@
@Tag(name = "Analysis Worker Internal", description = "자소서 분석 worker 내부 통신 API")
public class AnalysisWorkerInternalController {

private static final String INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key";

private final InternalApiKeyValidator internalApiKeyValidator;
private final AnalysisWorkerBridgeService analysisWorkerBridgeService;
private final AnalysisAsyncTaskService analysisAsyncTaskService;

@Operation(summary = "자소서 분석 worker 작업 실행 상태 반영", description = "worker가 taskId 기준 자소서 분석 작업을 실행 중 상태로 변경합니다.")
@PostMapping("/tasks/{taskId}/running")
public ApiResponse<Void> markRunning(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId,
@Valid @RequestBody AnalysisWorkerRunningRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
analysisWorkerBridgeService.markRunning(taskId, request.workerId(), request.retryCount(), request.submittedAt());
return ApiResponse.onSuccess("자소서 분석 worker 작업 시작 상태를 반영했습니다.");
}

@Operation(summary = "자소서 분석 worker 작업 재시도 상태 반영", description = "worker가 자소서 분석 작업 실패 후 재시도 상태와 메타데이터를 반영합니다.")
@PostMapping("/tasks/{taskId}/retry")
public ApiResponse<Void> markRetry(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId,
@Valid @RequestBody AnalysisWorkerRetryRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
analysisWorkerBridgeService.markRetry(
taskId,
request.failureReason(),
Expand All @@ -74,11 +65,9 @@ public ApiResponse<Void> markRetry(
@Operation(summary = "자소서 분석 worker 작업 실패 반영", description = "worker가 자소서 분석 작업 실패 상태와 실패 메타데이터를 반영합니다.")
@PostMapping("/tasks/{taskId}/failed")
public ApiResponse<Void> failTask(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId,
@Valid @RequestBody AnalysisWorkerFailureRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
analysisWorkerBridgeService.failTask(
taskId,
request.failureReason(),
Expand All @@ -93,10 +82,8 @@ public ApiResponse<Void> failTask(
@Operation(summary = "자소서 분석 worker 컨텍스트 조회", description = "worker가 분석 실행에 필요한 자소서, 공고, 지원 정보 컨텍스트를 조회합니다.")
@PostMapping("/context")
public ApiResponse<AnalysisWorkerContextResponse> getContext(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@Valid @RequestBody AnalysisWorkerContextRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
return ApiResponse.onSuccess(
"자소서 분석 worker 컨텍스트 조회에 성공했습니다.",
analysisWorkerBridgeService.getContext(request.taskId(), request.userId(), request.mockApplyId())
Expand All @@ -106,11 +93,9 @@ public ApiResponse<AnalysisWorkerContextResponse> getContext(
@Operation(summary = "자소서 분석 worker 작업 완료 반영", description = "worker가 생성한 분석 결과를 저장하고 taskId 기준 작업 완료 상태를 반영합니다.")
@PostMapping("/tasks/{taskId}/complete")
public ApiResponse<AnalysisResponse> completeTask(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId,
@Valid @RequestBody AnalysisWorkerCompleteRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
return ApiResponse.onSuccess(
"자소서 분석 worker 작업 완료 상태를 반영했습니다.",
analysisWorkerBridgeService.completeTask(taskId, request)
Expand All @@ -120,22 +105,18 @@ public ApiResponse<AnalysisResponse> completeTask(
@Operation(summary = "자소서 분석 worker 결과 선저장", description = "worker가 complete 호출 전에 taskId 기준 분석 결과를 durable storage에 저장합니다.")
@PostMapping("/tasks/{taskId}/result")
public ApiResponse<Void> storeResult(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId,
@Valid @RequestBody AnalysisWorkerResultStoreRequest request
) {
internalApiKeyValidator.validate(internalApiKey);
analysisWorkerBridgeService.storeGeneratedResult(taskId, request);
return ApiResponse.onSuccess("자소서 분석 worker 결과 선저장에 성공했습니다.");
}

@Operation(summary = "자소서 분석 worker 저장 결과 조회", description = "worker가 taskId 기준으로 저장된 분석 결과 payload를 조회합니다.")
@GetMapping("/tasks/{taskId}/result")
public ApiResponse<WorkerTaskResultResponse> getStoredResult(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId
) {
internalApiKeyValidator.validate(internalApiKey);
return ApiResponse.onSuccess(
"자소서 분석 worker 저장 결과 조회에 성공했습니다.",
analysisWorkerBridgeService.getStoredResult(taskId)
Expand All @@ -145,10 +126,8 @@ public ApiResponse<WorkerTaskResultResponse> getStoredResult(
@Operation(summary = "자소서 분석 worker 작업 상태 조회", description = "taskId 기준 자소서 분석 worker 비동기 작업 상태를 내부 용도로 조회합니다.")
@GetMapping("/tasks/{taskId}")
public ApiResponse<AnalysisAsyncStatusResponse> getTask(
@RequestHeader(INTERNAL_API_KEY_HEADER) String internalApiKey,
@PathVariable String taskId
) {
internalApiKeyValidator.validate(internalApiKey);
return ApiResponse.onSuccess(
"자소서 분석 worker 작업 상태 조회에 성공했습니다.",
analysisAsyncTaskService.getTaskStatusByTaskId(taskId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,16 @@ public static Analysis create(
}

public void updateHighlightsJson(String keyStrengthsJson, String keyWeaknessesJson) {
updateResultPayloadJson(keyStrengthsJson, keyWeaknessesJson, this.missingKeywordsJson);
}

public void updateResultPayloadJson(
String keyStrengthsJson,
String keyWeaknessesJson,
String missingKeywordsJson
) {
this.keyStrengthsJson = keyStrengthsJson == null ? "[]" : keyStrengthsJson;
this.keyWeaknessesJson = keyWeaknessesJson == null ? "[]" : keyWeaknessesJson;
this.missingKeywordsJson = missingKeywordsJson == null ? "[]" : missingKeywordsJson;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.jobdri.jobdri_api.domain.analysis.service.core;

import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisQuestionResponse;
import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisResponse;
import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionAnalysisResponse;
import com.jobdri.jobdri_api.domain.analysis.entity.Analysis;
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis;
import com.jobdri.jobdri_api.domain.analysis.service.sanitization.AnalysisResultSanitizationService;
import com.jobdri.jobdri_api.domain.analysis.type.QuestionAnalysisStatus;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class AnalysisResponseAssembler {
private final MockApplyRepository mockApplyRepository;

AnalysisResponse toResponse(
MockApply mockApply,
Analysis analysis,
List<Question> questions,
List<QuestionAnalysis> questionAnalyses,
AnalysisResultSanitizationService.AnalysisResultPayload resultPayload
) {
Map<Long, Question> questionById = questions.stream()
.collect(Collectors.toMap(Question::getId, Function.identity()));
Map<Long, List<QuestionAnalysisResponse>> analysesByQuestionId = questionAnalyses.stream()
.filter(questionAnalysis -> isValidQuestionAnalysisForResponse(
questionAnalysis,
questionById.get(questionAnalysis.getQuestion().getId())
))
.collect(Collectors.groupingBy(
questionAnalysis -> questionAnalysis.getQuestion().getId(),
Collectors.mapping(QuestionAnalysisResponse::from, Collectors.toList())
));

List<AnalysisQuestionResponse> questionResponses = questions.stream()
.sorted(Comparator.comparing(Question::getId))
.map(question -> AnalysisQuestionResponse.of(
question,
analysesByQuestionId.getOrDefault(question.getId(), List.of())
))
.toList();

return AnalysisResponse.of(
analysis,
mockApply.getStatus(),
mockApplyRepository.calculateSequence(mockApply),
resultPayload.keyStrengths(),
resultPayload.keyWeaknesses(),
resultPayload.missingKeywords(),
questionResponses
);
}

private boolean isValidQuestionAnalysisForResponse(QuestionAnalysis questionAnalysis, Question question) {
if (questionAnalysis == null || question == null) {
return false;
}
if (questionAnalysis.getStatus() == QuestionAnalysisStatus.MISSING) {
return false;
}
String answer = question.getAnswer();
String sentence = questionAnalysis.getSentence();
int start = questionAnalysis.getStart();
int end = questionAnalysis.getEnd();
if (!StringUtils.hasText(answer) || !StringUtils.hasText(sentence)) {
return false;
}
if (start < 0 || end <= start || end > answer.length()) {
return false;
}
return answer.substring(start, end).equals(sentence);
}
}
Loading
Loading