Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
dd69103
mosu-240 feat: ratelimit 관리할 객체들 구현
jbh010204 Aug 7, 2025
15580b0
mosu-240 feat: TimePenalty 관리할 enum 생성
jbh010204 Aug 7, 2025
516efda
mosu-240 feat: 페널티 증가에 따른 접속 차단 시간 증가 구현
jbh010204 Aug 7, 2025
7a4b055
mosu-240 refactor: 카페인 캐시 설정들 config로 분리
jbh010204 Aug 7, 2025
51c8e7b
mosu-240 refactor: IP 차단 로그를 기록하기 위한 Entity 생성
jbh010204 Aug 7, 2025
e37be02
mosu-240 feat: 차단된 유저 기록 저장을 위한 엔티티 생성
jbh010204 Aug 7, 2025
1bcb134
mosu-240 feat: 차단IP기록 DB영속화 및 배치 insert구현
jbh010204 Aug 7, 2025
1ccfd98
mosu-240 feat: 배치 처리를 위한 Achiever 구현
jbh010204 Aug 7, 2025
ece06ed
mosu-240 feat: 폴더 구조 변경
jbh010204 Aug 7, 2025
5bc3998
mosu-240 refactor: 리뷰 내용 반영
jbh010204 Aug 7, 2025
4df6718
Merge pull request #245 from mosu-dev/refactor/mosu-240
jbh010204 Aug 7, 2025
be9b81c
mosu-246 refactor: 공지 목록 조회시 파일 첨부 x
jbh010204 Aug 7, 2025
b3421cd
mosu-246 refactor: public URL변경
jbh010204 Aug 7, 2025
0f3198d
MOSU-246 refactor:: integrate user details into notice creation and u…
jbh010204 Aug 7, 2025
0700fce
MOSU-246 refactor: enhance inquiry answer handling with author tracki…
jbh010204 Aug 7, 2025
46a6bac
MOSU-246 refactor: streamline attachment handling by consolidating de…
jbh010204 Aug 7, 2025
dafcc96
MOSU-246 refactor: enhance global exception handling with additional …
jbh010204 Aug 7, 2025
1983734
MOSU-246 refactor: enhance inquiry answer methods to include user tra…
jbh010204 Aug 7, 2025
bde3392
refactor: simplify custom runtime exception handling by removing unne…
jbh010204 Aug 7, 2025
722d9c1
MOSU-246 refactor: update title and content field lengths in inquiry …
jbh010204 Aug 7, 2025
a02c2ab
Merge pull request #247 from mosu-dev/refactor/mosu-246
jbh010204 Aug 7, 2025
4e5fba1
feat: add ArchivingOrchestratorJob for scheduled domain archiving
polyglot-k Aug 8, 2025
9156dae
feat: add QuartzAutoRegisterConfig for automatic job registration and…
polyglot-k Aug 8, 2025
a23d073
feat: add CronJob annotation and CronJobExecutor interface for schedu…
polyglot-k Aug 8, 2025
72d05de
feat: add CronTarget annotation and DomainArchiveExecutor interface f…
polyglot-k Aug 8, 2025
4f76947
feat: rename and refactor log cleanup and archiving classes for cron …
polyglot-k Aug 8, 2025
3a9f056
feat: rename AutowiringSpringBeanJobFactory and update package to sup…
polyglot-k Aug 8, 2025
bc5b811
feat: rename LogCleanupJob and update package structure; refactor cle…
polyglot-k Aug 8, 2025
22ddb8b
feat: rename cleanup and archiving classes to use Executor pattern; u…
polyglot-k Aug 8, 2025
9904a69
feat: comment out scheduled processing in RollbackLogScheduler
polyglot-k Aug 8, 2025
791ff20
feat: add DisallowConcurrentExecution annotation to ArchivingOrchestr…
polyglot-k Aug 8, 2025
003ce2f
feat: remove debug log statement from PaymentFailureLogDomainArchiveE…
polyglot-k Aug 8, 2025
c0d23ea
feat: handle exceptions during Quartz job registration in QuartzAutoR…
polyglot-k Aug 8, 2025
ef2a45f
Merge pull request #255 from mosu-dev/refactor/enhanced-cron
polyglot-k Aug 8, 2025
7c3b84a
feat: add LuaScriptsFunctionalRegistrar to ApplicationContextInitiali…
polyglot-k Aug 8, 2025
546d6ce
feat: rename Lua script files for clarity and consistency
polyglot-k Aug 8, 2025
9d6003d
feat: add AtomicOperatorAutoRegistrar for dynamic registration of Cac…
polyglot-k Aug 8, 2025
6c4dcd2
feat: add LuaScriptsFunctionalRegistrar for dynamic loading of Lua sc…
polyglot-k Aug 8, 2025
8db0333
feat: refactor AtomicExamQuota operators to use dynamic Lua script lo…
polyglot-k Aug 8, 2025
7c0d2b7
feat: add @Lazy annotation to examQuotaCacheAtomicOperatorMap injecti…
polyglot-k Aug 8, 2025
fb7a598
Merge branch 'develop' of https://github.com/mosu-dev/mosu-server int…
polyglot-k Aug 8, 2025
e5076ec
chore: add .gitkeep files to maintain empty directory structure
polyglot-k Aug 8, 2025
211511b
feat: ensure Redis scripts for quota operations are not null
polyglot-k Aug 8, 2025
1daba52
fix: correct index calculation for script path in LuaScriptsFunctiona…
polyglot-k Aug 8, 2025
4233ed8
Merge pull request #260 from mosu-dev/refactor/enhanced-redis
polyglot-k Aug 8, 2025
795a6e9
chore: dev.mosuedu.com:3000 cors 설정 추가
toothlessdev Aug 9, 2025
6ea148a
feat: update self-deploy workflow to include JDK 21 setup and Docker …
polyglot-k Aug 9, 2025
9e44526
MOSU fix: 회원가입 정보 추가
wlgns12370 Aug 9, 2025
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
42 changes: 41 additions & 1 deletion .github/workflows/self-depoly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,52 @@ name: Docker CI/CD - Deploy
on:
workflow_dispatch:
branches:
- test
- develop
jobs:
deploy:
runs-on: self-hosted

steps:
- name: Checkout source
uses: actions/checkout@v3

- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'

- name: Cache Gradle files
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-

- name: Clone external repo with jar into libs/
run: |
mkdir -p libs
git clone https://x-access-token:${{ secrets.GH_PAT }}@github.com/mosu-dev/mosu-kmc-jar.git temp-jar
cp temp-jar/*.jar libs/

- name: Build with Gradle
run: ./gradlew build -x test

- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build Docker image
run: docker build -t kangtaehyun1107/mosu-server:${{ github.sha }} .
working-directory:
- name: Push Docker image
run: docker push kangtaehyun1107/mosu-server:${{ github.sha }}

- name: Deploy via SSH
run: |
cd ~/mosu-server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import java.time.LocalDateTime;
import life.mosu.mosuserver.domain.application.repository.ApplicationFailureLogJpaRepository;
import life.mosu.mosuserver.global.support.LogCleanup;
import life.mosu.mosuserver.global.support.cron.LogCleanupExecutor;
import lombok.RequiredArgsConstructor;
import org.quartz.DisallowConcurrentExecution;
import org.springframework.stereotype.Component;

@DisallowConcurrentExecution
@Component
@RequiredArgsConstructor
public class ApplicationFailureLogCleanup implements LogCleanup {
public class ApplicationFailureLogCleanupExecutor implements LogCleanupExecutor {

private final ApplicationFailureLogJpaRepository applicationFailureLogJpaRepository;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@
import life.mosu.mosuserver.domain.application.entity.ApplicationJpaEntity;
import life.mosu.mosuserver.domain.application.repository.ApplicationFailureLogJpaRepository;
import life.mosu.mosuserver.domain.application.repository.ApplicationJpaRepository;
import life.mosu.mosuserver.global.support.DomainArchiver;
import life.mosu.mosuserver.global.support.cron.DomainArchiveExecutor;
import life.mosu.mosuserver.infra.cron.annotation.CronTarget;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@DisallowConcurrentExecution
@Component
@CronTarget
@RequiredArgsConstructor
public class ApplicationFailureLogDomainArchiver implements DomainArchiver {
public class ApplicationFailureLogDomainArchiveExecutor implements DomainArchiveExecutor {

private final static Duration DURATION_HOURS_STANDARD = Duration.ofHours(1);
private final static int BATCH_SIZE = 500;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package life.mosu.mosuserver.application.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import life.mosu.mosuserver.domain.caffeine.dto.BlockedIpHistory;
import life.mosu.mosuserver.domain.caffeine.entity.BlockedIpHistoryLogJpaEntity;
import life.mosu.mosuserver.domain.caffeine.repository.BlockedIpHistoryLogJpaJpaRepository;
import life.mosu.mosuserver.global.filter.TimePenalty;
import life.mosu.mosuserver.global.support.cron.DomainArchiveExecutor;
import life.mosu.mosuserver.infra.cron.annotation.CronTarget;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@CronTarget
@RequiredArgsConstructor
public class BlockedIpBatchAchiever implements DomainArchiveExecutor {

private final static Duration DURATION_HOURS_STANDARD = Duration.ofHours(1);
private final static int BATCH_SIZE = 500;

private final BlockedIpHistoryLogJpaJpaRepository blockedIpHistoryLogJpaRepository;
private final Cache<String, BlockedIpHistory> blockedHistoryCache;


@Override
public void archive() {
Map<String, BlockedIpHistory> blockedHistoryMap = blockedHistoryCache.asMap();

List<BlockedIpHistoryLogJpaEntity> logs = blockedHistoryMap.values().stream()
.filter(entry -> entry.getPenaltyLevel() == TimePenalty.LEVEL_5)
.map(this::createBlockedHistoryLog)
.toList();

if (logs.isEmpty()) {
log.debug("[BlockedIpArchiver] 저장할 로그가 없음.");
return;
}

for (int i = 0; i < logs.size(); i += BATCH_SIZE) {
int end = Math.min(i + BATCH_SIZE, logs.size());
List<BlockedIpHistoryLogJpaEntity> batch = logs.subList(i, end);

try {
blockedIpHistoryLogJpaRepository.saveAllUsingBatch(batch);
log.debug("[BlockedIpArchiver] 저장 완료: {}개", batch.size());
} catch (Exception e) {
log.error("[BlockedIpArchiver] 저장 실패: {}~{} 인덱스", i, end, e);
}
}
}
Comment on lines +29 to +53

Choose a reason for hiding this comment

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

medium

The current implementation of the archive method collects all matching log entries into a list before processing them in batches. If the blockedHistoryCache contains a very large number of entries, this could lead to high memory consumption.

A more memory-efficient approach would be to iterate through the cache entries and build batches on the fly, without creating a large intermediate list.

    @Override
    public void archive() {
        List<BlockedIpHistoryLogJpaEntity> batch = new ArrayList<>(BATCH_SIZE);
        int savedCount = 0;

        for (BlockedIpHistory history : blockedHistoryCache.asMap().values()) {
            if (history.getPenaltyLevel() == TimePenalty.LEVEL_5) {
                batch.add(createBlockedHistoryLog(history));
                if (batch.size() >= BATCH_SIZE) {
                    try {
                        blockedIpHistoryLogJpaRepository.saveAllUsingBatch(batch);
                        log.debug("[BlockedIpArchiver] 저장 완료: {}개", batch.size());
                        savedCount += batch.size();
                    } catch (Exception e) {
                        log.error("[BlockedIpArchiver] 저장 실패. Batch size: {}", batch.size(), e);
                    }
                    batch.clear();
                }
            }
        }

        if (!batch.isEmpty()) {
            try {
                blockedIpHistoryLogJpaRepository.saveAllUsingBatch(batch);
                log.debug("[BlockedIpArchiver] 저장 완료: {}개", batch.size());
                savedCount += batch.size();
            } catch (Exception e) {
                log.error("[BlockedIpArchiver] 저장 실패. Batch size: {}", batch.size(), e);
            }
        }

        if (savedCount == 0) {
            log.debug("[BlockedIpArchiver] 저장할 로그가 없음.");
        }
    }


@Override
public String getName() {
return "blocked-ip";
}

private BlockedIpHistoryLogJpaEntity createBlockedHistoryLog(
BlockedIpHistory blockedIpHistory) {
return new BlockedIpHistoryLogJpaEntity(
blockedIpHistory.getIp(),
blockedIpHistory.getPenaltyLevel(),
blockedIpHistory.getBlockedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package life.mosu.mosuserver.application.exam.cache;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator;
Expand All @@ -15,12 +17,17 @@ public class AtomicExamQuotaDecrementOperator implements VoidCacheAtomicOperator
private final RedisTemplate<String, Long> redisTemplate;
private final DefaultRedisScript<Long> decrementScript;


public AtomicExamQuotaDecrementOperator(
RedisTemplate<String, Long> redisTemplate,
@Qualifier("decrementExamQuotaScript") DefaultRedisScript<Long> decrementScript

@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Qualifier("examLuaScripts")
Map<String, DefaultRedisScript<Long>> examLuaScripts
) {
this.redisTemplate = redisTemplate;
this.decrementScript = decrementScript;
this.decrementScript = Objects.requireNonNull(examLuaScripts.get("decrementQuota"),
"Redis script 'decrementQuota' not found");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package life.mosu.mosuserver.application.exam.cache;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class AtomicExamQuotaIncrementOperator implements VoidCacheAtomicOperator<String, Long> {

private final RedisTemplate<String, Long> redisTemplate;
private final DefaultRedisScript<Long> decrementScript;
private final DefaultRedisScript<Long> incrementScript;

public AtomicExamQuotaIncrementOperator(
RedisTemplate<String, Long> redisTemplate,
@Qualifier("incrementExamQuotaScript") DefaultRedisScript<Long> decrementScript

@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Qualifier("examLuaScripts")
Map<String, DefaultRedisScript<Long>> examLuaScripts
) {
this.redisTemplate = redisTemplate;
this.decrementScript = decrementScript;
this.incrementScript = Objects.requireNonNull(examLuaScripts.get("incrementQuota"),
"Redis script 'incrementQuota' not found");
}

@Override
Expand All @@ -36,7 +44,7 @@ public String getActionName() {
@Override
public void execute(String key) {
try {
Long result = redisTemplate.execute(decrementScript, List.of(
Long result = redisTemplate.execute(incrementScript, List.of(
ExamQuotaPrefix.CURRENT_APPLICATIONS.with(key),
ExamQuotaPrefix.MAX_CAPACITY.with(key)
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -29,7 +30,9 @@ public ExamQuotaCacheManager(
CacheWriter<String, Long> cacheWriter,
CacheReader<String, Long> cacheReader,

@Qualifier("examCacheAtomicOperatorMap")
@Lazy
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Qualifier("examQuotaCacheAtomicOperatorMap")
Map<String, ? extends CacheAtomicOperator<String, Long>> cacheAtomicOperatorMap,
ExamJpaRepository examJpaRepository
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ public List<InquiryDetailResponse.AttachmentDetailResponse> toAttachmentResponse
.toList();
}

@Override
public void updateAttachment(
List<FileRequest> requests,
InquiryAnswerJpaEntity answerEntity
) {
deleteAttachment(answerEntity);
createAttachment(requests, answerEntity);
}

private InquiryDetailResponse.AttachmentResponse createAttachResponse(
InquiryAnswerAttachmentEntity attachment) {
String preSignedUrl = s3Service.getPreSignedUrl(attachment.getS3Key());
Expand All @@ -74,4 +83,5 @@ private InquiryDetailResponse.AttachmentDetailResponse createAttachDetailRespons
);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository;
import life.mosu.mosuserver.domain.inquiryAnswer.entity.InquiryAnswerJpaEntity;
import life.mosu.mosuserver.domain.inquiryAnswer.repository.InquiryAnswerJpaRepository;
import life.mosu.mosuserver.domain.user.entity.UserJpaEntity;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest;
Expand All @@ -28,14 +29,14 @@ public class InquiryAnswerService {
private final InquiryAnswerTxService eventTxService;

@Transactional
public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) {
public void createInquiryAnswer(Long postId, InquiryAnswerRequest request, UserJpaEntity user) {
isAnswerAlreadyRegister(postId);
InquiryJpaEntity inquiryEntity = getInquiry(postId);
Long userId = inquiryEntity.getUserId();

try {
InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.save(
request.toEntity(postId));
request.toEntity(postId, user));

answerAttachmentService.createAttachment(request.attachments(), answerEntity);
inquiryEntity.updateStatusToComplete();
Expand All @@ -51,12 +52,12 @@ public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) {
@Transactional
public void deleteInquiryAnswer(Long postId) {
InquiryJpaEntity inquiryEntity = getInquiry(postId);

InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.findByInquiryId(postId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_NOT_FOUND));

inquiryAnswerJpaRepository.delete(answerEntity);
inquiryEntity.updateStatusToPending();
answerAttachmentService.deleteAttachment(answerEntity);
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
Expand All @@ -71,17 +72,14 @@ public InquiryDetailResponse.InquiryAnswerDetailResponse getInquiryAnswerDetail(
}

@Transactional
public void updateInquiryAnswer(Long postId, InquiryAnswerUpdateRequest request) {
InquiryJpaEntity inquiryEntity = getInquiry(postId);

public void updateInquiryAnswer(Long postId, InquiryAnswerUpdateRequest request, UserJpaEntity user) {
InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.findByInquiryId(postId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_NOT_FOUND));

answerEntity.update(request.title(), request.content());
answerEntity.update(request.title(), request.content(), user.getName());
inquiryAnswerJpaRepository.save(answerEntity);

answerAttachmentService.deleteAttachment(answerEntity);
answerAttachmentService.createAttachment(request.attachments(), answerEntity);
answerAttachmentService.updateAttachment(request.attachments(), answerEntity);
}

private InquiryJpaEntity getInquiry(Long postId) {
Expand All @@ -96,4 +94,6 @@ private void isAnswerAlreadyRegister(Long postId) {
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public void deleteAttachment(InquiryJpaEntity entity) {
inquiryAttachmentJpaRepository.deleteAll(attachments);
}

@Override
public void updateAttachment(
List<FileRequest> requests,
InquiryJpaEntity inquiryEntity
) {
deleteAttachment(inquiryEntity);
createAttachment(requests, inquiryEntity);
}


public List<InquiryDetailResponse.AttachmentDetailResponse> toAttachmentResponses(
InquiryJpaEntity inquiry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,9 @@ public void deleteInquiry(UserJpaEntity user, Long postId) {
InquiryJpaEntity inquiry = getInquiry(postId);
hasPermission(inquiry.getUserId(), user);

inquiryAnswerJpaRepository.findByInquiryId(postId).ifPresent(answer -> {
inquiryAnswerService.deleteInquiryAnswer(postId);
});
inquiryAnswerService.deleteInquiryAnswer(postId);

Choose a reason for hiding this comment

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

high

When deleting an inquiry, it's possible that it doesn't have an answer. The previous implementation handled this case gracefully by using ifPresent. The new implementation directly calls inquiryAnswerService.deleteInquiryAnswer(postId), which will throw an INQUIRY_ANSWER_NOT_FOUND exception if no answer exists. This could prevent inquiries without answers from being deleted.

Suggested change
inquiryAnswerService.deleteInquiryAnswer(postId);
inquiryAnswerJpaRepository.findByInquiryId(postId).ifPresent(answer -> inquiryAnswerService.deleteInquiryAnswer(postId));


// inquiryAttachmentService.deleteAttachment(inquiry);
inquiryAttachmentService.deleteAttachment(inquiry);
inquiryJpaRepository.delete(inquiry);
}

Expand Down
Loading
Loading