diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml index b8d83337..7a9c7047 100644 --- a/.github/workflows/deploy_dev.yml +++ b/.github/workflows/deploy_dev.yml @@ -20,7 +20,7 @@ jobs: run: | rm -f ./src/main/resources/application-secret.yml touch ./src/main/resources/application-secret.yml - echo "${{ secrets.APPLICATION_SECRET_DEV }}" > ./src/main/resources/application-secret.yml + echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.yml # JDK version 설정 - name: Set up JDK 17 @@ -62,10 +62,10 @@ jobs: uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST_DEV }} - username: ec2-user # ubuntu 에서 변경 + username: ubuntu # ubuntu 에서 변경 key: ${{ secrets.EC2_KEY_DEV }} script: | - cd /home/ec2-user/ceos + cd /home/ubuntu/ceos sudo touch .env echo "${{ secrets.ENV_DEV }}" | sudo tee .env > /dev/null @@ -76,7 +76,7 @@ jobs: sudo docker rm -f $(docker ps -qa) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/ceos-backend-dev sudo docker pull ${{ secrets.DOCKER_USERNAME }}/ceos-nginx-dev - docker-compose -f docker-compose.dev.yml --env-file ./.env up -d + docker compose -f docker-compose.dev.yml --env-file .env up -d docker image prune -f diff --git a/build.gradle b/build.gradle index b32de78d..384f7a64 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { // Apache POI implementation 'org.apache.poi:poi:4.1.2' implementation 'org.apache.poi:poi-ooxml:4.1.2' + + // actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3704f97f..4bda5db0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,6 +9,8 @@ services: - .env expose: - "8080" + depends_on: + - mysql nginx: image: ceos17/ceos-nginx-dev @@ -18,9 +20,32 @@ services: ports: - "80:80" + mysql: + image: mysql:latest + container_name: mysql + restart: always + environment: + MYSQL_DATABASE: ceos-dev + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + TZ: Asia/Seoul + ports: + - "3306:3306" + command: + - "mysqld" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + volumes: + - ./data:/var/lib/mysql + + + + redis: image: redis:latest container_name: redis hostname: redis ports: - - "6379:6379" \ No newline at end of file + - "6379:6379" + diff --git a/src/main/java/ceos/backend/domain/application/helper/ApplicationHelper.java b/src/main/java/ceos/backend/domain/application/helper/ApplicationHelper.java index 34ed8047..5c319b01 100644 --- a/src/main/java/ceos/backend/domain/application/helper/ApplicationHelper.java +++ b/src/main/java/ceos/backend/domain/application/helper/ApplicationHelper.java @@ -57,6 +57,15 @@ public Application getApplicationById(Long id) { }); } + public Application getApplicationByIdForUpdate(Long id) { + return applicationRepository + .findByIdWithPessimisticLock(id) + .orElseThrow( + () -> { + throw ApplicantNotFound.EXCEPTION; + }); + } + public Application getApplicationByUuidAndEmail(String uuid, String email) { return applicationRepository .findByUuidAndEmail(uuid, email) diff --git a/src/main/java/ceos/backend/domain/application/repository/ApplicationRepository.java b/src/main/java/ceos/backend/domain/application/repository/ApplicationRepository.java index 18a0b86f..c1f75446 100644 --- a/src/main/java/ceos/backend/domain/application/repository/ApplicationRepository.java +++ b/src/main/java/ceos/backend/domain/application/repository/ApplicationRepository.java @@ -5,9 +5,12 @@ import ceos.backend.domain.application.domain.Pass; import ceos.backend.global.common.entity.Part; import java.util.Optional; + +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -63,4 +66,10 @@ Page findAllByPartAndDocumentPassAndFinalPass( @Param("convertedDocPass") Pass convertedDocPass, @Param("convertedFinalPass") Pass convertedFinalPass, PageRequest pageRequest); + + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select a from Application a where a.id = :id") + Optional findByIdWithPessimisticLock(@Param("id") Long id); + } diff --git a/src/main/java/ceos/backend/domain/application/service/ApplicationService.java b/src/main/java/ceos/backend/domain/application/service/ApplicationService.java index 5d421d7e..369cffa4 100644 --- a/src/main/java/ceos/backend/domain/application/service/ApplicationService.java +++ b/src/main/java/ceos/backend/domain/application/service/ApplicationService.java @@ -28,6 +28,7 @@ import ceos.backend.domain.recruitment.domain.Recruitment; import ceos.backend.domain.recruitment.helper.RecruitmentHelper; import ceos.backend.domain.recruitment.validator.RecruitmentValidator; +import ceos.backend.global.common.annotation.TransactionLog; import ceos.backend.global.common.dto.PageInfo; import ceos.backend.global.common.entity.Part; import ceos.backend.global.util.InterviewDateTimeConvertor; @@ -237,7 +238,7 @@ public GetInterviewTime getInterviewTime(Long applicationId) { public void updateInterviewTime(Long applicationId, UpdateInterviewTime updateInterviewTime) { recruitmentValidator.validateBetweenStartDateDocAndResultDateDoc(); // 기간 검증 applicationValidator.validateExistingApplicant(applicationId); // 유저 검증 - final Application application = applicationHelper.getApplicationById(applicationId); + final Application application = applicationHelper.getApplicationByIdForUpdate(applicationId); applicationValidator.validateDocumentPassStatus(application); // 서류 통과 검증 final List interviews = interviewRepository.findAll(); final String duration = @@ -258,19 +259,21 @@ public GetInterviewAvailability getInterviewAvailability(Long applicationId) { @Transactional + @TransactionLog public void updateDocumentPassStatus(Long applicationId, UpdatePassStatus updatePassStatus) { recruitmentValidator.validateBetweenStartDateDocAndResultDateDoc(); // 기간 검증 applicationValidator.validateExistingApplicant(applicationId); // 유저 검증 - final Application application = applicationHelper.getApplicationById(applicationId); + final Application application = applicationHelper.getApplicationByIdForUpdate(applicationId); application.updateDocumentPass(updatePassStatus.getPass()); } @Transactional + @TransactionLog public void updateFinalPassStatus(Long applicationId, UpdatePassStatus updatePassStatus) { recruitmentValidator.validateBetweenResultDateDocAndResultDateFinal(); // 기간 검증 applicationValidator.validateExistingApplicant(applicationId); // 유저 검증 - final Application application = applicationHelper.getApplicationById(applicationId); + final Application application = applicationHelper.getApplicationByIdForUpdate(applicationId); applicationValidator.validateDocumentPassStatus(application); // 서류 통과 검증 application.updateFinalPass(updatePassStatus.getPass()); diff --git a/src/main/java/ceos/backend/domain/recruitment/domain/Recruitment.java b/src/main/java/ceos/backend/domain/recruitment/domain/Recruitment.java index 69e3436d..3f95c6f9 100644 --- a/src/main/java/ceos/backend/domain/recruitment/domain/Recruitment.java +++ b/src/main/java/ceos/backend/domain/recruitment/domain/Recruitment.java @@ -55,6 +55,10 @@ public class Recruitment extends BaseEntity { @NotNull private LocalDate demodayDate; + private LocalDate startMTDate; + + private LocalDate endMTDate; + private LocalDateTime applicationExcelCreatedAt; // 생성자 @@ -75,6 +79,8 @@ private Recruitment( LocalDate ideathonDate, LocalDate hackathonDate, LocalDate demodayDate, + LocalDate startMTDate, + LocalDate endMTDate, LocalDateTime applicationExcelCreatedAt) { this.generation = generation; this.prodStudyUrl = prodStudyUrl; @@ -91,6 +97,8 @@ private Recruitment( this.ideathonDate = ideathonDate; this.hackathonDate = hackathonDate; this.demodayDate = demodayDate; + this.startMTDate = startMTDate; + this.endMTDate = endMTDate; this.applicationExcelCreatedAt = applicationExcelCreatedAt; } @@ -110,6 +118,8 @@ public void updateRecruitment(RecruitmentDTO recruitmentDTO) { this.ideathonDate = recruitmentDTO.getIdeathonDate(); this.hackathonDate = recruitmentDTO.getHackathonDate(); this.demodayDate = recruitmentDTO.getDemodayDate(); + this.startMTDate = recruitmentDTO.getStartMTDate(); + this.endMTDate = recruitmentDTO.getEndMTDate(); } public void updateApplicationExcelCreatedAt(LocalDateTime createdAt) { diff --git a/src/main/java/ceos/backend/domain/recruitment/dto/RecruitmentDTO.java b/src/main/java/ceos/backend/domain/recruitment/dto/RecruitmentDTO.java index c177e48c..29071bcb 100644 --- a/src/main/java/ceos/backend/domain/recruitment/dto/RecruitmentDTO.java +++ b/src/main/java/ceos/backend/domain/recruitment/dto/RecruitmentDTO.java @@ -24,6 +24,8 @@ public class RecruitmentDTO { private LocalDate ideathonDate; private LocalDate hackathonDate; private LocalDate demodayDate; + private LocalDate startMTDate; + private LocalDate endMTDate; @Builder public RecruitmentDTO( @@ -41,7 +43,9 @@ public RecruitmentDTO( LocalDate otDate, LocalDate ideathonDate, LocalDate hackathonDate, - LocalDate demodayDate) { + LocalDate demodayDate, + LocalDate startMTDate, + LocalDate endMTDate) { this.generation = generation; this.prodStudyUrl = prodStudyUrl; this.designStudyUrl = designStudyUrl; @@ -57,6 +61,8 @@ public RecruitmentDTO( this.ideathonDate = ideathonDate; this.hackathonDate = hackathonDate; this.demodayDate = demodayDate; + this.startMTDate = startMTDate; + this.endMTDate = endMTDate; } public static RecruitmentDTO from(Recruitment recruitment) { @@ -76,6 +82,8 @@ public static RecruitmentDTO from(Recruitment recruitment) { .ideathonDate(recruitment.getIdeathonDate()) .hackathonDate(recruitment.getHackathonDate()) .demodayDate(recruitment.getDemodayDate()) + .startMTDate(recruitment.getStartMTDate()) + .endMTDate(recruitment.getEndMTDate()) .build(); } } diff --git a/src/main/java/ceos/backend/domain/recruitment/dto/UserRecruitmentDTO.java b/src/main/java/ceos/backend/domain/recruitment/dto/UserRecruitmentDTO.java index d13b1a6d..ac2e9fae 100644 --- a/src/main/java/ceos/backend/domain/recruitment/dto/UserRecruitmentDTO.java +++ b/src/main/java/ceos/backend/domain/recruitment/dto/UserRecruitmentDTO.java @@ -23,6 +23,8 @@ public class UserRecruitmentDTO { private LocalDate ideathonDate; private LocalDate hackathonDate; private LocalDate demodayDate; + private LocalDate startMTDate; + private LocalDate endMTDate; @Builder public UserRecruitmentDTO( @@ -39,7 +41,9 @@ public UserRecruitmentDTO( LocalDate otDate, LocalDate ideathonDate, LocalDate hackathonDate, - LocalDate demodayDate) { + LocalDate demodayDate, + LocalDate startMTDate, + LocalDate endMTDate) { this.generation = generation; this.prodStudyUrl = prodStudyUrl; this.designStudyUrl = designStudyUrl; @@ -54,6 +58,8 @@ public UserRecruitmentDTO( this.ideathonDate = ideathonDate; this.hackathonDate = hackathonDate; this.demodayDate = demodayDate; + this.startMTDate = startMTDate; + this.endMTDate = endMTDate; } public static UserRecruitmentDTO from(Recruitment recruitment) { @@ -72,6 +78,8 @@ public static UserRecruitmentDTO from(Recruitment recruitment) { .ideathonDate(recruitment.getIdeathonDate()) .hackathonDate(recruitment.getHackathonDate()) .demodayDate(recruitment.getDemodayDate()) + .startMTDate(recruitment.getStartMTDate()) + .endMTDate(recruitment.getEndMTDate()) .build(); } } diff --git a/src/main/java/ceos/backend/global/common/annotation/TransactionLog.java b/src/main/java/ceos/backend/global/common/annotation/TransactionLog.java new file mode 100644 index 00000000..ca491348 --- /dev/null +++ b/src/main/java/ceos/backend/global/common/annotation/TransactionLog.java @@ -0,0 +1,10 @@ +package ceos.backend.global.common.annotation; + +import java.lang.annotation.*; + + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TransactionLog { +} \ No newline at end of file diff --git a/src/main/java/ceos/backend/global/common/aop/TransactionLoggingAspect.java b/src/main/java/ceos/backend/global/common/aop/TransactionLoggingAspect.java new file mode 100644 index 00000000..b3bf3c40 --- /dev/null +++ b/src/main/java/ceos/backend/global/common/aop/TransactionLoggingAspect.java @@ -0,0 +1,34 @@ +package ceos.backend.global.common.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronizationManager; + + +@Slf4j +@Aspect +@Component +public class TransactionLoggingAspect { + + @Around("@annotation(ceos.backend.global.common.annotation.TransactionLog)") + public Object logTxMethod(ProceedingJoinPoint joinPoint) throws Throwable { + String methodName = joinPoint.getSignature().toShortString(); + + log.info("[TX START] {}", methodName); + String txName = TransactionSynchronizationManager.getCurrentTransactionName(); + log.info("[TX NAME] = {}", txName); + + + try { + Object result = joinPoint.proceed(); + log.info("[TX COMMIT] {}", methodName); + return result; + } catch (Exception e) { + log.warn("[TX ROLLBACK] {} - Exception: {}", methodName, e.getMessage()); + throw e; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f7b08eec..c73da917 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,7 +21,7 @@ springdoc: default-produces-media-type: application/json swagger-ui: operations-sorter: alpha # 오름차순 - path: /swagger-ui.html + path: /swagger logging.level: org.hibernate.SQL: debug