Skip to content

[Feat] 주요 변경 작업 감사로그 기록 기능 추가#74

Merged
whc9999 merged 1 commit into
mainfrom
feat/#73-auditlog
May 22, 2026
Merged

[Feat] 주요 변경 작업 감사로그 기록 기능 추가#74
whc9999 merged 1 commit into
mainfrom
feat/#73-auditlog

Conversation

@whc9999
Copy link
Copy Markdown
Collaborator

@whc9999 whc9999 commented May 22, 2026

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

  • AuditLog 엔티티와 Repository, Service 추가
  • 요청 IP와 User-Agent 자동 수집 처리
  • 공고 생성/수정, 모의 서류 지원 생성 감사로그 기록
  • 문항 후보 추가, 문항 선택 저장, 답변 저장 감사로그 기록
  • 자소서 분석 실행 결과 감사로그 기록
  • 변경 전/후 값을 JSON 문자열로 저장하도록 구현

📸 작업 화면 스크린샷

⚠️ PR하기 전에 확인해주세요

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

🚨 관련 이슈 번호 [#73]

Summary by CodeRabbit

  • New Features
    • Implemented comprehensive audit logging system to track and record all user activities and data changes across the platform.
    • Activity logging now covers analysis operations, question management, job posting creation and updates, and mock application creation.
    • Detailed audit trails are maintained for enhanced accountability, monitoring, and compliance purposes.

Review Change Stack

- AuditLog 엔티티와 Repository, Service 추가
- 요청 IP와 User-Agent 자동 수집 처리
- 공고 생성/수정, 모의 서류 지원 생성 감사로그 기록
- 문항 후보 추가, 문항 선택 저장, 답변 저장 감사로그 기록
- 자소서 분석 실행 결과 감사로그 기록
- 변경 전/후 값을 JSON 문자열로 저장하도록 구현
@whc9999 whc9999 self-assigned this May 22, 2026
@whc9999 whc9999 added the 📖 docs Improvements or additions to documentation label May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

The PR introduces a complete audit logging infrastructure and integrates it across multiple services. It adds a persistent AuditLog entity, Spring Data repository, and AuditLogService that captures request context and state snapshots, then applies audit recording to analysis, question, job posting, and mock apply operations.

Changes

Audit Logging Infrastructure and Integration

Layer / File(s) Summary
Audit Persistence Foundation
src/main/java/com/jobdri/jobdri_api/domain/audit/entity/AuditLog.java, src/main/java/com/jobdri/jobdri_api/domain/audit/repository/AuditLogRepository.java
AuditLog JPA entity persists audit records in audit_logs table with user reference, action type, target metadata, JSON-serialized before/after state, request context (IP, user-agent), and creation timestamp. AuditLogRepository provides Spring Data JPA CRUD operations.
Audit Service Implementation
src/main/java/com/jobdri/jobdri_api/domain/audit/service/AuditLogService.java
AuditLogService implements transactional record(...) method that captures active HttpServletRequest via RequestContextHolder, JSON-serializes before/after values with fallback to String.valueOf(), resolves client IP from X-Forwarded-For/X-Real-IP/getRemoteAddr headers, truncates User-Agent header, and persists AuditLog entities.
Analysis Audit Integration
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java
AnalysisService.analyze(...) refactored to build AnalysisResponse directly via toResponse(...) and record ANALYSIS_RUN audit event after persistence. New AnalysisAuditValue record encapsulates analysis id, score metrics, and highlight count as audit payload.
Question Audit Integration
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java
QuestionService integrates audit recording into three operations: addCustomQuestionCandidate() records event after candidate creation; saveSelectedQuestions() captures before snapshot of existing questions, then records before/after comparison; saveAnswers() captures before snapshot of answers, then records before/after comparison. New QuestionAuditValue and QuestionAnswerAuditValue records transform entities into audit payloads with state snapshots.
Job Posting Audit Integration
src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java
JobPostingService records JOB_POSTING_CREATE event (null beforeValue) and JOB_POSTING_UPDATE event (before snapshot + after state) after respective operations. New JobPostingAuditValue record maps JobPosting entity fields (id, company, classification, tasks/requirements/preferred) into audit payload.
Mock Apply Audit Integration
src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java
Three MockApply creation flows (createActualApply, createMockApplyFromJobPosting, createMockApply) refactored to call recordMockApplyCreated() helper before returning response. Helper method and new MockApplyAuditValue record encapsulate apply id, job posting id, apply type, and status as audit payload.

Sequence Diagram

sequenceDiagram
  participant Service
  participant AuditLogService
  participant RequestContext
  participant ObjectMapper
  participant AuditLogRepository
  Service->>AuditLogService: record(user, action, targetType, targetId, beforeValue, afterValue)
  AuditLogService->>RequestContext: currentRequest()
  RequestContext-->>AuditLogService: HttpServletRequest
  AuditLogService->>ObjectMapper: toJson(beforeValue)
  ObjectMapper-->>AuditLogService: serialized beforeValue
  AuditLogService->>ObjectMapper: toJson(afterValue)
  ObjectMapper-->>AuditLogService: serialized afterValue
  AuditLogService->>AuditLogService: resolveIpAddress(request)
  AuditLogService->>AuditLogService: resolveUserAgent(request)
  AuditLogService->>AuditLogRepository: save(AuditLog)
  AuditLogRepository-->>AuditLogService: persisted AuditLog
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • JobDri-Developer/BackEnd#73: This PR implements the comprehensive audit logging feature with AuditLog entity, service, and integrations across domain services as described in the audit logs issue.

Possibly related PRs

  • JobDri-Developer/BackEnd#61: Both PRs modify AnalysisService.analyze() method; PR #61 implements core analysis logic while this PR adds audit event recording around the analysis run.

Poem

🐰 Hop along the audit trail so bright,
With logs of who, what, when, and right,
From requests deep to persistence store,
Each action tracked forevermore!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: adding audit log recording functionality for key operations.
Description check ✅ Passed The description covers all required template sections with detailed implementation details, local testing confirmation, and issue reference included.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#73-auditlog

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java`:
- Around line 84-103: The auditLogService.record(...) call is inside the same
try that catches RuntimeException and triggers creditService.refund(...),
causing successful analyses to be refunded if the audit log fails; update
AnalysisService so audit logging failures do not trigger refunds by moving the
auditLogService.record(...) outside the main try/catch that wraps analysis
execution or by wrapping only the audit call in its own try/catch that logs
audit errors (but does not call creditService.refund) — keep
creditService.refund(...) only in the branch that genuinely represents analysis
execution failure.

In
`@src/main/java/com/jobdri/jobdri_api/domain/audit/service/AuditLogService.java`:
- Around line 82-86: The current catch in AuditLogService that returns
String.valueOf(value) can produce non-JSON output; change the fallback to
produce a valid JSON string instead — call
objectMapper.writeValueAsString(String.valueOf(value)) in the catch so
beforeValue/afterValue remain JSON, and if that secondary call can fail, fall
back to returning a safely quoted/escaped JSON string (i.e., ensure the final
return is a JSON string value). Update the try/catch around
objectMapper.writeValueAsString(value) accordingly so the fallback preserves
JSON format.
- Around line 59-67: The current IP capture in AuditLogService reads
client-supplied headers ("X-Forwarded-For", "X-Real-IP") which can be spoofed;
change the implementation of the IP extraction (the method that calls
request.getHeader(...) in AuditLogService) to stop trusting those headers and
always use the proxy-resolved address returned by request.getRemoteAddr();
remove or ignore usage of forwardedFor.split(...) and realIp.trim() in this
method so audit entries rely solely on request.getRemoteAddr() at this layer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ecb9db7a-481b-4266-b983-7bb91e795615

📥 Commits

Reviewing files that changed from the base of the PR and between adf7cc8 and 64e2104.

📒 Files selected for processing (7)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java
  • src/main/java/com/jobdri/jobdri_api/domain/audit/entity/AuditLog.java
  • src/main/java/com/jobdri/jobdri_api/domain/audit/repository/AuditLogRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/audit/service/AuditLogService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java
  • src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java

Comment on lines +84 to 103
auditLogService.record(
user,
"ANALYSIS_RUN",
"MOCK_APPLY",
mockApply.getId(),
null,
new AnalysisAuditValue(
analysis.getId(),
analysis.getScore(),
analysis.getJobFit(),
analysis.getImpact(),
analysis.getCompleteness(),
questionAnalyses.size()
)
);

return response;
} catch (RuntimeException e) {
creditService.refund(user, 1, "자소서 분석 실패 환불", referenceId);
throw e;
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

Audit failure currently triggers an incorrect credit refund path.

At Line 84, auditLogService.record(...) is inside the same try as the analysis execution, and Line 101 catches all RuntimeExceptions. If audit logging fails after analysis is already saved, Line 102 refunds credit even though the analysis actually succeeded.

Suggested fix
-            AnalysisResponse response = toResponse(mockApply, analysis, questions, questionAnalyses);
-            auditLogService.record(
-                    user,
-                    "ANALYSIS_RUN",
-                    "MOCK_APPLY",
-                    mockApply.getId(),
-                    null,
-                    new AnalysisAuditValue(
-                            analysis.getId(),
-                            analysis.getScore(),
-                            analysis.getJobFit(),
-                            analysis.getImpact(),
-                            analysis.getCompleteness(),
-                            questionAnalyses.size()
-                    )
-            );
-
-            return response;
+            AnalysisResponse response = toResponse(mockApply, analysis, questions, questionAnalyses);
+            try {
+                auditLogService.record(
+                        user,
+                        "ANALYSIS_RUN",
+                        "MOCK_APPLY",
+                        mockApply.getId(),
+                        null,
+                        new AnalysisAuditValue(
+                                analysis.getId(),
+                                analysis.getScore(),
+                                analysis.getJobFit(),
+                                analysis.getImpact(),
+                                analysis.getCompleteness(),
+                                questionAnalyses.size()
+                        )
+                );
+            } catch (RuntimeException ignored) {
+                // 감사로그 실패가 분석 성공/크레딧 정산을 깨지 않도록 분리
+            }
+            return response;
🤖 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/service/AnalysisService.java`
around lines 84 - 103, The auditLogService.record(...) call is inside the same
try that catches RuntimeException and triggers creditService.refund(...),
causing successful analyses to be refunded if the audit log fails; update
AnalysisService so audit logging failures do not trigger refunds by moving the
auditLogService.record(...) outside the main try/catch that wraps analysis
execution or by wrapping only the audit call in its own try/catch that logs
audit errors (but does not call creditService.refund) — keep
creditService.refund(...) only in the branch that genuinely represents analysis
execution failure.

Comment on lines +59 to +67
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isBlank()) {
return forwardedFor.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp.trim();
}
return request.getRemoteAddr();
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

Harden IP capture against header spoofing.

Line 59–66 trust client-supplied forwarding headers directly, so audit IP can be forged. For audit integrity, only use trusted proxy-resolved remote address at this layer.

Suggested fix
 private String resolveIpAddress(HttpServletRequest request) {
     if (request == null) {
         return null;
     }
-
-    String forwardedFor = request.getHeader("X-Forwarded-For");
-    if (forwardedFor != null && !forwardedFor.isBlank()) {
-        return forwardedFor.split(",")[0].trim();
-    }
-    String realIp = request.getHeader("X-Real-IP");
-    if (realIp != null && !realIp.isBlank()) {
-        return realIp.trim();
-    }
     return request.getRemoteAddr();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isBlank()) {
return forwardedFor.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp.trim();
}
return request.getRemoteAddr();
return request.getRemoteAddr();
🤖 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/audit/service/AuditLogService.java`
around lines 59 - 67, The current IP capture in AuditLogService reads
client-supplied headers ("X-Forwarded-For", "X-Real-IP") which can be spoofed;
change the implementation of the IP extraction (the method that calls
request.getHeader(...) in AuditLogService) to stop trusting those headers and
always use the proxy-resolved address returned by request.getRemoteAddr();
remove or ignore usage of forwardedFor.split(...) and realIp.trim() in this
method so audit entries rely solely on request.getRemoteAddr() at this layer.

Comment on lines +82 to +86
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
return String.valueOf(value);
}
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

Keep beforeValue/afterValue strictly JSON on serialization fallback.

Line 85 currently returns raw String.valueOf(value), which can store non-JSON text and break downstream parsing expectations.

Suggested fix
     try {
         return objectMapper.writeValueAsString(value);
     } catch (JsonProcessingException e) {
-        return String.valueOf(value);
+        try {
+            return objectMapper.writeValueAsString(String.valueOf(value));
+        } catch (JsonProcessingException ignored) {
+            return "\"<unserializable>\"";
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
return String.valueOf(value);
}
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
try {
return objectMapper.writeValueAsString(String.valueOf(value));
} catch (JsonProcessingException ignored) {
return "\"<unserializable>\"";
}
}
🤖 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/audit/service/AuditLogService.java`
around lines 82 - 86, The current catch in AuditLogService that returns
String.valueOf(value) can produce non-JSON output; change the fallback to
produce a valid JSON string instead — call
objectMapper.writeValueAsString(String.valueOf(value)) in the catch so
beforeValue/afterValue remain JSON, and if that secondary call can fail, fall
back to returning a safely quoted/escaped JSON string (i.e., ensure the final
return is a JSON string value). Update the try/catch around
objectMapper.writeValueAsString(value) accordingly so the fallback preserves
JSON format.

@whc9999 whc9999 merged commit 64e2104 into main May 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📖 docs Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant