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 @@ -62,6 +62,10 @@ public static ParticipantAttendanceStatus resolveAttendanceStatus(
return ParticipantAttendanceStatus.ENDED;
}

if (exam.getState() == ExamState.WAITING) {
return ParticipantAttendanceStatus.WAITING;
}

if (submitted && resolveSubmissionDisplayStatus(submitted, submission, score)
!= ParticipantSubmissionDisplayStatus.NOT_SUBMITTED) {
return ParticipantAttendanceStatus.SUBMITTED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.yd.vibecode.domain.admin.domain.service.MasterActivityLogService;
import com.yd.vibecode.domain.auth.application.usecase.AdminLogoutUseCase;
import com.yd.vibecode.domain.auth.domain.entity.Admin;
import com.yd.vibecode.domain.auth.domain.service.AdminAccountDeletionService;
Expand All @@ -20,13 +21,17 @@ public class DeleteOwnAdminAccountUseCase {

private final AdminService adminService;
private final AdminAccountDeletionService adminAccountDeletionService;
private final MasterActivityLogService masterActivityLogService;
private final AdminLogoutUseCase adminLogoutUseCase;
private final CookieUtils cookieUtils;

@Transactional
public void execute(Long adminId, String accessToken, HttpServletResponse httpResponse) {
Admin admin = adminService.findById(adminId);
Long targetAdminId = admin.getId();
String targetDisplayName = admin.getDisplayName();
adminAccountDeletionService.softDelete(admin, adminId, AUDIT_ACTION);
masterActivityLogService.logAdminAccountDeleted(null, targetAdminId, targetDisplayName);

adminLogoutUseCase.execute(accessToken);
cookieUtils.clearAccessTokenCookie(httpResponse);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.yd.vibecode.domain.admin.application.usecase;

import java.time.LocalDateTime;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.yd.vibecode.domain.admin.domain.entity.PlatformSettings;
import com.yd.vibecode.domain.admin.domain.repository.AdminActivityLogRepository;
import com.yd.vibecode.domain.admin.domain.repository.MasterActivityLogRepository;
import com.yd.vibecode.domain.chat.domain.repository.PromptEvaluationRepository;
import com.yd.vibecode.domain.chat.domain.repository.PromptMessageRepository;
import com.yd.vibecode.domain.chat.domain.repository.PromptSessionRepository;
import com.yd.vibecode.domain.exam.domain.entity.ExamState;
import com.yd.vibecode.domain.submission.domain.repository.OutboxEventRepository;
import com.yd.vibecode.domain.submission.domain.repository.ScoreRepository;
import com.yd.vibecode.domain.submission.domain.repository.SubmissionRepository;
import com.yd.vibecode.domain.submission.domain.repository.SubmissionRunRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* 플랫폼 전역 보관 정책에 따라 만료된 로그·제출·AI 산출물을 물리 삭제한다.
* {@code autoDeleteExpiredData=false}이면 스케줄러가 이 UseCase를 호출하지 않는다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PurgeExpiredPlatformDataUseCase {

static final String SUBMISSION_AGGREGATE_TYPE = "SUBMISSION";

private final AdminActivityLogRepository adminActivityLogRepository;
private final MasterActivityLogRepository masterActivityLogRepository;
private final SubmissionRepository submissionRepository;
private final SubmissionRunRepository submissionRunRepository;
private final ScoreRepository scoreRepository;
private final OutboxEventRepository outboxEventRepository;
private final PromptSessionRepository promptSessionRepository;
private final PromptMessageRepository promptMessageRepository;
private final PromptEvaluationRepository promptEvaluationRepository;

@Transactional
public void execute(PlatformSettings settings) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime logCutoff = now.minusDays(settings.getLogRetentionDays());
LocalDateTime submissionCutoff = now.minusDays(settings.getSubmissionRetentionDays());

purgeExpiredLogs(logCutoff);
purgeExpiredSubmissionArtifacts(submissionCutoff);
purgeExpiredPromptArtifacts(submissionCutoff);
}

private void purgeExpiredLogs(LocalDateTime logCutoff) {
int adminLogsDeleted = adminActivityLogRepository.deleteByCreatedAtBefore(logCutoff);
int masterLogsDeleted = masterActivityLogRepository.deleteByCreatedAtBefore(logCutoff);
log.info(
"[DataRetention] Purged expired logs (cutoff={}): adminActivityLogs={}, masterActivityLogs={}",
logCutoff,
adminLogsDeleted,
masterLogsDeleted
);
}

private void purgeExpiredSubmissionArtifacts(LocalDateTime submissionCutoff) {
List<Long> submissionIds = submissionRepository.findIdsByExamStateAndCreatedAtBefore(
ExamState.ENDED,
submissionCutoff
);
if (submissionIds.isEmpty()) {
log.info(
"[DataRetention] No expired submission artifacts to purge (cutoff={}, examState=ENDED)",
submissionCutoff
);
return;
}

int outboxDeleted = outboxEventRepository.deleteByAggregateTypeAndAggregateIdIn(
SUBMISSION_AGGREGATE_TYPE,
submissionIds
);
int runsDeleted = submissionRunRepository.deleteBySubmissionIdIn(submissionIds);
int scoresDeleted = scoreRepository.deleteBySubmissionIdIn(submissionIds);
int submissionsDeleted = submissionRepository.deleteByIdIn(submissionIds);

log.info(
"[DataRetention] Purged expired submission artifacts (cutoff={}, examState=ENDED): "
+ "outboxEvents={}, submissionRuns={}, scores={}, submissions={}",
submissionCutoff,
outboxDeleted,
runsDeleted,
scoresDeleted,
submissionsDeleted
);
}

private void purgeExpiredPromptArtifacts(LocalDateTime submissionCutoff) {
List<Long> sessionIds = promptSessionRepository.findIdsByExamStateAndCreatedAtBefore(
ExamState.ENDED,
submissionCutoff
);
if (sessionIds.isEmpty()) {
log.info(
"[DataRetention] No expired prompt artifacts to purge (cutoff={}, examState=ENDED)",
submissionCutoff
);
return;
}

int messagesDeleted = promptMessageRepository.deleteBySessionIdIn(sessionIds);
int evaluationsDeleted = promptEvaluationRepository.deleteBySessionIdIn(sessionIds);
int sessionsDeleted = promptSessionRepository.deleteByIdIn(sessionIds);

log.info(
"[DataRetention] Purged expired prompt artifacts (cutoff={}, examState=ENDED): "
+ "promptMessages={}, promptEvaluations={}, promptSessions={}",
submissionCutoff,
messagesDeleted,
evaluationsDeleted,
sessionsDeleted
);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.yd.vibecode.domain.admin.domain.repository;

import java.time.LocalDateTime;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -26,4 +29,8 @@ Page<AdminActivityLog> searchByAdmin(
@Param("type") AdminActivityLogType type,
@Param("keyword") String keyword,
Pageable pageable);

@Modifying(clearAutomatically = true)
@Query("DELETE FROM AdminActivityLog l WHERE l.createdAt < :cutoff")
int deleteByCreatedAtBefore(@Param("cutoff") LocalDateTime cutoff);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.yd.vibecode.domain.admin.domain.repository;

import java.time.LocalDateTime;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -24,4 +27,8 @@ Page<MasterActivityLog> search(
@Param("type") MasterActivityLogType type,
@Param("keyword") String keyword,
Pageable pageable);

@Modifying(clearAutomatically = true)
@Query("DELETE FROM MasterActivityLog l WHERE l.createdAt < :cutoff")
int deleteByCreatedAtBefore(@Param("cutoff") LocalDateTime cutoff);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.yd.vibecode.domain.admin.infrastructure;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.yd.vibecode.domain.admin.application.usecase.PurgeExpiredPlatformDataUseCase;
import com.yd.vibecode.domain.admin.domain.entity.PlatformSettings;
import com.yd.vibecode.domain.admin.domain.service.PlatformSettingsService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* 플랫폼 전역 데이터 보관 정책에 따라 만료 데이터를 주기적으로 삭제한다.
* {@code autoDeleteExpiredData=false}이면 아무 데이터도 삭제하지 않는다.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DataRetentionScheduler {

private static final long PURGE_INTERVAL_MS = 86_400_000L;

private final PlatformSettingsService platformSettingsService;
private final PurgeExpiredPlatformDataUseCase purgeExpiredPlatformDataUseCase;

@Scheduled(fixedDelay = PURGE_INTERVAL_MS)
public void purgeExpiredData() {
PlatformSettings settings = platformSettingsService.getOrCreate();
if (!Boolean.TRUE.equals(settings.getAutoDeleteExpiredData())) {
log.debug("[DataRetention] autoDeleteExpiredData=false; skipping purge");
return;
}

try {
purgeExpiredPlatformDataUseCase.execute(settings);
} catch (Exception e) {
log.error("[DataRetention] Failed to purge expired platform data", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.yd.vibecode.domain.chat.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.yd.vibecode.domain.chat.domain.entity.PromptEvaluation;

import com.yd.vibecode.domain.chat.domain.entity.PromptSession;
import java.util.Collection;
import java.util.Optional;

public interface PromptEvaluationRepository extends JpaRepository<PromptEvaluation, Long> {
Expand All @@ -12,4 +16,8 @@ Optional<PromptEvaluation> findBySessionAndTurnAndEvaluationType(
Integer turn,
PromptEvaluation.EvaluationType evaluationType
);

@Modifying(clearAutomatically = true)
@Query("DELETE FROM PromptEvaluation pe WHERE pe.session.id IN :sessionIds")
int deleteBySessionIdIn(@Param("sessionIds") Collection<Long> sessionIds);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.yd.vibecode.domain.chat.domain.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.yd.vibecode.domain.chat.domain.entity.PromptMessage;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

public interface PromptMessageRepository extends JpaRepository<PromptMessage, Long> {

List<PromptMessage> findBySessionIdOrderByTurnAsc(Long sessionId);
Expand All @@ -22,4 +24,8 @@ public interface PromptMessageRepository extends JpaRepository<PromptMessage, Lo
*/
@Query("SELECT MAX(pm.turn) FROM PromptMessage pm WHERE pm.sessionId = :sessionId")
Optional<Integer> findMaxTurnBySessionId(@Param("sessionId") Long sessionId);

@Modifying(clearAutomatically = true)
@Query("DELETE FROM PromptMessage pm WHERE pm.sessionId IN :sessionIds")
int deleteBySessionIdIn(@Param("sessionIds") Collection<Long> sessionIds);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.yd.vibecode.domain.chat.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.yd.vibecode.domain.chat.domain.entity.PromptSession;
import com.yd.vibecode.domain.exam.domain.entity.ExamState;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

Expand All @@ -14,4 +20,18 @@ public interface PromptSessionRepository extends JpaRepository<PromptSession, Lo
List<PromptSession> findByExamId(Long examId);

List<PromptSession> findByParticipantId(Long participantId);

@Query("""
SELECT ps.id FROM PromptSession ps, Exam e
WHERE ps.examId = e.id
AND e.state = :examState
AND ps.createdAt < :cutoff
""")
List<Long> findIdsByExamStateAndCreatedAtBefore(
@Param("examState") ExamState examState,
@Param("cutoff") LocalDateTime cutoff);

@Modifying(clearAutomatically = true)
@Query("DELETE FROM PromptSession ps WHERE ps.id IN :ids")
int deleteByIdIn(@Param("ids") Collection<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public void start() {
if (this.state != ExamState.WAITING) {
throw new RestApiException(ExamErrorStatus.INVALID_EXAM_STATE);
}
this.startsAt = LocalDateTime.now();
this.state = ExamState.RUNNING;
this.version++;
}
Expand All @@ -73,6 +74,7 @@ public void end() {
if (this.state != ExamState.RUNNING) {
throw new RestApiException(ExamErrorStatus.INVALID_EXAM_STATE);
}
this.endsAt = LocalDateTime.now();
this.state = ExamState.ENDED;
this.version++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ public interface ExamRepository extends JpaRepository<Exam, Long> {
* 자동 시작 대상: WAITING 상태이며 시작 시각이 현재 이하인 시험.
*/
List<Exam> findByStateAndStartsAtLessThanEqual(ExamState state, LocalDateTime startsAtThreshold);

/**
* 자동 종료 대상: RUNNING 상태이며 종료 시각이 설정되어 있고 현재 이하인 시험.
*/
List<Exam> findByStateAndEndsAtIsNotNullAndEndsAtLessThanEqual(ExamState state, LocalDateTime endsAtThreshold);
}
Loading
Loading