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 @@ -11,6 +11,7 @@
import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository;
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.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 @@ -44,6 +45,7 @@ public class AnalysisService {
private final CreditService creditService;

@Transactional
@AuditLogEvent(action = "ANALYSIS_RUN", targetType = "MOCK_APPLY", targetId = "#arg1")
public AnalysisResponse analyze(User user, Long mockApplyId) {
MockApply mockApply = getOwnedMockApply(user, mockApplyId);
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
Expand Down Expand Up @@ -78,7 +80,7 @@ public AnalysisResponse analyze(User user, Long mockApplyId) {
questionAnalysisRepository.saveAll(questionAnalyses);
mockApply.updateStatus(MockApplyStatus.COMPLETED);

return getAnalysis(user, mockApplyId);
return toResponse(mockApply, analysis, questions, questionAnalyses);
} catch (RuntimeException e) {
creditService.refund(user, 1, "자소서 분석 실패 환불", referenceId);
throw e;
Expand Down Expand Up @@ -223,4 +225,5 @@ private QuestionAnalysisStatus normalizeStatus(String status) {
return QuestionAnalysisStatus.MENTIONED;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
import com.jobdri.jobdri_api.domain.analysis.repository.CustomQuestionCandidateRepository;
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
import com.jobdri.jobdri_api.domain.audit.annotation.AuditLogEvent;
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 @@ -82,6 +83,7 @@ public List<QuestionCandidateResponse> getQuestionCandidates(User user, Long moc
}

@Transactional
@AuditLogEvent(action = "CUSTOM_QUESTION_CANDIDATE_ADD", targetType = "MOCK_APPLY", targetId = "#arg1")
public QuestionCandidateResponse addCustomQuestionCandidate(
User user,
Long mockApplyId,
Expand Down Expand Up @@ -141,6 +143,7 @@ public QuestionSelectionResponse getSelectedQuestions(User user, Long mockApplyI
}

@Transactional
@AuditLogEvent(action = "QUESTION_SELECTION_SAVE", targetType = "MOCK_APPLY", targetId = "#arg1")
public QuestionSelectionResponse saveSelectedQuestions(
User user,
Long mockApplyId,
Expand Down Expand Up @@ -171,6 +174,7 @@ public QuestionSelectionResponse saveSelectedQuestions(
}

@Transactional
@AuditLogEvent(action = "QUESTION_ANSWER_SAVE", targetType = "MOCK_APPLY", targetId = "#arg1")
public QuestionAnswerResponse saveAnswers(
User user,
Long mockApplyId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.jobdri.jobdri_api.domain.audit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLogEvent {
String action();

String targetType();

String targetId() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.jobdri.jobdri_api.domain.audit.aop;

import com.jobdri.jobdri_api.domain.audit.annotation.AuditLogEvent;
import com.jobdri.jobdri_api.domain.audit.service.AuditLogService;
import com.jobdri.jobdri_api.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class AuditLogAspect {

private static final String RESULT_VARIABLE = "result";

private final AuditLogService auditLogService;
private final ExpressionParser expressionParser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

@Around("@annotation(auditLogEvent)")
public Object recordAuditLog(ProceedingJoinPoint joinPoint, AuditLogEvent auditLogEvent) throws Throwable {
Map<String, Object> beforeValue = extractParameters(joinPoint);

Object result = joinPoint.proceed();

try {
auditLogService.record(
extractUser(joinPoint),
auditLogEvent.action(),
auditLogEvent.targetType(),
evaluateTargetId(joinPoint, auditLogEvent.targetId(), result),
beforeValue,
result
);
} catch (RuntimeException e) {
log.warn("Audit log recording failed. action={}, method={}",
auditLogEvent.action(),
joinPoint.getSignature().toShortString(),
e
);
throw e;
}
Comment on lines +50 to +57
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

Avoid propagating audit write failures to business flow.

Rethrowing here means transient audit issues (serialization/DB) can fail user-facing write operations across all annotated services. This should be best-effort logging unless fail-closed is explicitly required.

Suggested fix
         } catch (RuntimeException e) {
             log.warn("Audit log recording failed. action={}, method={}",
                     auditLogEvent.action(),
                     joinPoint.getSignature().toShortString(),
                     e
             );
-            throw e;
         }
🤖 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/aop/AuditLogAspect.java`
around lines 50 - 57, The catch block in AuditLogAspect that currently rethrows
RuntimeException is causing audit failures to bubble up into business logic;
update the exception handling in AuditLogAspect (the catch(RuntimeException e)
around the audit write) to log the error (as it already does) but do not rethrow
— swallow the exception so the joinPoint/proceeding business flow is unaffected;
keep or enhance the log call for visibility (include the exception) and
optionally emit a metric/event for monitoring instead of throwing.


return result;
}

private User extractUser(ProceedingJoinPoint joinPoint) {
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof User user) {
return user;
}
}
return null;
}

private Long evaluateTargetId(ProceedingJoinPoint joinPoint, String targetIdExpression, Object result) {
if (targetIdExpression == null || targetIdExpression.isBlank()) {
return null;
}

Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
null,
method,
joinPoint.getArgs(),
parameterNameDiscoverer
);
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable("arg" + i, args[i]);
context.setVariable("p" + i, args[i]);
}
context.setVariable(RESULT_VARIABLE, result);

Object value = expressionParser.parseExpression(targetIdExpression).getValue(context);
if (value instanceof Number number) {
return number.longValue();
}
if (value instanceof String string && !string.isBlank()) {
return Long.parseLong(string);
}
return null;
}

private Map<String, Object> extractParameters(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();

Map<String, Object> parameters = new LinkedHashMap<>();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof User) {
continue;
}
String parameterName = parameterNames != null && i < parameterNames.length
? parameterNames[i]
: "arg" + i;
parameters.put(parameterName, args[i]);
}
return parameters;
}
}
102 changes: 102 additions & 0 deletions src/main/java/com/jobdri/jobdri_api/domain/audit/entity/AuditLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.jobdri.jobdri_api.domain.audit.entity;

import com.jobdri.jobdri_api.domain.user.entity.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "audit_logs")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AuditLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@Column(nullable = false, length = 80)
private String action;

@Column(nullable = false, length = 80)
private String targetType;

private Long targetId;

@Column(columnDefinition = "TEXT")
private String beforeValue;

@Column(columnDefinition = "TEXT")
private String afterValue;

@Column(length = 100)
private String ipAddress;

@Column(length = 500)
private String userAgent;

@Column(nullable = false)
private LocalDateTime createdAt;

@Builder(access = AccessLevel.PRIVATE)
private AuditLog(
User user,
String action,
String targetType,
Long targetId,
String beforeValue,
String afterValue,
String ipAddress,
String userAgent,
LocalDateTime createdAt
) {
this.user = user;
this.action = action;
this.targetType = targetType;
this.targetId = targetId;
this.beforeValue = beforeValue;
this.afterValue = afterValue;
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.createdAt = createdAt;
}

public static AuditLog create(
User user,
String action,
String targetType,
Long targetId,
String beforeValue,
String afterValue,
String ipAddress,
String userAgent
) {
return AuditLog.builder()
.user(user)
.action(action)
.targetType(targetType)
.targetId(targetId)
.beforeValue(beforeValue)
.afterValue(afterValue)
.ipAddress(ipAddress)
.userAgent(userAgent)
.createdAt(LocalDateTime.now())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.jobdri.jobdri_api.domain.audit.repository;

import com.jobdri.jobdri_api.domain.audit.entity.AuditLog;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
}
Loading
Loading