diff --git a/.github/workflows/mentoview-be.yaml b/.github/workflows/mentoview-be.yaml new file mode 100644 index 0000000..53c6126 --- /dev/null +++ b/.github/workflows/mentoview-be.yaml @@ -0,0 +1,87 @@ +name: mentoview backend test 06 + +on: + # workflow_dispatch + push: + branches: + - test + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Make application.properties + run: | + mkdir -p ./src/main/resources + cd ./src/main/resources + touch ./application.properties + echo "${{ secrets.PROPERTIES }}" > ./application.properties + shell: bash + + - name: Grant execute permission for Gradle Wrapper + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew clean build -x test + # env: + # GRADLE_OPTS: "-Dorg.gradle.daemon=false" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: springboot-app + path: build/libs + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }} + IMAGE_TAG: be-${{ github.sha }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + cd: + runs-on: ubuntu-latest + needs: ci + + steps: + - name: Update application image version for ArgoCD + uses: actions/checkout@v4 + with: + repository: ${{ secrets.G_USER }}/${{ secrets.G_REPOSITORY }} + ref: main + token: ${{ secrets.G_TOKEN }} + + - name: Set up Image + run: | + sed -i "s%image: ${{ secrets.AWS_ECR_ID }}.dkr.ecr.${{ secrets.AWS_ECR_REGION }}.amazonaws.com/${{ secrets.AWS_ECR_REPOSITORY }}:be-[a-zA-Z0-9]*%image: ${{ secrets.AWS_ECR_ID }}.dkr.ecr.${{ secrets.AWS_ECR_REGION }}.amazonaws.com/${{ secrets.AWS_ECR_REPOSITORY }}:be-${{ github.sha }}%" ./BE/mv-be-manifest.yaml + + - name: Commit and push changes + run: | + git config --local user.email "${{ secrets.G_USER_EMAIL }}" + git config --local user.name "${{ secrets.G_USER_NAME }}" + git add . + git commit -m "Update application image version for ArgoCD" + git push diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..785d2da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# FROM openjdk:17 +# ARG JAR_FILE=build/libs/*.jar +# COPY ${JAR_FILE} app.jar +# ENTRYPOINT ["java", "-jar", "/app.jar"] + +### Tesseract와 Leptonica가 설치된 베이스 이미지 사용 +FROM joe1534/mentoview-tesseract:v3 + +### 라이브러리 경로 설정 (Tesseract와 Leptonica) +ENV TESSDATA_PREFIX=/usr/local/share +ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y tzdata && \ + ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone && \ + dpkg-reconfigure --frontend noninteractive tzdata + +# 타임존을 Asia/Seoul로 설정 +#ENV TZ=Asia/Seoul + +### 워킹 디렉토리 설정 +WORKDIR /app + +### jar 파일 app.jar로 복사 +COPY build/libs/*.jar app.jar + +### app.jar 실행 +#CMD ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] + +# 타임존을 환경 변수로 설정하여 JVM 옵션으로 전달합니다. +#ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-Duser.timezone=${TZ}", "-jar", "app.jar"] diff --git a/README.md b/README.md index d236549..0d7def6 100644 --- a/README.md +++ b/README.md @@ -1 +1,136 @@ -# MentoView-BE \ No newline at end of file +# 🧠 Mentoview - AI 면접 서비스 +> "이력서 기반으로 AI가 면접 질문을 생성하고, 응답을 분석하여 피드백을 제공합니다." + +## 📌프로젝트 개요 (Overview) +> 사용자가 이력서를 제출하면 AI가 면접을 진행하고, 응답 분석 결과를 피드백으로 제공합니다. + +## 💻기술 스택 (Tech Stack) +- Backend: Java 17, Spring Boot 3, Spring Security 6, Spring Data JPA, JPA, OpenAI, PDFBOX, Tesseract OCR, SWAGGER, Spring Reactive +- Frontend : React, JS ES6 , HTML5, CSS3 ,Redux, React-Query, Styled-Components +- DB: MySQL 8 +- Infra: AWS EKS, S3, Lambda, Docker, Prometheus, Grafana +- CICD : GitHub Actions, ArgoCD + + +## ⚙️기능 소개 (Features) +- 회원가입 (폼 & OAuth2) +- 이력서 등록 및 관리 +- AI 면접 질문 자동 생성 +- 응답 분석 및 피드백 제공 +- 정기 구독 및 결제 +- 모니터링 + +## ☁️배포 (Deployment) +- GitHub Actions CI/CD +- ArgoCD +- AWS CloudFormation(AWS EKS) + +## dependency +- joe1534/mentoview-tesseract:v3 +- S3 + +## 모니터링 / 성능 (Optional) +- Prometheus +- Grafana +- Alert(discord) + +## 💻 개발 환경 (Development Environment) + +- Java 17 (Temurin) +- Spring Boot 3.4.3 +- Gradle 8.12.1 +- MySQL 8.0.40 +- transcribe 2.30.26 +- openapi +- awssdk V2 +- s3:3.0.2 +- pdfbox 3.0.3 +- Tesseract 5.5.0 +- Leptonica 1.85.0 +- portone server-sdk 0.15.0 +- webflux +- prometheus +- jjwt jjwt-api:0.12.3/ jjwt-impl:0.12.3/ jjwt-jackson:0.12.3 +- spring security 6 + +## 👩‍💻팀원 / 역할 (Optional) +- 이승연 / 팀장 (linkpond0629@gmail.com) +- 박청조 / 팀원 (king01286@naver.com) +- 배희창 / 팀원 (madsens0527@gmail.com) +- 이서영 / 팀원 (7910trio@naver.com) +- 이사야 / 팀원 (syjoy1993@gmail.com) + +### FrontEnd +> https://github.com/linkpond/MentoView-FE +### manifast +> https://github.com/linkpond/MentoView-manifest + +### application.properties + +``` +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url= +spring.datasource.username= +spring.datasource.password= + +# JPA +spring.jpa.generate-ddl=false +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect + +# Actuator + +management.endpoint.prometheus.access=unrestricted +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=health,info,prometheus,metrics +management.server.port=9090 +management.endpoints.web.base-path=/api/management + +#prometheus + +# aws +cloud.aws.credentials.access-key= +cloud.aws.credentials.secret-key= +cloud.aws.s3.bucket.name= +cloud.aws.region.static=ap-northeast-2 +aws.lambda.header= +aws.lambda.secret-key= + +spring.servlet.multipart.max-file-size=20MB +spring.servlet.multipart.max-request-size=20MB + +# OAuth +spring.jwt.secret= +temporary-token-expiration: 120000 +access-token-expiration: 3000000 +refresh-token-expiration: 604800000 + +## +spring.task.scheduling.enabled=true + +### registration +spring.security.oauth2.client.registration.google.client-name=google +spring.security.oauth2.client.registration.google.client-id +spring.security.oauth2.client.registration.google.client-secret + +openai +spring.ai.openai.api-key= +spring.ai.openai.chat.options.model=gpt-4o-mini + +portone +IMP_API_KEY= +PORTONE_WEBHOOK_SECRET= +NOTIFICATION_URL=http://?/api/webhook/payment + +# tesseract +tesseract.tessdata.path=/usr/local/share/tessdata + +#swagger +#springdoc.api-docs.path=/api/v3/api-docs springdoc.swagger-ui.path=/api/swagger-ui +#springdoc.api-docs.path=/v3/api-docs springdoc.swagger-ui.path=/swagger-ui +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.api-docs.path=/v3/api-docs + +``` \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/MentoviewApplication.java b/src/main/java/ce2team1/mentoview/MentoviewApplication.java index 8214080..9d9af1c 100644 --- a/src/main/java/ce2team1/mentoview/MentoviewApplication.java +++ b/src/main/java/ce2team1/mentoview/MentoviewApplication.java @@ -2,8 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; - +@EnableScheduling @SpringBootApplication public class MentoviewApplication { diff --git a/src/main/java/ce2team1/mentoview/archive/dto/InterviewArchiveDto.java b/src/main/java/ce2team1/mentoview/archive/dto/InterviewArchiveDto.java new file mode 100644 index 0000000..b4ac77b --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/dto/InterviewArchiveDto.java @@ -0,0 +1,33 @@ +package ce2team1.mentoview.archive.dto; + +import ce2team1.mentoview.archive.entity.InterviewArchive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +/** + * DTO for {@link ce2team1.mentoview.archive.entity.InterviewArchive} + */ +@AllArgsConstructor +@Getter +@Builder +public class InterviewArchiveDto { + private final Long userId; + private final String interviewKey; + + + public static InterviewArchiveDto of(Long userId, String interviewKey){ + return new InterviewArchiveDto( + userId, + interviewKey + ); + } + + public static InterviewArchiveDto by(InterviewArchive interviewArchive){ + return InterviewArchiveDto.builder() + .userId(interviewArchive.getUserId()) + .interviewKey(interviewArchive.getInterviewKey()) + .build(); + + } +} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/archive/dto/InterviewData.java b/src/main/java/ce2team1/mentoview/archive/dto/InterviewData.java new file mode 100644 index 0000000..8b0c8e1 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/dto/InterviewData.java @@ -0,0 +1,24 @@ +package ce2team1.mentoview.archive.dto; + +import lombok.*; + +import java.util.List; +import java.util.UUID; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class InterviewData { + + private String interviewId; + + private List interviewEntries; + + public static InterviewData of(List entries) { + return InterviewData.builder() + .interviewId(UUID.randomUUID().toString()) + .interviewEntries(entries) + .build(); + } +} diff --git a/src/main/java/ce2team1/mentoview/archive/dto/InterviewEntry.java b/src/main/java/ce2team1/mentoview/archive/dto/InterviewEntry.java new file mode 100644 index 0000000..6500630 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/dto/InterviewEntry.java @@ -0,0 +1,31 @@ +package ce2team1.mentoview.archive.dto; + +import ce2team1.mentoview.entity.InterviewQuestion; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class InterviewEntry { + + private Long interviewId; // 인터뷰 id + private int questionNumber; // 번호 + + private String question; // 질문 내용 + private String response; // 응답 내용 + private String feedback; // 피드백 내용 + + public static InterviewEntry of(InterviewQuestion question) { + return InterviewEntry.builder() + .interviewId(question.getInterview().getInterviewId()) + .questionNumber(question.getQuestionId().intValue()) + .question(question.getQuestion()) + .response(question.getInterviewResponse()!= null? question.getInterviewResponse().getResponse() : "") + .feedback(question.getInterviewFeedback()!= null? question.getInterviewFeedback().getFeedback() : "") + .build(); + } +} diff --git a/src/main/java/ce2team1/mentoview/archive/entity/ArchiveBase.java b/src/main/java/ce2team1/mentoview/archive/entity/ArchiveBase.java new file mode 100644 index 0000000..b335fb3 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/entity/ArchiveBase.java @@ -0,0 +1,20 @@ +package ce2team1.mentoview.archive.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; + +import java.time.LocalDateTime; + +@MappedSuperclass +public class ArchiveBase { + + @Column(nullable = false) + protected LocalDateTime archivedAt; // 아카이브된 시간 + + @PrePersist + protected void prePersistArchivedAt(){ + this.archivedAt = LocalDateTime.now(); + } + +} diff --git a/src/main/java/ce2team1/mentoview/archive/entity/InterviewArchive.java b/src/main/java/ce2team1/mentoview/archive/entity/InterviewArchive.java new file mode 100644 index 0000000..1d6d8fc --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/entity/InterviewArchive.java @@ -0,0 +1,41 @@ +package ce2team1.mentoview.archive.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + + +@Builder +@NoArgsConstructor +@Getter +@AllArgsConstructor +@Entity +@ToString +public class InterviewArchive { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long userId; + + @Column(length = 255, nullable = false) + private String interviewKey; // S3 키 값 + + @Column(nullable = false) + private LocalDateTime archivedAt; // 아카이브된 시간 + + + public static InterviewArchive of(Long userId, String interviewKey) { + return InterviewArchive.builder() + .userId(userId) + .interviewKey(interviewKey) + .archivedAt(LocalDateTime.now()) + .build(); + } +} + + + diff --git a/src/main/java/ce2team1/mentoview/archive/entity/PaymentArchive.java b/src/main/java/ce2team1/mentoview/archive/entity/PaymentArchive.java new file mode 100644 index 0000000..f1de78c --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/entity/PaymentArchive.java @@ -0,0 +1,48 @@ +package ce2team1.mentoview.archive.entity; + +import ce2team1.mentoview.entity.atrribute.PaymentStatus; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +public class PaymentArchive { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private BigDecimal amount; + + private String approvalCode; // 승인번호 + + @Enumerated(EnumType.STRING) + private PaymentStatus status; + + private String subscription; + + @Column(name = "payment_date") + private LocalDateTime paymentDate; + + @Column(nullable = false) + protected LocalDateTime archivedAt; // 아카이브된 시간 + + public static PaymentArchive of(Long userId, BigDecimal amount, String approvalCode, String subscription, PaymentStatus status, LocalDateTime paymentDate) { + return PaymentArchive.builder() + .userId(userId) + .amount(amount) + .approvalCode(approvalCode) + .subscription(subscription) + .status(status) + .paymentDate(paymentDate) + .archivedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/ce2team1/mentoview/archive/entity/UserArchive.java b/src/main/java/ce2team1/mentoview/archive/entity/UserArchive.java new file mode 100644 index 0000000..e5d90e1 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/entity/UserArchive.java @@ -0,0 +1,61 @@ +package ce2team1.mentoview.archive.entity; + +import ce2team1.mentoview.entity.atrribute.Role; +import ce2team1.mentoview.entity.atrribute.UserStatus; +import ce2team1.mentoview.utils.jpaconverter.UserRoleConverter; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +/* +* UserStatus가 DORMANT, DELETED 인 유저를 관리한다 +*/ + +@Entity +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserArchive { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private Long userId; // 기존식별자 + + @Column(unique = true, nullable = false) + private String email; // 기존 ID + + @Convert(converter = UserRoleConverter.class) + private Role role; // 기존역할 + + @Enumerated(EnumType.STRING) + private UserStatus status; // 상태 , DORMANT, DELETED만허용 + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime expirationDate; + + @Column(columnDefinition = "TEXT") + private String additionalData; // JSON 형식, 추가 데이터 + + @Column(nullable = false) + protected LocalDateTime archivedAt; // 아카이브된 시간 + + public static UserArchive of(Long userId, String email, Role role, UserStatus status, LocalDateTime createdAt, String additionalData) { + return UserArchive.builder() + .userId(userId) + .email(email) + .role(role) + .status(status) + .createdAt(createdAt) + .expirationDate(LocalDateTime.now().plusYears(1)) + .additionalData(additionalData) + .archivedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/ce2team1/mentoview/archive/repository/InterviewArchiveRepository.java b/src/main/java/ce2team1/mentoview/archive/repository/InterviewArchiveRepository.java new file mode 100644 index 0000000..5a59a65 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/repository/InterviewArchiveRepository.java @@ -0,0 +1,8 @@ +package ce2team1.mentoview.archive.repository; + +import ce2team1.mentoview.archive.entity.InterviewArchive; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterviewArchiveRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/archive/repository/PaymentArchiveRepository.java b/src/main/java/ce2team1/mentoview/archive/repository/PaymentArchiveRepository.java new file mode 100644 index 0000000..b5c0132 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/repository/PaymentArchiveRepository.java @@ -0,0 +1,7 @@ +package ce2team1.mentoview.archive.repository; + +import ce2team1.mentoview.archive.entity.PaymentArchive; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentArchiveRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/archive/repository/UserArchiveRepository.java b/src/main/java/ce2team1/mentoview/archive/repository/UserArchiveRepository.java new file mode 100644 index 0000000..f43872c --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/repository/UserArchiveRepository.java @@ -0,0 +1,7 @@ +package ce2team1.mentoview.archive.repository; + +import ce2team1.mentoview.archive.entity.UserArchive; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserArchiveRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/archive/service/ArchiveBatch.java b/src/main/java/ce2team1/mentoview/archive/service/ArchiveBatch.java new file mode 100644 index 0000000..08be0ff --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/service/ArchiveBatch.java @@ -0,0 +1,54 @@ +package ce2team1.mentoview.archive.service; + +import ce2team1.mentoview.entity.User; +import ce2team1.mentoview.entity.atrribute.UserStatus; +import ce2team1.mentoview.repository.UserRepository; +import ce2team1.mentoview.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ArchiveBatch { + + private final ArchiveService archiveService; + private final UserRepository userRepository; + @Scheduled(cron = "0 0 2 * * ?") + @Transactional + public void archiveDeletedUsers() { + log.info("[유저 삭제 배치 시작] 탈퇴한 유저의 데이터를 아카이브"); + + List deletingUsers = userRepository.findAllByStatus(UserStatus.DELETED); + if (!deletingUsers.isEmpty()) { + log.info("[유저 삭제 배치 완료!] 탈퇴 유저 없음"); + return; + } + for (User deletingUser : deletingUsers) { + Long userId = deletingUser.getUserId(); + log.info("[아카이브 진행 유저] userId = {}", userId); + + log.info("[아카이브 인터뷰 아카이브 시작]"); + archiveService.archiveUserInterviews(userId); + log.info("[아카이브 인터뷰 아카이브 종료]"); + + log.info("[아카이브 결제 아카이브 시작]"); + archiveService.archiveUserPayments(userId); + log.info("[아카이브 결제 아카이브 종료]"); + + log.info("[아카이브 유저 아카이브 시작]"); + archiveService.archiveUser(userId); + log.info("[아카이브 유저 아카이브 종료]"); + + log.info("[아카이브 배치 완료] 모든 유저 데이터 아카이브 작업 완료 "); + + } + } + + +} diff --git a/src/main/java/ce2team1/mentoview/archive/service/ArchiveService.java b/src/main/java/ce2team1/mentoview/archive/service/ArchiveService.java new file mode 100644 index 0000000..71dd3ea --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/service/ArchiveService.java @@ -0,0 +1,133 @@ +package ce2team1.mentoview.archive.service; + +import ce2team1.mentoview.archive.dto.InterviewData; +import ce2team1.mentoview.archive.dto.InterviewEntry; +import ce2team1.mentoview.archive.entity.*; +import ce2team1.mentoview.archive.repository.InterviewArchiveRepository; +import ce2team1.mentoview.archive.repository.PaymentArchiveRepository; +import ce2team1.mentoview.archive.repository.UserArchiveRepository; +import ce2team1.mentoview.entity.*; +import ce2team1.mentoview.entity.atrribute.UserStatus; +import ce2team1.mentoview.repository.*; +import ce2team1.mentoview.service.AwsS3Service; +import ce2team1.mentoview.utils.archive.JsonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ArchiveService { + private final InterviewArchiveRepository archiveRepository; + private final ResumeRepository resumeRepository; + private final PaymentArchiveRepository paymentArchiveRepository; + private final InterviewRepository interviewRepository; + private final PaymentRepository paymentRepository; + private final SubscriptionRepository subscriptionRepository; + private final AwsS3Service awsS3Service; + private final UserRepository userRepository; + private final UserArchiveRepository userArchiveRepository ; + private final InterviewQuestionRepository interviewQuestionRepository; + private final ApplicationEventPublisher eventPublisher; +/* +* todo +* lamdba 작업이 필요한듯 +* */ + // Interview Archive 생성 + @Transactional(readOnly = false) + public InterviewArchive archiveUserInterviews(Long userId) { + List resumesId = resumeRepository.findAllByUserId(userId).stream().map(Resume::getResumeId).collect(Collectors.toList()); + if (resumesId.isEmpty()) { + return InterviewArchive.of(0L,""); + } + System.out.println("resumesId = " + resumesId); + + List interviewList = interviewRepository.findAllByResumeId(resumesId); + if (interviewList.isEmpty()) { + return InterviewArchive.of(0L,""); + } + + List interviewIds = interviewList.stream().map(Interview::getInterviewId).collect(Collectors.toList()); + + //interviewId 기준 그룹핑 + Map> entryGroup = interviewQuestionRepository.findAllWithResponseAndFeedback(interviewIds) + .stream().map(InterviewEntry::of) + .collect(Collectors.groupingBy(InterviewEntry::getInterviewId)); + + + List allInterviewData = interviewList.stream().map(interview + -> InterviewData.of(entryGroup.getOrDefault(interview.getInterviewId(), new ArrayList<>()))) + .collect(Collectors.toList()); + + + String interviewDataJson = JsonUtils.toJson(allInterviewData); + System.out.println("interviewDataJson = " + interviewDataJson); + + String s3key = awsS3Service.interviewArchiveUpload(userId, interviewDataJson); + + InterviewArchive save = archiveRepository.save(InterviewArchive.of(userId, s3key)); + return save; + + + } + + @Transactional(readOnly = false) + public List archiveUserPayments(Long userId) { + List subscriptions = subscriptionRepository.findAllByUser_UserId(userId); + if (subscriptions.isEmpty()) { + return new ArrayList<>(); + } + + List subIds = subscriptions.stream().map(subscription -> subscription.getSubId()) + .collect(Collectors.toList()); + + List paymentList = paymentRepository.findBySubIds(subIds); + + List paymentArchiveList = paymentList.stream().map( + payment -> PaymentArchive.of( + userId, + payment.getAmount(), + payment.getApprovalCode(), + payment.getSubscription().getPlan().name(), + payment.getStatus(), + payment.getPaymentDate() + )) + .collect(Collectors.toList()); + + + List paymentArchives = paymentArchiveRepository.saveAll(paymentArchiveList); + return paymentArchives; + + } + + @Transactional(readOnly = false) + public void archiveUser(Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("user not found")); + + UserArchive userArchive = UserArchive.of( + userId, + user.getEmail(), + user.getRole(), + UserStatus.DELETED, + user.getCreatedAt(), + "" + ); + userArchiveRepository.save(userArchive); + log.info("archive userId = " + userId); + + eventPublisher.publishEvent(new UserDeletedEvent(userId)); + + } + + + +} diff --git a/src/main/java/ce2team1/mentoview/archive/service/UserDeletedEvent.java b/src/main/java/ce2team1/mentoview/archive/service/UserDeletedEvent.java new file mode 100644 index 0000000..7c488c1 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/service/UserDeletedEvent.java @@ -0,0 +1,20 @@ +package ce2team1.mentoview.archive.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Getter +@Slf4j +@RequiredArgsConstructor +public class UserDeletedEvent { + private final Long userId; + + + public void start() { + log.info("[{}, 데이터 삭제 시작 ]", userId); + } +} diff --git a/src/main/java/ce2team1/mentoview/archive/service/UserDeletionListener.java b/src/main/java/ce2team1/mentoview/archive/service/UserDeletionListener.java new file mode 100644 index 0000000..672dab1 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/archive/service/UserDeletionListener.java @@ -0,0 +1,150 @@ +package ce2team1.mentoview.archive.service; + +import ce2team1.mentoview.entity.Interview; +import ce2team1.mentoview.entity.Payment; +import ce2team1.mentoview.entity.Resume; +import ce2team1.mentoview.entity.Subscription; +import ce2team1.mentoview.repository.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class UserDeletionListener { + private final UserRepository userRepository; + private final InterviewRepository interviewRepository; + private final ResumeRepository resumeRepository; + private final PaymentRepository paymentRepository; + private final SubscriptionRepository subscriptionRepository; + private final EntityManager entityManager; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void deleteUserAfterArchiving(UserDeletedEvent event) { + Long userId = event.getUserId(); + log.info("[유저 데이터 삭제 진행] UserId {} ", userId); + + List resumeIds = resumeRepository.findAllByUserId(userId) + .stream().map(Resume::getResumeId).toList(); + + List interviewIds = interviewRepository.findAllByResumeId(resumeIds) + .stream().map(Interview::getInterviewId).toList(); + + List subIds = subscriptionRepository.findAllByUser_UserId(userId) + .stream().map(Subscription::getSubId).toList(); + + List paymentIds = paymentRepository.findBySubIds(subIds) + .stream().map(Payment::getPaymentId).toList(); + + log.info("[삭제 대상 조회 완료] Resume: {}, Interview: {}, Subscription: {}, Payment: {}", + resumeIds.size(), interviewIds.size(), subIds.size(), paymentIds.size()); + + if (!interviewIds.isEmpty()) { + deleteInterviewResponsesInBatch(interviewIds); + log.info("[삭제 완료] InterviewResponses 삭제 완료"); + } + + + if (!interviewIds.isEmpty()) { + deleteInterviewFeedbacksInBatch(interviewIds); + log.info("[삭제 완료] InterviewFeedbacks 삭제 완료"); + } + + if (!interviewIds.isEmpty()) { + deleteInterviewQuestionsInBatch(interviewIds); + log.info("[삭제 완료] InterviewQuestions 삭제 완료"); + } + + if (!paymentIds.isEmpty()) { + deletePaymentsInBatch(paymentIds); + log.info("[삭제 완료] Payments 삭제 완료"); + } + + if (!subIds.isEmpty()) { + deleteSubscriptionsInBatch(subIds); + log.info("[삭제 완료] Subscriptions 삭제 완료"); + } + + if (!interviewIds.isEmpty()) { + deleteInterviewsInBatch(interviewIds); + log.info("[삭제 완료] Interviews 삭제 완료"); + } + + if (!resumeIds.isEmpty()) { + deleteResumesInBatch(resumeIds); + log.info("[삭제 완료] Resumes 삭제 완료"); + } + + // ✅ 6. 최종적으로 유저 삭제 + userRepository.deleteById(userId); + log.info("[유저 삭제 완료] UserId: {}", userId); + } + private void deleteInterviewResponsesInBatch(List interviewIds) { + Query query = entityManager.createQuery( + "DELETE FROM InterviewResponse ir WHERE ir.question.interview.interviewId IN :interviewIds" + ); + query.setParameter("interviewIds", interviewIds); + query.executeUpdate(); + entityManager.clear(); + } + + private void deleteInterviewFeedbacksInBatch(List interviewIds) { + Query query = entityManager.createQuery( + "DELETE FROM InterviewFeedback ifb WHERE ifb.question.interview.interviewId IN :interviewIds" + ); + query.setParameter("interviewIds", interviewIds); + query.executeUpdate(); + entityManager.clear(); + } + + + private void deleteInterviewQuestionsInBatch(List interviewIds) { + Query query = entityManager.createQuery( + "DELETE FROM InterviewQuestion iq WHERE iq.interview.interviewId IN :interviewIds" + ); + query.setParameter("interviewIds", interviewIds); + query.executeUpdate(); + entityManager.clear(); + } + + private void deletePaymentsInBatch(List paymentIds) { + Query query = entityManager.createQuery("delete from Payment p where p.id in :paymentIds"); + query.setParameter("paymentIds", paymentIds); + query.executeUpdate(); + entityManager.clear(); + } + + private void deleteSubscriptionsInBatch(List subIds) { + Query query = entityManager.createQuery("delete from Subscription s where s.id in :subIds"); + query.setParameter("subIds", subIds); + query.executeUpdate(); + entityManager.clear(); + } + + private void deleteInterviewsInBatch(List interviewIds) { + Query query = entityManager.createQuery("delete from Interview i where i.id in :interviewIds"); + query.setParameter("interviewIds", interviewIds); + query.executeUpdate(); + entityManager.clear(); + } + + private void deleteResumesInBatch(List resumeIds) { + Query query = entityManager.createQuery("delete from Resume r where r.id in :resumeIds"); + query.setParameter("resumeIds", resumeIds); + query.executeUpdate(); + entityManager.clear(); + } + + +} diff --git a/src/main/java/ce2team1/mentoview/config/AsyncConfig.java b/src/main/java/ce2team1/mentoview/config/AsyncConfig.java index bebe731..ad9efbd 100644 --- a/src/main/java/ce2team1/mentoview/config/AsyncConfig.java +++ b/src/main/java/ce2team1/mentoview/config/AsyncConfig.java @@ -14,9 +14,9 @@ public class AsyncConfig { @Bean(name = "generateFeedbackExecutor") public Executor generateFeedbackExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(20); // 최소 스레드 개수 (기본값) - executor.setMaxPoolSize(50); // 최대 스레드 개수 (테스트 후 조정 가능) - executor.setQueueCapacity(200); // 대기 큐 크기 (이후 요청을 저장) + executor.setCorePoolSize(10); // 최소 스레드 개수 (기본값) + executor.setMaxPoolSize(40); // 최대 스레드 개수 (테스트 후 조정 가능) + executor.setQueueCapacity(100); // 대기 큐 크기 (이후 요청을 저장) executor.setThreadNamePrefix("Feedback-"); executor.initialize(); return executor; diff --git a/src/main/java/ce2team1/mentoview/config/CorsConfig.java b/src/main/java/ce2team1/mentoview/config/CorsConfig.java index 789ddee..d9ab998 100644 --- a/src/main/java/ce2team1/mentoview/config/CorsConfig.java +++ b/src/main/java/ce2team1/mentoview/config/CorsConfig.java @@ -15,7 +15,7 @@ public class CorsConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:3001")); + configuration.setAllowedOrigins(Arrays.asList("https://mentoview.site", "http://localhost:3000", "http://localhost:3001")); configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setExposedHeaders(List.of("Authorization")); diff --git a/src/main/java/ce2team1/mentoview/config/JpaConfig.java b/src/main/java/ce2team1/mentoview/config/JpaConfig.java index a3ae618..bf6a704 100644 --- a/src/main/java/ce2team1/mentoview/config/JpaConfig.java +++ b/src/main/java/ce2team1/mentoview/config/JpaConfig.java @@ -1,10 +1,16 @@ package ce2team1.mentoview.config; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing +@RequiredArgsConstructor public class JpaConfig { + + } diff --git a/src/main/java/ce2team1/mentoview/config/MvAuditorAware.java b/src/main/java/ce2team1/mentoview/config/MvAuditorAware.java index 4c8c094..52801df 100644 --- a/src/main/java/ce2team1/mentoview/config/MvAuditorAware.java +++ b/src/main/java/ce2team1/mentoview/config/MvAuditorAware.java @@ -1,34 +1,56 @@ package ce2team1.mentoview.config; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.dto.MvPrincipalDetails; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.AuditorAware; +import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import java.util.Map; import java.util.Optional; @Component("MvAuditorAware") +@RequiredArgsConstructor public class MvAuditorAware implements AuditorAware { + + private final JwtTokenProvider jwtTokenProvider; + @Override public Optional getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - return Optional.of("anonymousUser"); // 인증되지 않은 경우 anonymousUser + return Optional.of("anonymousUser"); // 인증x anonymousUser } Object principal = authentication.getPrincipal(); - if (principal instanceof MvPrincipalDetails) { //폼 로그인 사용자 + if (principal instanceof MvPrincipalDetails) { //폼 로그인 return Optional.of(((MvPrincipalDetails) principal).getUsername()); - } else if (principal instanceof OAuth2User) { // OAuth 로그인 사용자 + } else if (principal instanceof OAuth2User) { // OAuth 로그인 Map attributes = ((OAuth2User) principal).getAttributes(); return Optional.ofNullable((String) attributes.get("email")); } - return Optional.of("anonymousUser"); // + return getUserFromJwt(); + } + + private Optional getUserFromJwt() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); // "Bearer " 제거 + if (jwtTokenProvider.validateToken(token)) { + return Optional.of(jwtTokenProvider.getEmailFromToken(token)); + } + } + return Optional.of("anonymousUser"); } } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/config/SecurityConfig.java b/src/main/java/ce2team1/mentoview/config/SecurityConfig.java index 8701de4..799a8d3 100644 --- a/src/main/java/ce2team1/mentoview/config/SecurityConfig.java +++ b/src/main/java/ce2team1/mentoview/config/SecurityConfig.java @@ -1,9 +1,15 @@ package ce2team1.mentoview.config; -import ce2team1.mentoview.security.*; +import ce2team1.mentoview.security.filter.*; +import ce2team1.mentoview.security.handler.MvLogoutHandler; +import ce2team1.mentoview.security.handler.MvOAuth2FormFailureHandler; +import ce2team1.mentoview.security.handler.MvOAuth2FormSuccessHandler; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.service.MvAuthenticationProvider; import ce2team1.mentoview.security.service.MvOAuth2UserService; import ce2team1.mentoview.security.service.RefreshTokenService; +import ce2team1.mentoview.security.social.MvHttpCookieOAuth2AuthorizationRequestRepository; +import ce2team1.mentoview.security.social.MvOAuth2ClientRegistration; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; @@ -22,9 +28,7 @@ import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.security.web.context.SecurityContextHolderFilter; -import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.context.*; import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.web.cors.CorsConfigurationSource; @@ -39,12 +43,17 @@ public class SecurityConfig { private final RefreshTokenService refreshTokenService; private final JwtTokenProvider jwtTokenProvider; private final MvRequestFilter mvRequestFilter; + private final MvLogoutHandler mvLogoutHandler; + private final LambdaRequestFilter lambdaFilter; private final AuthenticationConfiguration authenticationConfiguration; + private final WebhookFilter webhookFilter; @Bean public SecurityContextRepository securityContextRepository() { - return new HttpSessionSecurityContextRepository(); + return new DelegatingSecurityContextRepository( + new RequestAttributeSecurityContextRepository(), + new HttpSessionSecurityContextRepository()); } @@ -81,17 +90,32 @@ private void configureCommon(HttpSecurity security) throws Exception { @Bean public WebSecurityCustomizer webSecurityCustomizer() { - return web -> web.ignoring().requestMatchers("/img/**") + return web -> web.ignoring() + .requestMatchers("/api/management/**","/api/targets","/api/config","/api/actuator","/api/actuator/**") + .requestMatchers("/img/**") + .requestMatchers("/api/management/**") .requestMatchers("/api/test") - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**") //swagger + .requestMatchers("/api/swagger-ui/**", "/api/v3/api-docs/**", "/api/swagger-resources/**") //swagger .requestMatchers("/", "/favicon.ico", "/static", "/about", "/contactus") .requestMatchers("/error", "/error/**"); + } + @Bean + public SecurityFilterChain monitoringSecurityFilterChain(HttpSecurity security) throws Exception { + configureCommon(security); + security.securityMatcher("/api/admin/**") + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/admin/login").permitAll() // 관리자가 로그인할 수 있도록 허용 + .requestMatchers("/api/admin/**").hasRole("ADMIN") // ✅ 관리자만 접근 가능 + .anyRequest().authenticated() + ); + return security.build(); } @Bean public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity security, - AuthenticationManager authenticationManager) throws Exception { + AuthenticationManager authenticationManager, + DeletedUserFilter deletedUserFilter) throws Exception { configureCommon(security); security.securityMatcher("/api/oauth2/authorization/google","/login/oauth2/code/google","/api/authorization/google/**","/api/login/oauth2/code/**");// 이 URL만 처리함 security .authorizeHttpRequests(auth -> auth @@ -113,33 +137,47 @@ public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity security, .successHandler(mvOAuth2FormSuccessHandler()) .failureHandler(mvOAuth2FormFailureHandler()) ); + security.addFilterBefore(deletedUserFilter, UsernamePasswordAuthenticationFilter.class); return security.build(); } @Bean public SecurityFilterChain formLoginSecurityFilterChain(HttpSecurity security, AuthenticationManager authenticationManager, - MvLoginFormFilter mvLoginFormFilter, AuthenticationConfiguration authenticationConfiguration) throws Exception { + MvLoginFormFilter mvLoginFormFilter, + AuthenticationConfiguration authenticationConfiguration, + DeletedUserFilter deletedUserFilter) throws Exception { configureCommon(security); security.securityMatcher("/api/login") // 폼 로그인 요청만 매칭 .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) .addFilterBefore(mvLoginFormFilter, UsernamePasswordAuthenticationFilter.class); - security.addFilterAfter(mvRequestFilter, SecurityContextHolderFilter.class); + security.addFilterBefore(deletedUserFilter, UsernamePasswordAuthenticationFilter.class); return security.build(); - } - @Bean - public SecurityFilterChain commonSecurityFilterChain(HttpSecurity security) throws Exception { + public SecurityFilterChain commonSecurityFilterChain(HttpSecurity security, + DeletedUserFilter deletedUserFilter) throws Exception { configureCommon(security); security.authorizeHttpRequests(authorizeRequests -> authorizeRequests - .requestMatchers("/api/signup/**").permitAll() - .requestMatchers("/api/auth/me").hasRole("USER") - .requestMatchers("/admin/**").hasRole("ADMIN")); - + .requestMatchers("/api/interview/response/transcription").permitAll() + .requestMatchers("/api/signup/form").permitAll() + .requestMatchers("/api/webhook/**").permitAll() + .requestMatchers("/error").permitAll() + .requestMatchers("/api/admin/**").hasRole("ADMIN") + .requestMatchers("/api/auth/me").hasAnyRole("USER", "ADMIN") + .requestMatchers("/api/token/access").authenticated() + .requestMatchers("/api/**").authenticated()); + + security.addFilterBefore(lambdaFilter, UsernamePasswordAuthenticationFilter.class); + security.addFilterBefore(webhookFilter, UsernamePasswordAuthenticationFilter.class); security.addFilterBefore(mvRequestFilter, UsernamePasswordAuthenticationFilter.class); + + security.addFilterBefore(deletedUserFilter, UsernamePasswordAuthenticationFilter.class); + security.logout(logout -> logout + .addLogoutHandler(mvLogoutHandler) + .logoutUrl("/api/logout")); return security.build(); } @@ -161,7 +199,3 @@ public MvLoginFormFilter mvLoginFormFilter(AuthenticationManager authenticationM return filter; } } - - - - diff --git a/src/main/java/ce2team1/mentoview/controller/PaymentController.java b/src/main/java/ce2team1/mentoview/controller/PaymentController.java deleted file mode 100644 index 65d7a39..0000000 --- a/src/main/java/ce2team1/mentoview/controller/PaymentController.java +++ /dev/null @@ -1,13 +0,0 @@ -package ce2team1.mentoview.controller; - -import ce2team1.mentoview.service.PaymentService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api") -public class PaymentController { - private final PaymentService paymentService; -} diff --git a/src/main/java/ce2team1/mentoview/controller/ResumeController.java b/src/main/java/ce2team1/mentoview/controller/ResumeController.java index 1d39a8b..ca57baa 100644 --- a/src/main/java/ce2team1/mentoview/controller/ResumeController.java +++ b/src/main/java/ce2team1/mentoview/controller/ResumeController.java @@ -4,6 +4,7 @@ import ce2team1.mentoview.security.dto.MvPrincipalDetails; import ce2team1.mentoview.service.ResumeService; import ce2team1.mentoview.service.dto.ResumeDto; +import ce2team1.mentoview.service.dto.UserDto; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -22,7 +23,12 @@ public class ResumeController { @GetMapping public ResponseEntity> getFullRes(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails ) { + System.out.println("principal - " + mvPrincipalDetails); + UserDto dto = mvPrincipalDetails.getUserDto(); + System.out.println("UserDto - " + dto); + Long userId = mvPrincipalDetails.getUserId(); + // Long userId = 1L; List resumes = resumeService.getResumesByUserId(userId); return ResponseEntity.ok(resumes); @@ -33,6 +39,7 @@ public ResponseEntity createRes(@RequestParam("file") MultipartFile @AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) { Long userId = mvPrincipalDetails.getUserId(); + // Long userId = 1L; ResumeDto resumeDto = resumeService.createResume(file, userId); ResumeResp response = ResumeResp.from(resumeDto); @@ -44,8 +51,9 @@ public ResponseEntity deleteRes(@PathVariable Long resumeId, @AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) { Long userId = mvPrincipalDetails.getUserId(); + // Long userId = 1L; resumeService.deleteResume(resumeId, userId); return ResponseEntity.ok().build(); } -} \ No newline at end of file +} diff --git a/src/main/java/ce2team1/mentoview/controller/SubscriptionController.java b/src/main/java/ce2team1/mentoview/controller/SubscriptionController.java index ae7bf4d..7a15009 100644 --- a/src/main/java/ce2team1/mentoview/controller/SubscriptionController.java +++ b/src/main/java/ce2team1/mentoview/controller/SubscriptionController.java @@ -33,13 +33,13 @@ public class SubscriptionController { @Operation(summary = "구독 조회", description = "사용자의 모든 구독 내역과 구독 각각에 대한 결제 내역을 전달합니다.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "구독 조회 성공") + @ApiResponse(responseCode = "200", description = "구독 조회에 성공했습니다.") }) @GetMapping("/subscription") public ResponseEntity> getSubscription(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) { Long userId = mvPrincipalDetails.getUserId(); -// Long userId = 1L; + // Long userId = 1L; List subscriptions = subscriptionService.getSubscriptions(userId); for (SubscriptionResp subscriptionResp : subscriptions) { @@ -52,13 +52,13 @@ public ResponseEntity> getSubscription(@AuthenticationPri @Operation(summary = "구독 상태 조회", description = "구독 처리 진행 상태를 조회합니다.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "구독 상태 조회를 성공하였습니다.") + @ApiResponse(responseCode = "200", description = "구독 상태 조회에 성공했습니다.") }) @GetMapping("/subscription/status") public ResponseEntity getSubscriptionStatus(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) { Long userId = mvPrincipalDetails.getUserId(); -// Long userId = 1L; + // Long userId = 1L; if (subscriptionService.getSubscriptionByUserId(userId, SubscriptionStatus.ACTIVE) != null) { return ResponseEntity.ok("구독 처리 완료"); @@ -69,22 +69,26 @@ public ResponseEntity getSubscriptionStatus(@AuthenticationPrincipal MvP @Operation(summary = "구독 해지", description = "결제 예약 취소 후 구독의 상태를 CANCELED로 변경합니다.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "구독 해지 성공") + @ApiResponse(responseCode = "200", description = "구독 해지 요청이 성공적으로 처리됐습니다."), + @ApiResponse(responseCode = "404", description = "해당 구독은 유효하지 않습니다."), + @ApiResponse(responseCode = "500", description = "구독 해지 요청 처리 중 문제가 발생했습니다.") + }) @DeleteMapping("/subscription/{subscription_id}") public ResponseEntity deleteSubscription(@PathVariable("subscription_id") Long sId, @AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) throws JsonProcessingException { Long uId = mvPrincipalDetails.getUserId(); -// Long uId = 1L; + // Long uId = 1L; Long checkSId = subscriptionService.checkSubscription(uId); if (checkSId != null && checkSId.equals(sId)) { - // 결제 예약 취소 + // 결제 예약 취소 후 빌링키 삭제 portonePaymentService.cancelScheduling(uId); // 구독의 상태 변경 subscriptionService.deleteSubscription(sId); + return ResponseEntity.ok("구독 해지 성공"); } else { throw new SubscriptionException("해당 구독 내역은 유효하지 않습니다.", HttpStatus.BAD_REQUEST); @@ -102,15 +106,36 @@ public ResponseEntity deleteSubscription(@PathVariable("subscription_id" public ResponseEntity createSubscription(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) throws JsonProcessingException { Long uId = mvPrincipalDetails.getUserId(); -// Long uId = 1L; + // Long uId = 1L; - if (subscriptionService.getSubscriptionByUserId(uId, SubscriptionStatus.CANCELED) == null) { - portonePaymentService.createPayment(uId); - } else { + if (subscriptionService.getSubscriptionByUserId(uId, SubscriptionStatus.CANCELED) != null) { // 현재 CANCELED 상태의 구독의 nextBillingDate로 결제를 예약하고, 구독의 상태 ACTIVE로 변경 portonePaymentService.processSubscriptionReactivation(uId); + + } else { + // 바로 결제 진행 + portonePaymentService.createPayment(uId); + } return ResponseEntity.ok("구독 요청이 정상적으로 처리되었습니다."); } + + @Operation(summary = "프리티어 구독 생성", description = "결제 없이 FREE-TIER 구독을 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프리티어 구독 요청 처리가 성공적으로 이루어졌습니다."), + @ApiResponse(responseCode = "500", description = "프리티어 구독 요청 처리 중 문제가 발생했습니다.") + }) + @PostMapping("/subscription/freetier") + public ResponseEntity createSubscriptionFreetier(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails) throws JsonProcessingException { + + Long uId = mvPrincipalDetails.getUserId(); + // Long uId = 1L; + + // 첫 구독 -> free-tire로 구독 생성 + portonePaymentService.processFreeTierSubscription(uId); + + return ResponseEntity.ok("구독 요청이 정상적으로 처리되었습니다."); + + } } diff --git a/src/main/java/ce2team1/mentoview/controller/UserController.java b/src/main/java/ce2team1/mentoview/controller/UserController.java index d56eade..608e364 100644 --- a/src/main/java/ce2team1/mentoview/controller/UserController.java +++ b/src/main/java/ce2team1/mentoview/controller/UserController.java @@ -3,11 +3,15 @@ import ce2team1.mentoview.controller.dto.request.UserCreateForm; import ce2team1.mentoview.controller.dto.request.UserMypage; +import ce2team1.mentoview.controller.dto.request.UserPasswordCreate; import ce2team1.mentoview.controller.dto.request.UserPasswordModify; import ce2team1.mentoview.controller.dto.response.FormUserResp; import ce2team1.mentoview.controller.dto.response.UserResp; import ce2team1.mentoview.exception.UserException; import ce2team1.mentoview.security.dto.MvPrincipalDetails; +import ce2team1.mentoview.security.service.JwtTokenProvider; +import ce2team1.mentoview.security.service.RefreshTokenService; +import ce2team1.mentoview.service.ResumeService; import ce2team1.mentoview.service.SubscriptionService; import ce2team1.mentoview.service.UserService; import ce2team1.mentoview.service.dto.UserDto; @@ -17,12 +21,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.header.Header; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.net.URI; + @RestController @RequestMapping("/api") @Tag(name = "User API", description = "회원가입, myPage API") @@ -30,7 +40,9 @@ public class UserController { private final UserService userService; - private final SubscriptionService subscriptionService; + private final ResumeService resumeService; + private final RefreshTokenService refreshTokenService; + private final JwtTokenProvider jwtTokenProvider; @Operation(summary = "Form 회원가입", description = "새로운 사용자를 등록합니다.") @ApiResponses(value = { @@ -56,7 +68,7 @@ public ResponseEntity signup(@RequestBody @Validated UserCreateFor @ApiResponse(responseCode = "400", description = "인증 없음") }) @ResponseStatus(HttpStatus.OK) - @PostMapping("/signup/api/mypage") + @PostMapping("/mypage") public ResponseEntity mypage (@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails, @Valid @RequestBody UserMypage userMypage) { Long userId = mvPrincipalDetails.getUserId(); @@ -77,7 +89,6 @@ public ResponseEntity mypage (@AuthenticationPrincipal MvPrincipalDeta @PostMapping("/mypage/password") public ResponseEntity modifyPassword(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails, @Valid @RequestBody UserPasswordModify userPasswordModify) { Long userId = mvPrincipalDetails.getUserId(); - if (!userPasswordModify.getAfterPassword().equals(userPasswordModify.getAfterPasswordCheck())) { throw new IllegalArgumentException("새 비밀번호와 비밀번호 확인이 일치하지 않습니다."); } @@ -85,20 +96,69 @@ public ResponseEntity modifyPassword(@AuthenticationPrincipal MvPrincipa return ResponseEntity.ok("비밀번호 변경 성공"); + } + @Operation(summary = "회원 탈퇴", description = "회원 탈퇴") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "탈퇴 성공"), + @ApiResponse(responseCode = "400", description = "권한 없음") + }) @DeleteMapping("/mypage/delete/{id}") public ResponseEntity userDelete(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails, @PathVariable Long id) { Long userId = mvPrincipalDetails.getUserId(); + if (!userId.equals(id)) { throw new UserException( "삭제할 권한이 없습니다.", HttpStatus.NOT_FOUND); } + userService.softDelete(userId); + resumeService.hardDeleteResume(userId); + refreshTokenService.deleteRefreshToken(mvPrincipalDetails.getName()); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getName().equals(mvPrincipalDetails.getName())) { + SecurityContextHolder.clearContext(); // 강제 로그아웃 + } return ResponseEntity.ok("삭제완료"); } + @Operation(summary = "social 비밀번호 생성", description = "social 사용자의 비밀번호를 생성합니다") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "social 비밀번호 생성"), + @ApiResponse(responseCode = "400", description = "잘못된 요청") + }) + @PostMapping("/social/password") + public ResponseEntity createPassword(@AuthenticationPrincipal MvPrincipalDetails mvPrincipalDetails, @Valid @RequestBody UserPasswordCreate userPasswordCreate) { + Long userId = mvPrincipalDetails.getUserId(); + if (!userPasswordCreate.getPassword().equals(userPasswordCreate.getPasswordCheck())) { + throw new IllegalArgumentException("새 비밀번호와 비밀번호 확인이 일치하지 않습니다."); + } + UserDto password = userService.createPassword(userId, userPasswordCreate.getPassword()); + UserResp resp = UserResp.of(password, "비밀번호 변경 성공"); + + return ResponseEntity.status(HttpStatus.OK).body(resp); + + } + + + /* + * String accessToken = jwtTokenProvider.createAccessToken(password.getEmail() , password.getRole()); + + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create("https://mentoview.site/")); + headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + + return ResponseEntity.status(HttpStatus.FOUND) + .headers(headers).body(resp); + + * + * */ + + + } diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackCreate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackCreate.java deleted file mode 100644 index 6b05e79..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackCreate.java +++ /dev/null @@ -1,36 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - - -import ce2team1.mentoview.service.dto.FeedbackDto; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class FeedbackCreate { - private String feedback; - private Integer score; - @NotNull - private Long questionId; - - public static FeedbackCreate of(String feedback, int score, Long questionId) { - return new FeedbackCreate( - feedback, - score, - questionId - ); - } - public FeedbackDto toDto() { - return FeedbackDto.builder() - .feedback(this.feedback) - .score(this.score) - .questionId(this.questionId) - .build(); - } - -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackRead.java b/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackRead.java deleted file mode 100644 index d0bba25..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/FeedbackRead.java +++ /dev/null @@ -1,25 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class FeedbackRead { - @NotNull - private Long feedbackId; - @NotNull - private String feedback; - @NotNull - private Integer score; - @NotNull - private Long questionId; - - - -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/InterviewRead.java b/src/main/java/ce2team1/mentoview/controller/dto/request/InterviewRead.java deleted file mode 100644 index 41e97ce..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/InterviewRead.java +++ /dev/null @@ -1,19 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class InterviewRead { - @NotNull - private Long interviewId; - @NotNull - private Long resumeId; -} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionCreate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionCreate.java deleted file mode 100644 index 5ddb325..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionCreate.java +++ /dev/null @@ -1,22 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import ce2team1.mentoview.entity.atrribute.Difficulty; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class QuestionCreate { - @NotBlank - private String question; - @NotNull - private Difficulty difficulty; - @NotNull - private Long interviewId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionRead.java b/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionRead.java deleted file mode 100644 index 8616f7a..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/QuestionRead.java +++ /dev/null @@ -1,20 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * DTO for {@link ce2team1.mentoview.entity.InterviewQuestion} - */ -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class QuestionRead { - private Long questionId; - @NotNull - private Long interviewId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseCreate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseCreate.java deleted file mode 100644 index 49994e0..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseCreate.java +++ /dev/null @@ -1,30 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.Duration; - -/** - * DTO for {@link ce2team1.mentoview.entity.InterviewResponse} - */ -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ResponseCreate { - @NotBlank - private String respUrl; - @NotBlank - private String response; - @NotNull - private Boolean answered = false; - @NotNull - private Duration duration = Duration.ZERO; - - private Long questionId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseRead.java b/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseRead.java deleted file mode 100644 index 5c9b990..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/ResponseRead.java +++ /dev/null @@ -1,21 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * DTO for {@link ce2team1.mentoview.entity.InterviewResponse} - */ -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ResponseRead { - @NotNull - private Long responseId; - @NotNull - private Long questionId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionCreate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionCreate.java deleted file mode 100644 index 7308371..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionCreate.java +++ /dev/null @@ -1,36 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import ce2team1.mentoview.entity.atrribute.SubscriptionPlan; -import ce2team1.mentoview.entity.atrribute.SubscriptionStatus; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; - - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class SubscriptionCreate { - @NotBlank - private SubscriptionStatus status; - @NotBlank - private SubscriptionPlan plan; - @NotBlank - private LocalDate startDate; - @NotBlank - private LocalDate endDate; - @NotBlank - private LocalDate nextBillingDate; - @NotBlank - private String paymentMethod; - @NotNull - private Long userId; - - -} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionModify.java b/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionModify.java deleted file mode 100644 index d95f5a4..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionModify.java +++ /dev/null @@ -1,21 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import ce2team1.mentoview.entity.atrribute.SubscriptionPlan; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class SubscriptionModify { - @NotNull - Long subId; - SubscriptionPlan plan; - String paymentMethod; - @NotNull - Long userId; -} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionRead.java b/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionRead.java deleted file mode 100644 index ee804b2..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionRead.java +++ /dev/null @@ -1,16 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class SubscriptionRead { - private Long subId; - -} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionUpdate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionUpdate.java deleted file mode 100644 index d94159f..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/SubscriptionUpdate.java +++ /dev/null @@ -1,39 +0,0 @@ -package ce2team1.mentoview.controller.dto.request; - - -import ce2team1.mentoview.entity.atrribute.SubscriptionPlan; -import ce2team1.mentoview.entity.atrribute.SubscriptionStatus; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; - - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class SubscriptionUpdate { - @NotNull - private Long subId; - @NotBlank - private SubscriptionStatus status; - @NotBlank - private SubscriptionPlan plan; - @NotBlank - private LocalDate startDate; - @NotBlank - private LocalDate endDate; - @NotBlank - private LocalDate nextBillingDate; - @NotBlank - private String paymentMethod; - @NotNull - private Long userId; - - -} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/UserCreateForm.java b/src/main/java/ce2team1/mentoview/controller/dto/request/UserCreateForm.java index e7dfe2e..e3b0c48 100644 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/UserCreateForm.java +++ b/src/main/java/ce2team1/mentoview/controller/dto/request/UserCreateForm.java @@ -1,6 +1,7 @@ package ce2team1.mentoview.controller.dto.request; - +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; @@ -18,14 +19,20 @@ public class UserCreateForm { @NotBlank private String email; - @NotBlank + + @NotBlank(message = "비밀번호를 입력해주세요") + @Size(min = 8, max =15, message = "비밀번호 최소 8자 이상, 최대 15자 이하로 입력해주세요") + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.@$!%*?&\\^~+=()\\[\\]#\\-])[A-Za-z\\d.@$!%*?&\\^~+=()\\[\\]#\\-]{8,15}$", + message = "비밀번호는 최소 8자 이상 15자 이하이며, 숫자와 특수문자를 포함해야 합니다." + ) + private String password; + @NotBlank private String name; @Builder.Default private boolean isSocial = false; - - -} \ No newline at end of file +} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordCreate.java b/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordCreate.java new file mode 100644 index 0000000..d8fae28 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordCreate.java @@ -0,0 +1,30 @@ +package ce2team1.mentoview.controller.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * DTO for {@link ce2team1.mentoview.entity.User} + */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserPasswordCreate { + + + @NotBlank(message = "비밀번호를 입력해주세요") + @Size(min = 8, max =15, message = "비밀번호 최소 8자 이상, 최대 15자 이하로 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.@$!%*?&\\^~+=()\\[\\]#\\-])[A-Za-z\\d.@$!%*?&\\^~+=()\\[\\]#\\-]{8,15}$", + message = "비밀번호는 최소 8자 이상 15자 이하이며, 숫자와 특수문자를 포함해야 합니다.") + private String password; + + @NotBlank(message = "비밀번호 확인을 입력해주세요") + private String passwordCheck; + +} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordModify.java b/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordModify.java index a3ce650..6aa0f21 100644 --- a/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordModify.java +++ b/src/main/java/ce2team1/mentoview/controller/dto/request/UserPasswordModify.java @@ -22,13 +22,11 @@ public class UserPasswordModify { @NotBlank(message = "비밀번호를 입력해주세요") @Size(min = 8, max =15, message = "비밀번호 최소 8자 이상, 최대 15자 이하로 입력해주세요") - @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,15}$", + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.@$!%*?&\\^~+=()\\[\\]#\\-])[A-Za-z\\d.@$!%*?&\\^~+=()\\[\\]#\\-]{8,15}$", message = "비밀번호는 최소 8자 이상 15자 이하이며, 숫자와 특수문자를 포함해야 합니다.") private String afterPassword; @NotBlank(message = "비밀번호 확인을 입력해주세요") private String afterPasswordCheck; - - -} \ No newline at end of file +} diff --git a/src/main/java/ce2team1/mentoview/controller/dto/response/FeedbackResp.java b/src/main/java/ce2team1/mentoview/controller/dto/response/FeedbackResp.java deleted file mode 100644 index 087d28b..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/response/FeedbackResp.java +++ /dev/null @@ -1,20 +0,0 @@ -package ce2team1.mentoview.controller.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * DTO for {@link ce2team1.mentoview.entity.InterviewFeedback} - */ -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class FeedbackResp { - private Long feedbackId; - private String feedback; - private Integer score; - private Long questionId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/controller/dto/response/ResponseResp.java b/src/main/java/ce2team1/mentoview/controller/dto/response/ResponseResp.java deleted file mode 100644 index 7e8788a..0000000 --- a/src/main/java/ce2team1/mentoview/controller/dto/response/ResponseResp.java +++ /dev/null @@ -1,24 +0,0 @@ -package ce2team1.mentoview.controller.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.Duration; - -/** - * DTO for {@link ce2team1.mentoview.entity.InterviewResponse} - */ -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ResponseResp { - private Long responseId; - private String respUrl; - private String response; - private Boolean answered; - private Duration duration; - private Long questionId; -} \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/entity/Interview.java b/src/main/java/ce2team1/mentoview/entity/Interview.java index ec3b7b4..0996243 100644 --- a/src/main/java/ce2team1/mentoview/entity/Interview.java +++ b/src/main/java/ce2team1/mentoview/entity/Interview.java @@ -25,9 +25,11 @@ public class Interview extends AuditingFields { private Long interviewId; @Enumerated(EnumType.STRING) + @Column(nullable = false) private InterviewStatus interviewStatus; @Enumerated(EnumType.STRING) + @Column(nullable = false) private InterviewType interviewType; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/ce2team1/mentoview/entity/InterviewFeedback.java b/src/main/java/ce2team1/mentoview/entity/InterviewFeedback.java index 6563187..ece24b5 100644 --- a/src/main/java/ce2team1/mentoview/entity/InterviewFeedback.java +++ b/src/main/java/ce2team1/mentoview/entity/InterviewFeedback.java @@ -21,7 +21,7 @@ public class InterviewFeedback extends AuditingFields { private Long feedbackId; @Lob - @Column(columnDefinition = "TEXT") + @Column(columnDefinition = "TEXT", nullable = false) private String feedback; private Integer score; diff --git a/src/main/java/ce2team1/mentoview/entity/InterviewQuestion.java b/src/main/java/ce2team1/mentoview/entity/InterviewQuestion.java index 7cf122a..a0f1475 100644 --- a/src/main/java/ce2team1/mentoview/entity/InterviewQuestion.java +++ b/src/main/java/ce2team1/mentoview/entity/InterviewQuestion.java @@ -20,9 +20,11 @@ public class InterviewQuestion extends AuditingFields { @Column(name = "question_id") private Long questionId; + @Column(nullable = false) private String question; @Enumerated(EnumType.STRING) + @Column(nullable = false) private Difficulty difficulty; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/ce2team1/mentoview/entity/InterviewResponse.java b/src/main/java/ce2team1/mentoview/entity/InterviewResponse.java index 8f25ed1..d2c7dc3 100644 --- a/src/main/java/ce2team1/mentoview/entity/InterviewResponse.java +++ b/src/main/java/ce2team1/mentoview/entity/InterviewResponse.java @@ -21,13 +21,17 @@ public class InterviewResponse extends AuditingFields { private Long responseId; @Column(name = "s3_key") - private String s3Key;// 음성파일 저장 url + private String s3Key;// 음성파일 저장 s3 key @Lob @Column(columnDefinition = "TEXT") private String response; // AWS Transcribe 가 변환 + + @Column(nullable = false) private Boolean answered; // 상태 필드 3가 필요시 Enum - private Duration duration;//? + + @Column(nullable = false) + private Duration duration; // 응답 제출 안했을 시 0초 지정 @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "question_id", nullable = false, unique = true) diff --git a/src/main/java/ce2team1/mentoview/entity/Payment.java b/src/main/java/ce2team1/mentoview/entity/Payment.java index 52b280a..0ba93a3 100644 --- a/src/main/java/ce2team1/mentoview/entity/Payment.java +++ b/src/main/java/ce2team1/mentoview/entity/Payment.java @@ -21,18 +21,21 @@ public class Payment { @Column(name = "payment_id") private Long paymentId; + @Column(nullable = false) private BigDecimal amount; + @Column(nullable = false) private String approvalCode; + @Column(nullable = false) @Enumerated(EnumType.STRING) private PaymentStatus status; - @Column(name = "payment_date") + @Column(name = "payment_date", nullable = false) private LocalDateTime paymentDate; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "subscription_id") + @JoinColumn(name = "subscription_id", nullable = false) private Subscription subscription; public static Payment of(BigDecimal amount, String approvalCode,PaymentStatus status, LocalDateTime paymentDate, Subscription subscription) { diff --git a/src/main/java/ce2team1/mentoview/entity/Subscription.java b/src/main/java/ce2team1/mentoview/entity/Subscription.java index b684761..7215f24 100644 --- a/src/main/java/ce2team1/mentoview/entity/Subscription.java +++ b/src/main/java/ce2team1/mentoview/entity/Subscription.java @@ -27,16 +27,22 @@ public class Subscription extends AuditingFields { @Column(name = "subscription_id") private Long subId; + @Column(nullable = false) @Convert(converter = SubscriptionStatusConverter.class) private SubscriptionStatus status; + @Column(nullable = false) @Enumerated(EnumType.STRING) private SubscriptionPlan plan; + @Column(nullable = false) private LocalDate startDate; + @Column(nullable = false) private LocalDate endDate; + @Column(nullable = false) private LocalDate nextBillingDate; + @Column(nullable = false) @Enumerated(EnumType.STRING) private PaymentMethod paymentMethod; @@ -47,7 +53,7 @@ public class Subscription extends AuditingFields { private String portoneScheduleId; // 결제 예약 건 id : 결제 수단 변경 시 필요 @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private User user; // public static Subscription of(SubscriptionStatus status, SubscriptionPlan plan, LocalDate startDate, LocalDate endDate, LocalDate nextBillingDate, PaymentMethod paymentMethod, String billingKey, String portonePaymentId, String portoneScheduleId, User user) { @@ -88,13 +94,16 @@ public void modifyStatus(SubscriptionStatus subscriptionStatus) { this.status = subscriptionStatus; } - public void modifyEndDateAndNextBillingDate(String paidAt) { + public void modifyEndDateAndNextBillingDateAndPlan(String paidAt) { ZonedDateTime zonedDateTime = ZonedDateTime.parse(paidAt, DateTimeFormatter.ISO_DATE_TIME); LocalDate localDate = LocalDate.ofInstant(zonedDateTime.toInstant(), ZoneId.systemDefault()); this.endDate = localDate.plusDays(30); this.nextBillingDate = localDate.plusDays(31); + if (this.plan == SubscriptionPlan.FREE_TIER) { + this.plan = SubscriptionPlan.BASIC; + } } public void setPaymentIdAndScheduleId(String portonePaymentId, String portoneScheduleId) { diff --git a/src/main/java/ce2team1/mentoview/entity/User.java b/src/main/java/ce2team1/mentoview/entity/User.java index 2154050..b29115f 100644 --- a/src/main/java/ce2team1/mentoview/entity/User.java +++ b/src/main/java/ce2team1/mentoview/entity/User.java @@ -28,7 +28,7 @@ public class User extends AuditingFields { @Column(unique = true, nullable = false) private String email; // OAuth + 폼 - @Column(nullable = true) + @Column(nullable = false) private String password;//폼전용 private String name; @@ -47,12 +47,12 @@ public class User extends AuditingFields { private String billingKey; - public static User of(String email, String password, String name, Role role, SocialProvider socialProvider, String providerId, boolean isForm, UserStatus status, String billingKey ) { + public static User of(String email, String password, String name, Role role, SocialProvider socialProvider, String providerId, boolean isForm, UserStatus status, String billingKey ) { return new User( null, email, - password, // ✅ 소셜 로그인 사용자는 비밀번호 입력 필수 + password != null ? password : "", // ✅ 소셜 로그인 사용자는 비밀번호 입력 필수 name, role != null ? role : Role.USER, // 기본값: USER socialProvider, @@ -65,21 +65,17 @@ public static User toEntity(UserDto userDto) { return User.builder() .userId(userDto.getUserId()) .email(userDto.getEmail()) - .password(userDto.getPassword()) + .password(userDto.getPassword() != null ? userDto.getPassword() : "") .name(userDto.getName()) .role(userDto.getRole()) .socialProvider(userDto.getSocialProvider()) .providerId(userDto.getProviderId()) .status(userDto.getStatus()) + .billingKey(userDto.getBillingKey()) .build(); } -/* - // OAuth2용 - public void updateSocialInfo(String newProviderId) { - this.providerId = newProviderId; - } -*/ + public User updateBillingKey(String billingKey) { return this.toBuilder() diff --git a/src/main/java/ce2team1/mentoview/entity/atrribute/AuditingFields.java b/src/main/java/ce2team1/mentoview/entity/atrribute/AuditingFields.java index e11f0d6..426fe2c 100644 --- a/src/main/java/ce2team1/mentoview/entity/atrribute/AuditingFields.java +++ b/src/main/java/ce2team1/mentoview/entity/atrribute/AuditingFields.java @@ -18,18 +18,15 @@ public class AuditingFields { @Column(name = "created_at", updatable = false, length = 20) @CreatedDate - protected LocalDateTime createdAt ; - - + private LocalDateTime createdAt ; @CreatedBy - @Column(name = "created_by",updatable = false, length = 20) - protected String createdBy; + @Column(name = "created_by",updatable = false, length = 50) + private String createdBy; @LastModifiedDate - @Column(name = "modified_at") - protected LocalDateTime modifiedAt; - + @Column(name = "modified_at", updatable = true, length = 50) + private LocalDateTime modifiedAt; @LastModifiedBy - @Column(name = "modified_by", updatable = true, length = 20) - protected String modifiedBy; + @Column(name = "modified_by", updatable = true, length = 50) + private String modifiedBy; } diff --git a/src/main/java/ce2team1/mentoview/entity/atrribute/LoginType.java b/src/main/java/ce2team1/mentoview/entity/atrribute/LoginType.java deleted file mode 100644 index 447a623..0000000 --- a/src/main/java/ce2team1/mentoview/entity/atrribute/LoginType.java +++ /dev/null @@ -1,8 +0,0 @@ -package ce2team1.mentoview.entity.atrribute; - -import com.fasterxml.jackson.annotation.JsonFormat; - -@JsonFormat(shape = JsonFormat.Shape.STRING) -public enum LoginType { - FORM, OAUTH2, BOTH -} diff --git a/src/main/java/ce2team1/mentoview/entity/atrribute/SubscriptionPlan.java b/src/main/java/ce2team1/mentoview/entity/atrribute/SubscriptionPlan.java index f117d70..80122d2 100644 --- a/src/main/java/ce2team1/mentoview/entity/atrribute/SubscriptionPlan.java +++ b/src/main/java/ce2team1/mentoview/entity/atrribute/SubscriptionPlan.java @@ -4,7 +4,8 @@ @JsonFormat(shape = JsonFormat.Shape.STRING) public enum SubscriptionPlan { - FREE_TIRE, + FREE_TIER, + BASIC, PREMIUM; } diff --git a/src/main/java/ce2team1/mentoview/entity/atrribute/UserStatus.java b/src/main/java/ce2team1/mentoview/entity/atrribute/UserStatus.java index dff6803..b9e5f26 100644 --- a/src/main/java/ce2team1/mentoview/entity/atrribute/UserStatus.java +++ b/src/main/java/ce2team1/mentoview/entity/atrribute/UserStatus.java @@ -5,9 +5,8 @@ @JsonFormat(shape = JsonFormat.Shape.STRING) public enum UserStatus { ACTIVE, //("활성") - INACTIVE, //("미인증") - LOCKED, //("잠금") SUSPENDED, //("정지") + DORMANT, // ("휴면") DELETED; //("탈퇴") } diff --git a/src/main/java/ce2team1/mentoview/repository/InterviewQuestionRepository.java b/src/main/java/ce2team1/mentoview/repository/InterviewQuestionRepository.java index c946820..579db3c 100644 --- a/src/main/java/ce2team1/mentoview/repository/InterviewQuestionRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/InterviewQuestionRepository.java @@ -1,8 +1,10 @@ package ce2team1.mentoview.repository; import ce2team1.mentoview.entity.InterviewQuestion; +import org.hibernate.annotations.BatchSize; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Collection; import java.util.List; @@ -12,8 +14,17 @@ public interface InterviewQuestionRepository extends JpaRepository findQuestionsWithResponsesAndFeedback(Long interviewId); List findAllByQuestionIdIn(Collection questionIds); -} \ No newline at end of file + + @Query("select iq from InterviewQuestion iq " + + "left join fetch iq.interviewResponse ir " + + "left join fetch iq.interviewFeedback ifb " + + "where iq.interview.interviewId in :interviewIds " + + "order by iq.questionId ASC") + @BatchSize(size = 50) + List findAllWithResponseAndFeedback(@Param("interviewIds") Collection interviewIds); +} diff --git a/src/main/java/ce2team1/mentoview/repository/InterviewRepository.java b/src/main/java/ce2team1/mentoview/repository/InterviewRepository.java index e5444ff..7a36354 100644 --- a/src/main/java/ce2team1/mentoview/repository/InterviewRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/InterviewRepository.java @@ -2,9 +2,19 @@ import ce2team1.mentoview.entity.Interview; 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 org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.List; public interface InterviewRepository extends JpaRepository { List findAllByResumeResumeId(Long resumeId); + + @Query("select i from Interview i " + + "where i.resume.resumeId in :resumeIds") + List findAllByResumeId(@Param("resumeIds") Collection resumeIds); + } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/repository/PaymentRepository.java b/src/main/java/ce2team1/mentoview/repository/PaymentRepository.java index 4f9b5af..a118b2e 100644 --- a/src/main/java/ce2team1/mentoview/repository/PaymentRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/PaymentRepository.java @@ -2,9 +2,12 @@ import ce2team1.mentoview.entity.Payment; 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 org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.List; public interface PaymentRepository extends JpaRepository { @@ -12,4 +15,12 @@ public interface PaymentRepository extends JpaRepository { // List findAllBySubId(@Param("subId") Long subId); List findBySubscription_SubId(Long subId); + + @Query("select p from Payment p join fetch p.subscription where p.subscription.subId in :subIds") + List findBySubIds(@Param("subIds") Collection subIds); + + @Transactional + @Modifying + @Query("delete from Payment p") + int deleteFirstBy(); } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/repository/RefreshTokenRepository.java b/src/main/java/ce2team1/mentoview/repository/RefreshTokenRepository.java index 7f800a7..6ae0c34 100644 --- a/src/main/java/ce2team1/mentoview/repository/RefreshTokenRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/RefreshTokenRepository.java @@ -3,9 +3,16 @@ import ce2team1.mentoview.security.entity.RefreshToken; import jakarta.transaction.Transactional; 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 org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; +@Repository public interface RefreshTokenRepository extends JpaRepository { Boolean existsByRefreshToken(String refreshToken); @@ -14,4 +21,18 @@ public interface RefreshTokenRepository extends JpaRepository findByUserEmail(String email); + + @Query("select r from RefreshToken r where r.expirationDate < :threshold") // 임계값 + List findExpiringRefreshTokens(@Param("threshold") LocalDateTime threshold); + + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Transactional + @Query("delete from RefreshToken r where r.maxExpirationDate < :now") + int deleteMaxExpirationDate(@Param("now") LocalDateTime now); + + @Modifying + @Query("delete from RefreshToken r where r.userEmail = :email") + void deleteByUserEmail(String email); + } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/repository/ResumeRepository.java b/src/main/java/ce2team1/mentoview/repository/ResumeRepository.java index 2330055..7f44321 100644 --- a/src/main/java/ce2team1/mentoview/repository/ResumeRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/ResumeRepository.java @@ -3,6 +3,9 @@ import ce2team1.mentoview.entity.Resume; import ce2team1.mentoview.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + import java.util.List; public interface ResumeRepository extends JpaRepository { @@ -11,4 +14,7 @@ public interface ResumeRepository extends JpaRepository { long countAllByUserUserIdAndDeleteStatusIsFalse(Long userId); Long user(User user); + + @Query("select r from Resume r where r.user.userId = :userId") + List findAllByUserId(@Param("userId") Long userId); } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/repository/UserRepository.java b/src/main/java/ce2team1/mentoview/repository/UserRepository.java index 9e826f4..5eab44b 100644 --- a/src/main/java/ce2team1/mentoview/repository/UserRepository.java +++ b/src/main/java/ce2team1/mentoview/repository/UserRepository.java @@ -2,12 +2,16 @@ import ce2team1.mentoview.entity.User; +import ce2team1.mentoview.entity.atrribute.UserStatus; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByEmailAndProviderId(String email, String providerId); + + List findAllByStatus(UserStatus status); } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/security/controller/OAuth2Controller.java b/src/main/java/ce2team1/mentoview/security/controller/OAuth2Controller.java index 795d8c0..9f5b0a1 100644 --- a/src/main/java/ce2team1/mentoview/security/controller/OAuth2Controller.java +++ b/src/main/java/ce2team1/mentoview/security/controller/OAuth2Controller.java @@ -2,7 +2,7 @@ import ce2team1.mentoview.controller.dto.response.UserResp; import ce2team1.mentoview.entity.atrribute.Role; -import ce2team1.mentoview.security.JwtTokenProvider; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.dto.MvPrincipalDetails; import ce2team1.mentoview.service.UserService; import ce2team1.mentoview.service.dto.UserDto; @@ -28,14 +28,14 @@ @Slf4j @RestController @RequestMapping("/api/auth") -@Tag(name = "User API", description = "OAthu2") +@Tag(name = "Auth User API", description = "Auth") @RequiredArgsConstructor public class OAuth2Controller { private final UserService userService; private final JwtTokenProvider jwtTokenProvider; - @Operation(summary = "OAuth2", description = "OAuth2 Token && user data") + @Operation(summary = "Auth", description = "Auth Token && user data") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OAuth2 user"), @ApiResponse(responseCode = "400", description = "NOT_FOUND") @@ -46,6 +46,8 @@ public ResponseEntity getUserInfo (@AuthenticationPrincipal MvPrincipa HttpServletRequest request) { log.info("mvPrincipalDetails: {}", mvPrincipalDetails); + UserDto dto = mvPrincipalDetails.getUserDto(); + System.out.println("dto: " + dto); String existingToken = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)) .orElseThrow(() -> new AuthenticationServiceException("미인증 유저")); diff --git a/src/main/java/ce2team1/mentoview/security/controller/TokenController.java b/src/main/java/ce2team1/mentoview/security/controller/TokenController.java index 8bbf6ca..1acf695 100644 --- a/src/main/java/ce2team1/mentoview/security/controller/TokenController.java +++ b/src/main/java/ce2team1/mentoview/security/controller/TokenController.java @@ -1,7 +1,7 @@ package ce2team1.mentoview.security.controller; import ce2team1.mentoview.entity.atrribute.Role; -import ce2team1.mentoview.security.JwtTokenProvider; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.service.RefreshTokenService; import io.jsonwebtoken.ExpiredJwtException; import io.swagger.v3.oas.annotations.Operation; @@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -23,6 +24,11 @@ public class TokenController { private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenService refreshTokenService; + /* + * todo + * "Beare " : substring(0,7) => 로직 추가 + * */ + @Operation(summary = "Access Token", description = "Access Token 증명 & 발급") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "토큰 발급 완료"), @@ -34,23 +40,22 @@ public class TokenController { public ResponseEntity refreshAccessToken(HttpServletRequest request, HttpServletResponse response) { String authHeader = request.getHeader("Authorization"); //토큰 + String token = authHeader.substring(7); - String emailFromToken = jwtTokenProvider.getEmailFromToken(authHeader);//email + String emailFromToken = jwtTokenProvider.getEmailFromToken(token);//email String refreshToken = refreshTokenService.getRefreshToken(emailFromToken); // refresh if (refreshToken == null) { return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST); } - try { jwtTokenProvider.isExpired(refreshToken); } catch (ExpiredJwtException e) { - return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST); } - String type = jwtTokenProvider.getType(authHeader); + String type = jwtTokenProvider.getType(token); if (!type.equals("access")) { return new ResponseEntity<>("invalid token", HttpStatus.FORBIDDEN); } @@ -59,9 +64,10 @@ public ResponseEntity refreshAccessToken(HttpServletRequest request, HttpServ String emailFromRefresh = jwtTokenProvider.getEmailFromToken(refreshToken); String accessToken = jwtTokenProvider.createAccessToken(emailFromRefresh, roleFromToken); //엑세스 - response.setHeader("Authorization", accessToken); - return new ResponseEntity<>(HttpStatus.OK); + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + return new ResponseEntity<>(headers,HttpStatus.OK); } } diff --git a/src/main/java/ce2team1/mentoview/security/dto/LoginType.java b/src/main/java/ce2team1/mentoview/security/dto/LoginType.java new file mode 100644 index 0000000..b2e8e05 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/dto/LoginType.java @@ -0,0 +1,25 @@ +package ce2team1.mentoview.security.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +@Getter +@RequiredArgsConstructor +public enum LoginType { + FORM("from"), + OAUTH2("oauth2"), + OIDC("oidc"); + + + private final String code; + + public static LoginType ByCode(String code) { + return Arrays.stream(values()).filter(loginType + -> loginType.code.equals(code)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 LoginType: " + code)); + } + +} diff --git a/src/main/java/ce2team1/mentoview/security/dto/MvPrincipalDetails.java b/src/main/java/ce2team1/mentoview/security/dto/MvPrincipalDetails.java index 871071d..921ff63 100644 --- a/src/main/java/ce2team1/mentoview/security/dto/MvPrincipalDetails.java +++ b/src/main/java/ce2team1/mentoview/security/dto/MvPrincipalDetails.java @@ -26,18 +26,24 @@ public class MvPrincipalDetails implements OAuth2User, UserDetails{ private final UserDto userDto; private final Map attributes; private final OidcUser oidcUser; // OidcUser + private final LoginType loginType; - public static MvPrincipalDetails of(UserDto userDto) { //폼 - return new MvPrincipalDetails(userDto, Collections.emptyMap(), null); + public static MvPrincipalDetails of(UserDto userDto) { + return new MvPrincipalDetails(userDto, Collections.emptyMap(), null,null); } - public static MvPrincipalDetails of(UserDto userDto, Map attributes) {//OAuth2User전용 - return new MvPrincipalDetails(userDto, attributes, null); + public static MvPrincipalDetails of(UserDto userDto, LoginType loginType) { //폼 + return new MvPrincipalDetails(userDto, Collections.emptyMap(), null,loginType); } - public static MvPrincipalDetails of(UserDto userDto, OidcUser oidcUser) { // OIDC 구현체 전용 - return new MvPrincipalDetails(userDto, oidcUser.getAttributes(), oidcUser); + public static MvPrincipalDetails of(UserDto userDto, Map attributes, LoginType loginType) {//OAuth2User전용 + return new MvPrincipalDetails(userDto, attributes, null, loginType); + + } + public static MvPrincipalDetails of(UserDto userDto, OidcUser oidcUser, LoginType loginType) { // OIDC 구현체 전용 + return new MvPrincipalDetails(userDto, oidcUser.getAttributes(), oidcUser,loginType); } + public UserDto getUserDto() { return userDto; } @@ -54,7 +60,10 @@ public String getUsername() { @Override public String getPassword() { - return userDto.getPassword(); + if (userDto == null) { + return ""; + } + return userDto.getPassword() != null ? userDto.getPassword() : ""; } public Long getUserId() { // Long id @@ -65,7 +74,8 @@ public static UserDto toDto(MvPrincipalDetails mvPrincipalDetails) { return UserDto.builder() .userId(mvPrincipalDetails.getUserId()) .email(mvPrincipalDetails.getName()) - .name(mvPrincipalDetails.getUsername()) + .password(mvPrincipalDetails.getPassword() != null ? mvPrincipalDetails.getPassword() : "") + .name(mvPrincipalDetails.getRealName()) .build(); } @@ -76,7 +86,7 @@ public String getRealName() { @Override public boolean isEnabled() { - return userDto.getStatus() == UserStatus.ACTIVE; + return userDto.getStatus() != UserStatus.DELETED; } @Override diff --git a/src/main/java/ce2team1/mentoview/security/dto/RefreshTokenDto.java b/src/main/java/ce2team1/mentoview/security/dto/RefreshTokenDto.java index bbc5297..423a276 100644 --- a/src/main/java/ce2team1/mentoview/security/dto/RefreshTokenDto.java +++ b/src/main/java/ce2team1/mentoview/security/dto/RefreshTokenDto.java @@ -13,10 +13,11 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RefreshTokenDto { - Long id; - String userEmail; - String refreshToken; - LocalDateTime expirationDate; + private Long id; + private String userEmail; + private String refreshToken; + private LocalDateTime expirationDate; + private LocalDateTime maxExpirationDate; public static RefreshTokenDto of(String userEmail, String refreshToken, LocalDateTime expirationDate) { @@ -24,7 +25,8 @@ public static RefreshTokenDto of(String userEmail, String refreshToken, LocalDat null, userEmail, refreshToken, - expirationDate + expirationDate, + expirationDate.plusDays(RefreshToken.MAX_EXPIRATION_DAYS) ); } @@ -34,6 +36,7 @@ public static RefreshTokenDto toDto(RefreshToken refreshToken) { .userEmail(refreshToken.getUserEmail()) .refreshToken(refreshToken.getRefreshToken()) .expirationDate(refreshToken.getExpirationDate()) + .maxExpirationDate(refreshToken.getMaxExpirationDate()) .build(); } } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/security/entity/RefreshToken.java b/src/main/java/ce2team1/mentoview/security/entity/RefreshToken.java index 76abb59..27e25e2 100644 --- a/src/main/java/ce2team1/mentoview/security/entity/RefreshToken.java +++ b/src/main/java/ce2team1/mentoview/security/entity/RefreshToken.java @@ -1,14 +1,11 @@ package ce2team1.mentoview.security.entity; import ce2team1.mentoview.security.dto.RefreshTokenDto; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; -@Builder +@Builder(toBuilder = true) @Getter @Entity @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -17,24 +14,34 @@ public class RefreshToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private String userEmail; + @Column(nullable = false) private String refreshToken; + @Column(nullable = false) private LocalDateTime expirationDate; + @Column(nullable = false) + private LocalDateTime maxExpirationDate; - public static RefreshToken of(String email, String refreshToken, LocalDateTime expirationDate) { + public static final long MAX_EXPIRATION_DAYS = 14; + + public static RefreshToken of(String userEmail, String refreshToken, LocalDateTime expirationDate) { return new RefreshToken( null, - email, + userEmail, refreshToken, - expirationDate + expirationDate, + expirationDate.plusDays(MAX_EXPIRATION_DAYS) ); } - public static RefreshToken of(Long id, String email, String refreshToken, LocalDateTime expirationDate) { + + public static RefreshToken of(Long id, String userEmail, String refreshToken, LocalDateTime expirationDate) { return new RefreshToken( id, - email, + userEmail, refreshToken, - expirationDate + expirationDate, + expirationDate.plusDays(MAX_EXPIRATION_DAYS) ); } public static RefreshToken toEntity(RefreshTokenDto tokenDto) { @@ -42,10 +49,13 @@ public static RefreshToken toEntity(RefreshTokenDto tokenDto) { .userEmail(tokenDto.getUserEmail()) .refreshToken(tokenDto.getRefreshToken()) .expirationDate(tokenDto.getExpirationDate()) + .maxExpirationDate(tokenDto.getExpirationDate().plusDays(MAX_EXPIRATION_DAYS)) .build(); } - - + public void updateRefreshToken(String newRefreshToken) { + this.refreshToken = newRefreshToken; + this.expirationDate = this.expirationDate.plusDays(7); + } } diff --git a/src/main/java/ce2team1/mentoview/security/filter/DeletedUserFilter.java b/src/main/java/ce2team1/mentoview/security/filter/DeletedUserFilter.java new file mode 100644 index 0000000..e55fe0e --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/filter/DeletedUserFilter.java @@ -0,0 +1,52 @@ +package ce2team1.mentoview.security.filter; + +import ce2team1.mentoview.entity.User; +import ce2team1.mentoview.entity.atrribute.UserStatus; +import ce2team1.mentoview.repository.UserRepository; +import ce2team1.mentoview.security.dto.MvPrincipalDetails; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Slf4j +@RequiredArgsConstructor +public class DeletedUserFilter extends OncePerRequestFilter { + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.getPrincipal() instanceof MvPrincipalDetails principalDetails) { + Long userId = principalDetails.getUserId(); + UserStatus userStatus = userRepository.findById(userId).map(User::getStatus) + .orElse(null);// 유저 업으면 null + if (userStatus == UserStatus.DELETED) { + log.warn("탈퇴 유저 {}", userId); + SecurityContextHolder.clearContext(); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "탈퇴유저"); + return; + } + if (userStatus == UserStatus.SUSPENDED ) { + log.warn("제한된 유저 {}", userId); + SecurityContextHolder.clearContext(); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "접근 제한된 계정"); + return; + } + } + filterChain.doFilter(request, response); + + + } +} diff --git a/src/main/java/ce2team1/mentoview/security/filter/LambdaRequestFilter.java b/src/main/java/ce2team1/mentoview/security/filter/LambdaRequestFilter.java new file mode 100644 index 0000000..ddfdf24 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/filter/LambdaRequestFilter.java @@ -0,0 +1,50 @@ +package ce2team1.mentoview.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +public class LambdaRequestFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + private static final String ENDPOINT = "/api/interview/response/transcription"; + + @Value("${aws.lambda.header}") + private String lambdaHeader; + @Value("${aws.lambda.secret-key}") + private String lambdaSecretKey; + + public LambdaRequestFilter() { + this.requestMatcher = new AntPathRequestMatcher(ENDPOINT); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 요청이 특정 API에 해당하지 않으면 필터를 건너뜀 + if (!requestMatcher.matches(request)) { + chain.doFilter(request, response); + return; + } + + // AWS Lambda 요청을 식별하는 헤더 값 확인 + String lambdaApiKey = request.getHeader(lambdaHeader); + if (!lambdaSecretKey.equals(lambdaApiKey)) { + log.info("============== Wrong Lambda Key : {} ================", lambdaApiKey); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized Lambda request."); + return; + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/ce2team1/mentoview/security/MvLoginFormFilter.java b/src/main/java/ce2team1/mentoview/security/filter/MvLoginFormFilter.java similarity index 98% rename from src/main/java/ce2team1/mentoview/security/MvLoginFormFilter.java rename to src/main/java/ce2team1/mentoview/security/filter/MvLoginFormFilter.java index daaaf4f..d994f7c 100644 --- a/src/main/java/ce2team1/mentoview/security/MvLoginFormFilter.java +++ b/src/main/java/ce2team1/mentoview/security/filter/MvLoginFormFilter.java @@ -1,4 +1,4 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.filter; import ce2team1.mentoview.security.dto.UserFormLogin; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/ce2team1/mentoview/security/MvRequestFilter.java b/src/main/java/ce2team1/mentoview/security/filter/MvRequestFilter.java similarity index 83% rename from src/main/java/ce2team1/mentoview/security/MvRequestFilter.java rename to src/main/java/ce2team1/mentoview/security/filter/MvRequestFilter.java index 70be5ee..bca79f0 100644 --- a/src/main/java/ce2team1/mentoview/security/MvRequestFilter.java +++ b/src/main/java/ce2team1/mentoview/security/filter/MvRequestFilter.java @@ -1,7 +1,8 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.filter; -import ce2team1.mentoview.entity.atrribute.Role; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.dto.MvPrincipalDetails; +import ce2team1.mentoview.service.UserService; import ce2team1.mentoview.service.dto.UserDto; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -24,12 +25,13 @@ public class MvRequestFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; - @Override +/* @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); return path.startsWith("/api/oauth2/authorization") || path.startsWith("/api/login/oauth2/code"); - } + }*/ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -45,7 +47,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String authToken = bearerToken.substring(7).trim(); if (jwtTokenProvider.isExpired(authToken)) { - String emailFromToken = jwtTokenProvider.getEmailFromToken(authToken); + //String emailFromToken = jwtTokenProvider.getEmailFromToken(authToken); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); @@ -73,12 +75,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } String emailFromToken = jwtTokenProvider.getEmailFromToken(authToken); - Role roleFromToken = jwtTokenProvider.getRoleFromToken(authToken); - + //Role roleFromToken = jwtTokenProvider.getRoleFromToken(authToken); + + UserDto userDto = userService.findByEmail(emailFromToken); - MvPrincipalDetails mvPrincipalDetails = MvPrincipalDetails.of(UserDto.of(emailFromToken, roleFromToken)); + MvPrincipalDetails mvPrincipalDetails = MvPrincipalDetails.of(userDto); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(mvPrincipalDetails,null, mvPrincipalDetails.getAuthorities()); // null, 비번 + //SecurityContext context = SecurityContextHolder.createEmptyContext(); + //context.setAuthentication(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); diff --git a/src/main/java/ce2team1/mentoview/security/filter/WebhookFilter.java b/src/main/java/ce2team1/mentoview/security/filter/WebhookFilter.java new file mode 100644 index 0000000..2dbbc78 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/filter/WebhookFilter.java @@ -0,0 +1,59 @@ +package ce2team1.mentoview.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +public class WebhookFilter extends OncePerRequestFilter { + private final List allowedIps = List.of("52.78.5.241"); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + // 요청 경로 확인 + String requestUri = request.getRequestURI(); + + if (!requestUri.startsWith("/api/webhook/")) { + // 해당 경로가 아니면 필터를 통과 + chain.doFilter(request, response); + return; + } + + // X-Forwarded-For 헤더에서 클라이언트 IP 주소 가져오기 + String remoteAddr = request.getHeader("X-Forwarded-For"); + + // X-Forwarded-For가 없거나 비어있으면 기본 remoteAddr 가져오기 + if (remoteAddr == null || remoteAddr.isEmpty()) { + remoteAddr = request.getRemoteAddr(); + } else { + // X-Forwarded-For 헤더는 여러 IP 주소를 포함할 수 있으므로, + // 가장 왼쪽의 IP 주소를 가져옵니다. + String[] ipAddresses = remoteAddr.split(","); + remoteAddr = ipAddresses[0].trim(); // 첫 번째 IP 주소 + } + + System.out.println("Webhook 요청 IP: " + remoteAddr); + + if (!allowedIps.contains(remoteAddr)) { + System.out.println("허용되지 않은 IP 접근 차단: " + remoteAddr); + response.getWriter().write("Forbidden"); + return; + } + + chain.doFilter(request, response); + } +} + + + + + + diff --git a/src/main/java/ce2team1/mentoview/security/handler/MvLogoutHandler.java b/src/main/java/ce2team1/mentoview/security/handler/MvLogoutHandler.java new file mode 100644 index 0000000..4893d7f --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/handler/MvLogoutHandler.java @@ -0,0 +1,58 @@ +package ce2team1.mentoview.security.handler; + +import ce2team1.mentoview.exception.ServiceException; +import ce2team1.mentoview.security.service.JwtTokenProvider; +import ce2team1.mentoview.security.service.RefreshTokenService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.PrintWriter; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MvLogoutHandler implements LogoutHandler { + + private final RefreshTokenService refreshTokenService; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + + try { + String authorization = request.getHeader("Authorization"); + if (authorization == null || !authorization.startsWith("Bearer ")) { + extracted(response, HttpServletResponse.SC_BAD_REQUEST, "{\"code\" : \"INVALID_REQUEST\", \"message\" : \" 토큰이 없습니다\"}"); + return; + } + if (authorization != null && authorization.startsWith("Bearer ")) { + authorization = authorization.substring(7); + String email = jwtTokenProvider.getEmailFromToken(authorization); + refreshTokenService.deleteRefreshToken(email); + extracted(response, HttpServletResponse.SC_ACCEPTED, "{\"code\" : \"LOGOUT_SUCCESS\", \"message\" : \" 로그아웃 성공, Access Token 삭제 요청\"}"); + return; + } + } catch (ServiceException e) { + extracted(response, HttpServletResponse.SC_BAD_REQUEST, "{\"code\":\"TOKEN_NOT_FOUND\", \"message\":\"삭제할 토큰이 없습니다.\"}"); + } catch (Exception e) { + extracted(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "{\"code\":\"LOGOUT_ERROR\", \"message\":\"로그아웃 처리 중 오류 발생\"}"); + } + } + private void extracted(HttpServletResponse response, int status, String message) { + response.setStatus(status); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + try (PrintWriter writer = response.getWriter()) { + writer.write(message); + writer.flush(); + } catch (IOException e) { + log.error("{}",e.getMessage(),e); + } + } +} diff --git a/src/main/java/ce2team1/mentoview/security/MvOAuth2FormFailureHandler.java b/src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormFailureHandler.java similarity index 97% rename from src/main/java/ce2team1/mentoview/security/MvOAuth2FormFailureHandler.java rename to src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormFailureHandler.java index d2b9c48..9eb7241 100644 --- a/src/main/java/ce2team1/mentoview/security/MvOAuth2FormFailureHandler.java +++ b/src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormFailureHandler.java @@ -1,4 +1,4 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.handler; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/ce2team1/mentoview/security/MvOAuth2FormSuccessHandler.java b/src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormSuccessHandler.java similarity index 64% rename from src/main/java/ce2team1/mentoview/security/MvOAuth2FormSuccessHandler.java rename to src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormSuccessHandler.java index 4cbb7da..ff0bd21 100644 --- a/src/main/java/ce2team1/mentoview/security/MvOAuth2FormSuccessHandler.java +++ b/src/main/java/ce2team1/mentoview/security/handler/MvOAuth2FormSuccessHandler.java @@ -1,7 +1,9 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.handler; import ce2team1.mentoview.entity.atrribute.Role; +import ce2team1.mentoview.security.dto.LoginType; import ce2team1.mentoview.security.dto.MvPrincipalDetails; +import ce2team1.mentoview.security.service.JwtTokenProvider; import ce2team1.mentoview.security.service.RefreshTokenService; import ce2team1.mentoview.service.dto.UserDto; import jakarta.servlet.ServletException; @@ -13,7 +15,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; @@ -34,26 +35,24 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("‼️‼️‼️‼️‼️‼️‼️ OAuth2 로그인 성공: {}", authentication.getPrincipal().getClass()); MvPrincipalDetails mvPrincipalDetails; - if (authentication.getPrincipal() instanceof OidcUser oidcUser) { + if (authentication.getPrincipal() instanceof MvPrincipalDetails principalDetails) { + log.info("‼️‼️‼️‼️ MvPrincipalDetails 로그인 처리"); + mvPrincipalDetails = principalDetails; - log.info("‼️‼️‼️‼️‼️‼️‼ OidcUser 로그인 처리"); + } else if (authentication.getPrincipal() instanceof OidcUser oidcUser) { + log.info("‼️‼️‼️‼️‼️‼️‼️ OidcUser 로그인 처리"); UserDto userDto = UserDto.byOAuth2User(oidcUser); - mvPrincipalDetails = MvPrincipalDetails.of(userDto, oidcUser); + mvPrincipalDetails = MvPrincipalDetails.of(userDto, LoginType.OIDC); } else if (authentication.getPrincipal() instanceof OAuth2User oAuth2User) { - - log.info("‼️‼️‼️‼️‼️‼️‼ OAuth2User 로그인 처리"); + log.info("‼️‼️‼️‼️‼️‼️‼️ OAuth2User 로그인 처리"); UserDto userDto = UserDto.byOAuth2User(oAuth2User); - mvPrincipalDetails = MvPrincipalDetails.of(userDto, oAuth2User.getAttributes()); - - } else if (authentication.getPrincipal() instanceof UserDetails userDetails) { - - log.info("‼️‼️‼️‼️‼️‼️‼ UserDetails 로그인 처리"); - mvPrincipalDetails = (MvPrincipalDetails) userDetails; + mvPrincipalDetails = MvPrincipalDetails.of(userDto, LoginType.OAUTH2); - }else { - throw new IllegalStateException("지원하지 않는 OAuth2 인증 타입입니다."); + } else { + throw new IllegalStateException("지원하지 않는 인증 타입입니다: " + authentication.getPrincipal().getClass()); } + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(mvPrincipalDetails, null, mvPrincipalDetails.getAuthorities()); @@ -61,27 +60,37 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("️‼️‼️‼️‼️‼️‼ SecurityContext에 저장된 Authentication: {}", SecurityContextHolder.getContext().getAuthentication()); - String userEmail = mvPrincipalDetails.getName(); + log.info("‼️‼️‼11111111‼️‼️‼️‼️userEmail 확인 = {}", userEmail); Collection authorities = authentication.getAuthorities(); Iterator iterator = authorities.iterator(); GrantedAuthority auth = iterator.next(); String role = auth.getAuthority(); + log.info("‼️‼️‼️222‼️‼️‼️‼️userEmail 확인 = {}", userEmail); String temporaryToken = jwtTokenProvider.createTemporaryToken(userEmail, Role.toCode(role));// 2분 String realRefreshToken = jwtTokenProvider.createRefreshToken(userEmail, Role.toCode(role));// 7일 // 우리가 만든 refreshToken 디비로 저장 + log.info("‼️‼️‼️333333‼️‼️‼️‼️userEmail 확인 = {}", userEmail); refreshTokenService.updateOrAddRefreshToken(userEmail, realRefreshToken, jwtTokenProvider.getRefreshTokenExpiration()); - String tokenUrl = String.format("http://localhost:3000/google-login?token=%s", temporaryToken); - response.sendRedirect(tokenUrl); + String tokenUrl; //= String.format("http://localhost:3000/google-login?token=%s", temporaryToken); + + if (mvPrincipalDetails.getLoginType() != LoginType.FORM && mvPrincipalDetails.getPassword().isEmpty() ) { + tokenUrl = String.format("https://mentoview.site/mv-login?token=%s&ndg=%s", temporaryToken, "fa"); + } else { + tokenUrl = String.format("https://mentoview.site/mv-login?token=%s&ndg=%s", temporaryToken, "tu"); + } - log.info("jwtCookie{}" , temporaryToken); - log.info("jwtCookie{} 드림" , temporaryToken); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.sendRedirect(tokenUrl); + log.info("jwt{}" , temporaryToken); + log.info("jwt{} 드림" , temporaryToken); } } diff --git a/src/main/java/ce2team1/mentoview/security/JwtTokenProvider.java b/src/main/java/ce2team1/mentoview/security/service/JwtTokenProvider.java similarity index 87% rename from src/main/java/ce2team1/mentoview/security/JwtTokenProvider.java rename to src/main/java/ce2team1/mentoview/security/service/JwtTokenProvider.java index 75c688e..d0e1a48 100644 --- a/src/main/java/ce2team1/mentoview/security/JwtTokenProvider.java +++ b/src/main/java/ce2team1/mentoview/security/service/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.service; import ce2team1.mentoview.entity.atrribute.Role; import io.jsonwebtoken.Jwts; @@ -114,4 +114,17 @@ private String createToken(String type ,String email, Role role, Long expiratio .signWith(secretKey) .compact(); } + + // 토큰 생성! +/* private String createTemp(String type , String email, Role role, Long expiration, UserStatus status) { + return Jwts.builder() + .claim("type", type) + .claim("email", email) + .claim("role", role.getCode()) + .claim("status", status.name()) + .issuedAt(new Date(System.currentTimeMillis())) //시간 + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secretKey) + .compact(); + }*/ } diff --git a/src/main/java/ce2team1/mentoview/security/service/MvOAuth2UserService.java b/src/main/java/ce2team1/mentoview/security/service/MvOAuth2UserService.java index c0833db..1f4e4f9 100644 --- a/src/main/java/ce2team1/mentoview/security/service/MvOAuth2UserService.java +++ b/src/main/java/ce2team1/mentoview/security/service/MvOAuth2UserService.java @@ -5,6 +5,7 @@ import ce2team1.mentoview.entity.atrribute.UserStatus; import ce2team1.mentoview.repository.UserRepository; import ce2team1.mentoview.security.dto.GoogleOAuth2Response; +import ce2team1.mentoview.security.dto.LoginType; import ce2team1.mentoview.security.dto.MvPrincipalDetails; import ce2team1.mentoview.security.dto.OAuth2ResponseSocial; import ce2team1.mentoview.service.dto.UserDto; @@ -50,7 +51,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic .orElseGet(() -> userRepository.save(User.toEntity( UserDto.of( responseSocial.getEmail(), - null, + "", responseSocial.getName(), Role.USER, responseSocial.getProvider(), @@ -60,16 +61,17 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic ) ))); - log.info(" ✅ 저장된 사용자: {}", repositoryUser); + UserDto userDto = UserDto.toDto(repositoryUser); + log.info(" ✅ 저장된 사용자: {}", userDto); // ✅ OAuth2User -> MvPrincipalDetails 변환 MvPrincipalDetails mvPrincipalDetails; if (oAuth2User instanceof OidcUser oidcUser) { log.info(" ✈️✈️✈️ OidcUser "); - mvPrincipalDetails = MvPrincipalDetails.of(UserDto.toDto(repositoryUser), oidcUser); + mvPrincipalDetails = MvPrincipalDetails.of(UserDto.toDto(repositoryUser), oidcUser, LoginType.OIDC); } else { log.info(" ⛴️⛴️⛴️ OAuth2User "); - mvPrincipalDetails = MvPrincipalDetails.of(UserDto.toDto(repositoryUser), oAuth2User.getAttributes()); + mvPrincipalDetails = MvPrincipalDetails.of(UserDto.toDto(repositoryUser), oAuth2User.getAttributes(),LoginType.OAUTH2); } return mvPrincipalDetails; } diff --git a/src/main/java/ce2team1/mentoview/security/service/RefreshTokenBatchJob.java b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenBatchJob.java new file mode 100644 index 0000000..4c0692a --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenBatchJob.java @@ -0,0 +1,82 @@ +package ce2team1.mentoview.security.service; + +import ce2team1.mentoview.entity.atrribute.Role; +import ce2team1.mentoview.repository.RefreshTokenRepository; +import ce2team1.mentoview.security.entity.RefreshToken; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RefreshTokenBatchJob { + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + private final EntityManager entityManager; + private final ApplicationEventPublisher eventPublisher; + private static final int BATCH_SIZE = 50; + + + //만료토큰삭제 + @Scheduled(cron = "0 0 0 * * ?") // 매일 00시 : 초,분,시,일,월,요일 + @Transactional + public void deleteExpiredTokens() { + log.info("Deleting expired tokens"); + try { + int deletedRefreshToken + = refreshTokenRepository.deleteMaxExpirationDate(LocalDateTime.now()); + if (deletedRefreshToken == 0) { + log.info("만료된 토큰 없음"); + } + log.info("Deleted expired tokens: " + deletedRefreshToken); + eventPublisher.publishEvent(new RefreshTokenDeletedEvent(LocalDateTime.now())); + log.info("작업 완료"); + } catch (Exception e) { + log.error("만료된 토큰 삭제 중 오류 발생 {}", e.getMessage(),e); + } + + } + + + @TransactionalEventListener + public void rotateRefreshToken(RefreshTokenDeletedEvent deletedEvent) { + log.info("만료 토큰 삭제 완료 ==> Start Rotating refresh token!!"); + + List expiringRefreshTokens = refreshTokenRepository.findExpiringRefreshTokens(LocalDateTime.now().minusMinutes(1)); + int totalExpiring = expiringRefreshTokens.size(); + int updatedTokens = 0; + int failedTokens = 0; + + int counter = 0; + + try { + for (RefreshToken refreshToken : expiringRefreshTokens) { + String newRefreshToken = jwtTokenProvider.createRefreshToken(refreshToken.getUserEmail(), Role.USER); + refreshToken.updateRefreshToken(newRefreshToken); + + updatedTokens++; + + counter++; + if (counter % BATCH_SIZE == 0) { + entityManager.flush(); + entityManager.clear(); + } + } + } catch (Exception e) { + failedTokens++; + log.error("리프레시 토큰 갱신 중 오류발생 {} "+e.getMessage(), e); + } + log.info("Rotating refresh token!! ==> 만료 토큰 갱신 완료 "); + log.info("조회된 만료 예정 토큰 : {}, 갱신 성공 : {}, 갱신 실패 : {}", totalExpiring , updatedTokens, failedTokens); + } + +} diff --git a/src/main/java/ce2team1/mentoview/security/service/RefreshTokenDeletedEvent.java b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenDeletedEvent.java new file mode 100644 index 0000000..3f2acb3 --- /dev/null +++ b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenDeletedEvent.java @@ -0,0 +1,14 @@ +package ce2team1.mentoview.security.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class RefreshTokenDeletedEvent { + + private final LocalDateTime eventTime; + +} diff --git a/src/main/java/ce2team1/mentoview/security/service/RefreshTokenService.java b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenService.java index d408851..a264884 100644 --- a/src/main/java/ce2team1/mentoview/security/service/RefreshTokenService.java +++ b/src/main/java/ce2team1/mentoview/security/service/RefreshTokenService.java @@ -4,31 +4,66 @@ import ce2team1.mentoview.security.dto.RefreshTokenDto; import ce2team1.mentoview.security.entity.RefreshToken; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.Optional; +@Slf4j @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class RefreshTokenService { private final RefreshTokenRepository refreshTokenRepository; + public String getRefreshToken(String email) { + return refreshTokenRepository.findByUserEmail(email).map( + refreshToken -> refreshToken.getRefreshToken()) + .orElseThrow(() -> new AuthenticationServiceException( "No refresh token found for email: " + email)); + } + @Transactional(readOnly = false) - public RefreshTokenDto addRefreshToken(String userEmail, String refreshToken, Long expiration) { - LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(expiration), ZoneId.systemDefault()); - RefreshTokenDto tokenDto = RefreshTokenDto.of(userEmail, refreshToken, dateTime); - refreshTokenRepository.save(RefreshToken.toEntity(tokenDto)); - return tokenDto; + public void updateOrAddRefreshToken(String userEmail, String realRefreshToken, Long expiration) { + + log.info("‼️‼️‼️‼️‼️‼️‼️userEmail 확인 = {}", userEmail); // 메서드 호출 전 + + LocalDateTime expirationDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(expiration), ZoneId.of("Asia/Seoul")); //테이블용 시간 + + RefreshTokenDto tokenDto = RefreshTokenDto.of(userEmail, realRefreshToken, expirationDate); //RefreshDto 토큰 객체 + + Optional existToken = refreshTokenRepository.findByUserEmail(userEmail); // DB에 Refresh 토큰 객체를 email로 찾음 + if (!existToken.isPresent()) { // 디비에 객체 없어? + RefreshToken newRefreshToken = refreshTokenRepository.save(RefreshToken.toEntity(tokenDto)); + + } else {// 디비에 객체 있어? + RefreshToken updateRefreshToken = existToken.get().toBuilder() + .refreshToken(realRefreshToken) + .expirationDate(expirationDate) + .build(); + RefreshToken existedToken = refreshTokenRepository.save(updateRefreshToken); + } + } + @Transactional(readOnly = false) + public void deleteRefreshToken(String email) { + refreshTokenRepository.deleteByUserEmail(email); } - public String getRefreshToken(String email) { - return refreshTokenRepository.findByUserEmail(email).map( - refreshToken -> refreshToken.getRefreshToken()).orElse(null); + public RefreshTokenDto findByEmail(String email) { + RefreshToken refreshToken = refreshTokenRepository.findByUserEmail(email).orElseThrow( + () -> new AuthenticationServiceException("No refresh token found for email: " + email)); + return RefreshTokenDto.toDto(refreshToken); + } + } diff --git a/src/main/java/ce2team1/mentoview/security/MvHttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/ce2team1/mentoview/security/social/MvHttpCookieOAuth2AuthorizationRequestRepository.java similarity index 98% rename from src/main/java/ce2team1/mentoview/security/MvHttpCookieOAuth2AuthorizationRequestRepository.java rename to src/main/java/ce2team1/mentoview/security/social/MvHttpCookieOAuth2AuthorizationRequestRepository.java index fad6b8e..c4d380a 100644 --- a/src/main/java/ce2team1/mentoview/security/MvHttpCookieOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/ce2team1/mentoview/security/social/MvHttpCookieOAuth2AuthorizationRequestRepository.java @@ -1,4 +1,4 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.social; import ce2team1.mentoview.utils.security.MvCookieUtils; import io.micrometer.common.util.StringUtils; diff --git a/src/main/java/ce2team1/mentoview/security/MvOAuth2ClientRegistration.java b/src/main/java/ce2team1/mentoview/security/social/MvOAuth2ClientRegistration.java similarity index 97% rename from src/main/java/ce2team1/mentoview/security/MvOAuth2ClientRegistration.java rename to src/main/java/ce2team1/mentoview/security/social/MvOAuth2ClientRegistration.java index 3b35aec..b659d44 100644 --- a/src/main/java/ce2team1/mentoview/security/MvOAuth2ClientRegistration.java +++ b/src/main/java/ce2team1/mentoview/security/social/MvOAuth2ClientRegistration.java @@ -1,4 +1,4 @@ -package ce2team1.mentoview.security; +package ce2team1.mentoview.security.social; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.oauth2.client.registration.ClientRegistration; diff --git a/src/main/java/ce2team1/mentoview/service/AiService.java b/src/main/java/ce2team1/mentoview/service/AiService.java index 907f7f8..3ed0573 100644 --- a/src/main/java/ce2team1/mentoview/service/AiService.java +++ b/src/main/java/ce2team1/mentoview/service/AiService.java @@ -50,7 +50,8 @@ public CompletableFuture generateFeedbackFromQA(GenerateFeedbackDto "너가 제공한 면접 질문은 아래와 같아. \n" + request.getQuestion() + "\n해당 면접 질문과 응답을 보고 피드백을 생성해줘. " + - "마크다운은 사용하지 말고 오직 피드백만 제공해."); + "마크다운은 사용하지 말고 오직 피드백만 제공해. " + + "\n만약 답변이 너무 짧거나 없으면 피드백으로 모범답안을 제공해."); UserMessage userMessage = new UserMessage(request.getAnswer()); ChatResponse response = openAiChatModel.call(new Prompt(systemMessage, userMessage)); diff --git a/src/main/java/ce2team1/mentoview/service/AwsS3Service.java b/src/main/java/ce2team1/mentoview/service/AwsS3Service.java index b1f40e6..2f3c969 100644 --- a/src/main/java/ce2team1/mentoview/service/AwsS3Service.java +++ b/src/main/java/ce2team1/mentoview/service/AwsS3Service.java @@ -5,7 +5,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Utilities; @@ -15,6 +18,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.UUID; @Slf4j @@ -106,4 +110,21 @@ public void deleteS3Object(String key) { .build(); s3Client.deleteObject(deleteObjectRequest); } + + public String interviewArchiveUpload(Long userId,String interviewDataJson) { + try { + String s3key = "archive/interview-" + userId + "-" + UUID.randomUUID().toString().substring(0, 8) + ".json"; + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(s3key) + .contentType("application/json") + .build(); + + s3Client.putObject(objectRequest, RequestBody.fromString(interviewDataJson, StandardCharsets.UTF_8)); + return s3key; + } catch (SdkException e) { + log.error("S3 archive 업로드 오류"+e.getMessage()); + throw new RuntimeException("S3 archive 업로드 오류", e); + } + } } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/service/PaymentService.java b/src/main/java/ce2team1/mentoview/service/PaymentService.java index a0acc11..c2d3933 100644 --- a/src/main/java/ce2team1/mentoview/service/PaymentService.java +++ b/src/main/java/ce2team1/mentoview/service/PaymentService.java @@ -30,7 +30,6 @@ public void createPayment(PaymentCheckDto paymentCheckDto) { // 사용자에게 활성화된 구독(status == 'ACTIVE')이 존재하는지 확인 Long subId = subscriptionService.checkSubscription(Long.valueOf(paymentCheckDto.getCustomer().getId())); - System.out.println(subId); SubscriptionDto subscription; if (subId != null){ @@ -38,7 +37,6 @@ public void createPayment(PaymentCheckDto paymentCheckDto) { } else { subscription = subscriptionService.createSubscription(paymentCheckDto); } - System.out.println(subscription.getStartDate()); Payment payment = PaymentDto.checkToDto(paymentCheckDto, subscription.getSubId()).toEntity(subscriptionService.getSubscriptionByUserId(subscription.getUserId(), SubscriptionStatus.ACTIVE)); paymentRepository.save(payment); diff --git a/src/main/java/ce2team1/mentoview/service/PdfService.java b/src/main/java/ce2team1/mentoview/service/PdfService.java index accb9b2..c2b0e57 100644 --- a/src/main/java/ce2team1/mentoview/service/PdfService.java +++ b/src/main/java/ce2team1/mentoview/service/PdfService.java @@ -1,6 +1,7 @@ package ce2team1.mentoview.service; import ce2team1.mentoview.exception.InterviewException; +import jakarta.annotation.PreDestroy; import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import org.apache.pdfbox.io.RandomAccessReadBuffer; @@ -8,35 +9,48 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import javax.imageio.ImageIO; +import java.awt.*; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; @Service public class PdfService { + // 클래스 레벨에서 전역 ExecutorService 생성 (Lazy Initialization) + private static final ExecutorService executor = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1); + + @Value("${tesseract.tessdata.path}") + private String tessDataPath; + + @PreDestroy + public void executorShutdown() { + executor.shutdown(); + } + public String extractTextFromPDF(InputStream inputStream) throws IOException { - // PDF 파일에 대해 텍스트를 추출하는 로직을 구현 - PDFParser parser = new PDFParser(new RandomAccessReadBuffer(inputStream)); + // PDF 파일 메모리로 로딩 + byte[] pdfBytes = inputStream.readAllBytes(); + + // PDFParser를 사용하여 PDF 문서 읽기 + PDFParser parser = new PDFParser(new RandomAccessReadBuffer(pdfBytes)); try (PDDocument document = parser.parse()) { PDFTextStripper stripper = new PDFTextStripper(); String extractText = stripper.getText(document); if (extractText.trim().isEmpty() || extractText.length() < 10) { - extractText = extractTextFromImage(document); + extractText = extractTextFromImage(pdfBytes); // PDF를 바이트 배열로 전달 } return extractText; - } catch (IOException e) { - e.printStackTrace(); - return "텍스트 추출 실패: " + e.getMessage(); } catch (TesseractException e) { e.printStackTrace(); throw new InterviewException("Tesseract가 연결되어 있지 않습니다.", HttpStatus.INTERNAL_SERVER_ERROR); @@ -44,31 +58,61 @@ public String extractTextFromPDF(InputStream inputStream) throws IOException { } - public String extractTextFromImage(PDDocument document) throws IOException, TesseractException { - PDFRenderer pdfRenderer = new PDFRenderer(document); - StringBuilder extractedText = new StringBuilder(); - Tesseract tesseract = new Tesseract(); - tesseract.setDatapath("/opt/homebrew/share/tessdata"); - tesseract.setLanguage("kor+eng"); // 한글 & 영어 인식 - tesseract.setPageSegMode(4); + public String extractTextFromImage(byte[] pdfBytes) throws IOException, TesseractException { + + List> tasks = new ArrayList<>(); + + List images = new ArrayList<>(); - for (int i = 0; i < document.getNumberOfPages(); i++) { - BufferedImage image = pdfRenderer.renderImageWithDPI(i, 100); // 100 DPI로 이미지 변환 - File tempImage = new File("temp.png"); - ImageIO.write(image, "png", tempImage); - - try { - // OCR 수행 - String ocrText = tesseract.doOCR(tempImage); - extractedText.append(ocrText).append("\n"); - } catch (UnsatisfiedLinkError e) { - throw new InterviewException("Tesseract 라이브러리를 로드할 수 없습니다. 설치 경로를 확인하세요.", - HttpStatus.INTERNAL_SERVER_ERROR); - } finally { - Files.delete(tempImage.toPath()); // 임시 이미지 삭제 + // 1. PDDocument를 한 번만 로드하고, 모든 페이지를 이미지로 변환 + try (PDDocument document = new PDFParser(new RandomAccessReadBuffer(pdfBytes)).parse()) { + PDFRenderer renderer = new PDFRenderer(document); + + for (int i = 0; i < document.getNumberOfPages(); i++) { + BufferedImage image = renderer.renderImageWithDPI(i, 300); // DPI 300으로 변환 + images.add(image); } } - return extractedText.toString(); + // 2. 변환된 이미지 리스트를 병렬로 OCR 수행 + for (BufferedImage image : images) { + tasks.add(() -> processImage(image)); // PDDocument 공유 없이 OCR 수행 + } + + try { + List> futures = executor.invokeAll(tasks); + StringBuilder extractedText = new StringBuilder(); + for (Future future : futures) { + extractedText.append(future.get()).append("\n"); + } + return extractedText.toString(); + } catch (InterruptedException | ExecutionException e) { + throw new InterviewException("OCR 처리 중 오류 발생", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private String processImage(BufferedImage image) throws TesseractException { + Tesseract tesseract = new Tesseract(); + tesseract.setDatapath(tessDataPath); + tesseract.setLanguage("kor+eng"); + tesseract.setPageSegMode(4); + tesseract.setOcrEngineMode(1); + tesseract.setVariable("user_defined_dpi", "300"); + + BufferedImage preprocessImage = preprocessImage(image); + + return tesseract.doOCR(preprocessImage); + } + + + // PDF에서 추출한 이미지에 대해 전처리 수행 + private BufferedImage preprocessImage(BufferedImage image) { + // Grayscale 변환 + BufferedImage grayscaleImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = grayscaleImage.createGraphics(); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return grayscaleImage; } } diff --git a/src/main/java/ce2team1/mentoview/service/PortonePaymentService.java b/src/main/java/ce2team1/mentoview/service/PortonePaymentService.java index 9368727..359e8dd 100644 --- a/src/main/java/ce2team1/mentoview/service/PortonePaymentService.java +++ b/src/main/java/ce2team1/mentoview/service/PortonePaymentService.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.math.BigDecimal; @@ -175,7 +176,6 @@ private void validateBillingKey(BillingKeyCheckDto billingKeyCheckDto) throws Js throw new RuntimeException("Invalid billingkey response"); } else { - System.out.println(billingKeyCheckDto.getStatus()); userService.setBillingKey(Long.valueOf(billingKeyCheckDto.getCustomer().getId()), billingKeyCheckDto.getBillingKey()); } } @@ -250,9 +250,12 @@ public void cancelScheduling(Long uId) throws JsonProcessingException { System.out.println(response); } catch (Exception e) { - throw new SubscriptionException(e.getMessage() + " 구독 취소 요청이 실패하였습니다. 잠시 후에 다시 시도해주세요.", HttpStatus.INTERNAL_SERVER_ERROR); + throw new SubscriptionException(e.getMessage() + "구독 취소 요청이 실패하였습니다. 잠시 후에 다시 시도해주세요.", HttpStatus.INTERNAL_SERVER_ERROR); } + deleteBillingKey(billingKey); + userService.setBillingKey(uId, null); + } private String createRequestBodyForCancelScheduling(String billingKey) throws JsonProcessingException { @@ -267,6 +270,28 @@ private String createRequestBodyForCancelScheduling(String billingKey) throws Js return response; } + public void deleteBillingKey(String billingKey) throws JsonProcessingException { + + String encodedBillingKey = URLEncoder.encode(billingKey, StandardCharsets.UTF_8); + + + try { + String response = webClient.method(HttpMethod.DELETE) + .uri(baseUrl + "/billing-keys/" + encodedBillingKey) + .headers(headers -> headers.set("Authorization", "PortOne " + portoneApiSecret)) + .retrieve() + .bodyToMono(String.class) + .block(); + + System.out.println(response); + + } catch (Exception e) { + throw new SubscriptionException(e.getMessage() + "빌링키 삭제 요청이 실패하였습니다. 잠시 후에 다시 시도해주세요.", HttpStatus.INTERNAL_SERVER_ERROR); + } + + } + + public void processChangingBillingKey(BillingKeyCheckDto billingKeyCheckDto) throws JsonProcessingException { // 구독 조회 @@ -318,4 +343,13 @@ public void processSubscriptionReactivation(Long uId) throws JsonProcessingExcep subscriptionService.modifySubscriptionStatusToActive(uId, SubscriptionStatus.CANCELED); } + + public void processFreeTierSubscription(Long uId) throws JsonProcessingException { + String billingKey = userService.getBillingKey(uId); + + subscriptionService.createFreeTierSubscription(uId); + + schedulePayment(uId, billingKey, null, LocalDate.now().plusDays(31).atStartOfDay(ZoneOffset.UTC).toString()); + + } } diff --git a/src/main/java/ce2team1/mentoview/service/ResumeService.java b/src/main/java/ce2team1/mentoview/service/ResumeService.java index f74b1c1..4fcda0a 100644 --- a/src/main/java/ce2team1/mentoview/service/ResumeService.java +++ b/src/main/java/ce2team1/mentoview/service/ResumeService.java @@ -11,12 +11,14 @@ import ce2team1.mentoview.service.dto.InterviewDto; import ce2team1.mentoview.service.dto.ResumeDto; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class ResumeService { @@ -107,4 +109,23 @@ public void deleteResume(Long resumeId, Long userId) { resume.softDelete(); resumeRepository.save(resume); } + + public void hardDeleteResume(Long userId) { + + userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + List resumes = resumeRepository.findAllByUserId(userId); + if (resumes.isEmpty()) { + log.info("[S3 삭제] 유저 ID {}: 삭제할 이력서가 없습니다.", userId); + return; + } + for (Resume resume : resumes) { + if (resume.getS3Key()!=null) { + log.info("[S3 삭제] Resume ID: {}, S3 Key: {}", resume.getResumeId(), resume.getS3Key()); + awsS3Service.deleteS3Object(resume.getS3Key()); + } + } + log.info("[S3 삭제 완료] 유저 ID {}: 모든 이력서 삭제 완료", userId); + } } \ No newline at end of file diff --git a/src/main/java/ce2team1/mentoview/service/SubscriptionService.java b/src/main/java/ce2team1/mentoview/service/SubscriptionService.java index f9fb008..27582e8 100644 --- a/src/main/java/ce2team1/mentoview/service/SubscriptionService.java +++ b/src/main/java/ce2team1/mentoview/service/SubscriptionService.java @@ -60,7 +60,6 @@ public void deleteSubscriptionByPaymentId(String paymentId) { } public SubscriptionDto createSubscription(PaymentCheckDto paymentCheckDto) { - // paidAt 포맷팅 String paidAtString = paymentCheckDto.getPaidAt(); ZonedDateTime zonedDateTime = ZonedDateTime.parse(paidAtString, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(ZoneId.of("Asia/Seoul"));; @@ -72,7 +71,7 @@ public SubscriptionDto createSubscription(PaymentCheckDto paymentCheckDto) { PaymentMethod paymentMethod = "KAKAOPAY".equals(paymentCheckDto.getMethod().getProvider())? PaymentMethod.KAKAO_PAY : PaymentMethod.CREDIT_CARD; return SubscriptionDto.toDto(subscriptionRepository.save(Subscription.of( SubscriptionStatus.ACTIVE, - SubscriptionPlan.PREMIUM, + SubscriptionPlan.BASIC, ld, ld.plusDays(30), ld.plusDays(31), @@ -83,10 +82,27 @@ public SubscriptionDto createSubscription(PaymentCheckDto paymentCheckDto) { )); } + public void createFreeTierSubscription(Long uId) { + + LocalDate ld = LocalDate.now(); + User user = userRepository.findById(uId).orElseThrow(); + + subscriptionRepository.save(Subscription.of( + SubscriptionStatus.ACTIVE, + SubscriptionPlan.FREE_TIER, + ld, + ld.plusDays(30), + ld.plusDays(31), + PaymentMethod.KAKAO_PAY, + null, + null, + user) + ); + } + public Long checkSubscription(Long uId) { Subscription subscription = subscriptionRepository.findByUser_UserIdAndStatus(uId, SubscriptionStatus.ACTIVE); - System.out.println(subscription); if (subscription != null) { return subscription.getSubId(); @@ -98,7 +114,7 @@ public Long checkSubscription(Long uId) { public SubscriptionDto modifyEndDateAndNextBillingDate(Long subId, String paidAt) { Subscription subscription = subscriptionRepository.findById(subId).orElseThrow(); - subscription.modifyEndDateAndNextBillingDate(paidAt); + subscription.modifyEndDateAndNextBillingDateAndPlan(paidAt); return SubscriptionDto.toDto(subscription); } diff --git a/src/main/java/ce2team1/mentoview/service/UserService.java b/src/main/java/ce2team1/mentoview/service/UserService.java index 7732f30..5fcc4a7 100644 --- a/src/main/java/ce2team1/mentoview/service/UserService.java +++ b/src/main/java/ce2team1/mentoview/service/UserService.java @@ -7,10 +7,10 @@ import ce2team1.mentoview.repository.SubscriptionRepository; import ce2team1.mentoview.repository.UserRepository; import ce2team1.mentoview.service.dto.UserDto; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -22,7 +22,7 @@ public class UserService { private final SubscriptionRepository subscriptionRepository; private final PasswordEncoder passwordEncoder; - + @Transactional(readOnly = false) public UserDto createUser(UserDto userDto) { Optional findByEmail = userRepository.findByEmail(userDto.getEmail()); @@ -47,7 +47,6 @@ public UserDto accessMyPage(Long userId, String password) { User findUser = userRepository.findById(userId).orElseThrow(() -> new ServiceException("User not found")); - if(findUser.getPassword() == null || findUser.getPassword().isEmpty()) { throw new ServiceException("Password is empty"); } @@ -59,39 +58,22 @@ public UserDto accessMyPage(Long userId, String password) { } - + @Transactional(readOnly = false) public void changePassword(Long userId, String beforePassword, String afterPassword) { User user = userRepository.findById(userId).orElseThrow(() -> new ServiceException("User not found")); - UserDto userDto = UserDto.toDto(user); + //UserDto userDto = UserDto.toDto(user); - if (!passwordEncoder.matches(beforePassword, userDto.getPassword())) { + if (!passwordEncoder.matches(beforePassword, user.getPassword())) { throw new ServiceException("Password does not match"); } + UserDto changed = UserDto.toDto(user).toBuilder() .password(passwordEncoder.encode(afterPassword)) .build(); userRepository.save(User.toEntity(changed)); - } -/* // OAuth - public User updateUser(User user, OAuth2ResponseSocial responseSocial) { - user.updateSocialInfo(responseSocial.getProviderId()); - return userRepository.save(user); - } - // OAuth - public User createUser(OAuth2ResponseSocial responseSocial) { - return User.builder() - .email(responseSocial.getEmail()) - .name(responseSocial.getName()) - .role(Role.USER) - .socialProvider(responseSocial.getProvider()) - .providerId(responseSocial.getProviderId()) - .status(UserStatus.ACTIVE) - .build(); - }*/ - @Transactional public void setBillingKey(Long uId, String billingKey) { User user = userRepository.findById(uId).orElseThrow(); @@ -104,10 +86,43 @@ public String getBillingKey(Long uId) { return user.getBillingKey(); } + @Transactional(readOnly = true) public UserDto findByEmail(String email) { User user = userRepository.findByEmail(email).orElseThrow( () -> new ServiceException("User not found")); + + System.out.println("유저 id - " + user.getUserId()); return UserDto.toDto(user); } -} \ No newline at end of file + + @Transactional(readOnly = false) + public UserDto createPassword(Long userId, String password) { + User user = userRepository.findById(userId).orElseThrow(() -> new ServiceException("User not found")); + //UserDto userDto = UserDto.toDto(user); + + UserDto newUserDto = UserDto.toDto(user).toBuilder() + .password(passwordEncoder.encode(password)) + .build(); + User saved = userRepository.save(User.toEntity(newUserDto)); + + return UserDto.toDto(saved); + + } + + public UserDto findByUserId(Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new ServiceException("User not found")); + return UserDto.toDto(user); + } + + @Transactional(readOnly = false) + public void softDelete(Long userId) { + UserDto userDto = UserDto.toDto(userRepository.findById(userId).orElseThrow(() -> new ServiceException("User not found"))); + UserDto updatedDto = userDto.toBuilder() + .billingKey(null) + .status(UserStatus.DELETED) + .build(); + + userRepository.save(User.toEntity(updatedDto)); + } +} diff --git a/src/main/java/ce2team1/mentoview/service/dto/UserDto.java b/src/main/java/ce2team1/mentoview/service/dto/UserDto.java index 22c1410..0f3df95 100644 --- a/src/main/java/ce2team1/mentoview/service/dto/UserDto.java +++ b/src/main/java/ce2team1/mentoview/service/dto/UserDto.java @@ -5,12 +5,12 @@ import ce2team1.mentoview.entity.atrribute.Role; import ce2team1.mentoview.entity.atrribute.SocialProvider; import ce2team1.mentoview.entity.atrribute.UserStatus; +import ce2team1.mentoview.security.dto.MvPrincipalDetails; import lombok.*; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.Map; -import java.util.Objects; /** * DTO for {@link User} @@ -19,6 +19,7 @@ @Builder(toBuilder = true) @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE) +@ToString public class UserDto { private Long userId; private String email; @@ -31,39 +32,26 @@ public class UserDto { private String billingKey; public static UserDto of(String email, String password, String name, Role role, SocialProvider socialProvider, String providerId, UserStatus status, String billingKey) { - return new UserDto(null, email, password, name, role, socialProvider, providerId, status, null); + return new UserDto(null, email, password, name, role, socialProvider, providerId, status != null ? status : UserStatus.ACTIVE, null); } public static UserDto of(Long userId,String email, String password, String name, Role role, SocialProvider socialProvider, String providerId, UserStatus status, String billingKey) { - return new UserDto(userId, email, password, name, role, socialProvider, providerId, status, null); + return new UserDto(userId, email, password, name, role, socialProvider, providerId, status != null ? status : UserStatus.ACTIVE, null); } - public static UserDto of(String email, Role role) { + public static UserDto of(String email, Role role, Long userId, UserStatus status) { return UserDto.builder() + .userId(userId) .email(email) - .password(null) - .role(role) - .build(); - } - public static UserDto ofForm(String email, String password, Role role) { - Objects.requireNonNull(password, "폼 로그인 시 비밀번호는 필수입니다."); - return UserDto.builder() - .email(email) - .password(password) + .password("") .role(role) + .status(status != null ? status : UserStatus.ACTIVE) .build(); } - public UserDto updatePassword(String newPassword) { - Objects.requireNonNull(newPassword, "새로운 비밀번호는 null이 될 수 없습니다."); - return this.toBuilder() - .password(newPassword) - .build(); - } - - public static UserDto toDto(User user) { return UserDto.builder() .userId(user.getUserId()) .email(user.getEmail()) + .password(user.getPassword() != null ? user.getPassword() : "") .name(user.getName()) .role(user.getRole()) .socialProvider(user.getSocialProvider()) @@ -75,7 +63,7 @@ public static UserDto toForm(User user) { return UserDto.builder() .userId(user.getUserId()) .email(user.getEmail()) - .password(user.getPassword()) + .password(user.getPassword() != null ? user.getPassword() : "") .name(user.getName()) .role(user.getRole()) .socialProvider(user.getSocialProvider()) @@ -83,11 +71,11 @@ public static UserDto toForm(User user) { .status(user.getStatus()) .build(); } + public static UserDto byOAuth2User(OAuth2User oAuth2User) { String email = null; String name = null; - if(oAuth2User instanceof OidcUser oidcUser) { Map claims = oidcUser.getClaims(); email = (String) claims.getOrDefault("email", null); @@ -112,8 +100,12 @@ public static UserDto byOAuth2User(OAuth2User oAuth2User) { .email(email) .name(name) .role(Role.USER) - .status(UserStatus.ACTIVE) + // .status(null) .build(); } -} \ No newline at end of file + public static UserDto byOAuth2User(MvPrincipalDetails principalDetails) { + return principalDetails.getUserDto(); + } + +} diff --git a/src/main/java/ce2team1/mentoview/utils/archive/JsonUtils.java b/src/main/java/ce2team1/mentoview/utils/archive/JsonUtils.java new file mode 100644 index 0000000..e7cfa8d --- /dev/null +++ b/src/main/java/ce2team1/mentoview/utils/archive/JsonUtils.java @@ -0,0 +1,41 @@ +package ce2team1.mentoview.utils.archive; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import java.util.HashMap; +import java.util.Map; + +public class JsonUtils { + + private static final ObjectMapper mapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + // 직렬화 : JSON으로 + public static String toJson(Object obj) { + try{ + return mapper.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException("직렬화 오류",e); + } + } + + // 역직렬화 : Java로 + public static T byJson(String json, Class clazz) { + try { + return mapper.readValue(json, clazz); + } catch (Exception e) { + throw new RuntimeException("역직렬화 오류", e); + } + } + + // 에러 직렬화 + public static String errorToJson(int status, String errorMassage) { + Map errorResponse = new HashMap<>(); + errorResponse.put("status", status); + errorResponse.put("error", errorMassage); + + return toJson(errorResponse); + } +} diff --git a/src/test/java/ce2team1/mentoview/archive/ArchiveServiceTest.java b/src/test/java/ce2team1/mentoview/archive/ArchiveServiceTest.java new file mode 100644 index 0000000..6c2ba66 --- /dev/null +++ b/src/test/java/ce2team1/mentoview/archive/ArchiveServiceTest.java @@ -0,0 +1,80 @@ +package ce2team1.mentoview.archive; + +import ce2team1.mentoview.archive.dto.InterviewData; +import ce2team1.mentoview.archive.dto.InterviewEntry; +import ce2team1.mentoview.archive.entity.*; +import ce2team1.mentoview.archive.repository.InterviewArchiveRepository; +import ce2team1.mentoview.archive.repository.PaymentArchiveRepository; +import ce2team1.mentoview.archive.repository.UserArchiveRepository; +import ce2team1.mentoview.archive.service.ArchiveService; +import ce2team1.mentoview.entity.*; +import ce2team1.mentoview.repository.*; +import ce2team1.mentoview.service.AwsS3Service; +import ce2team1.mentoview.utils.archive.JsonUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@SpringBootTest +@Transactional +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class ArchiveServiceTest { + @Autowired + private InterviewRepository interviewRepository; + @Autowired + private ResumeRepository resumeRepository; + @Autowired + private PaymentArchiveRepository paymentArchiveRepository; + @Autowired + private PaymentRepository paymentRepository; + @Autowired + private SubscriptionRepository subscriptionRepository; + @Autowired + private AwsS3Service awsS3Service; + @Autowired + private UserRepository userRepository; + @Autowired + private UserArchiveRepository userArchiveRepository ; + @Autowired + private InterviewArchiveRepository archiveRepository; + @Autowired + private ArchiveService archiveService; + + + + + + @Test + public void archive() { + Long id = 13L; + + InterviewArchive interviewArchive = archiveService.archiveUserInterviews(id); + System.out.println("interviewArchive = " + interviewArchive); + + + } + + @Test + public void archive22() { + Long userId = 13L; + List paymentArchives = archiveService.archiveUserPayments(userId); + System.out.println("paymentArchives = " + paymentArchives.toString()); + + + } + + @Test + public void archive32() { + Long userId = 13L; + + archiveService.archiveUser(userId); + + } + + +} \ No newline at end of file diff --git a/src/test/java/ce2team1/mentoview/archive/service/UserDeletionListenerTest.java b/src/test/java/ce2team1/mentoview/archive/service/UserDeletionListenerTest.java new file mode 100644 index 0000000..f46320a --- /dev/null +++ b/src/test/java/ce2team1/mentoview/archive/service/UserDeletionListenerTest.java @@ -0,0 +1,123 @@ +package ce2team1.mentoview.archive.service; + +import ce2team1.mentoview.archive.entity.InterviewArchive; +import ce2team1.mentoview.archive.repository.InterviewArchiveRepository; +import ce2team1.mentoview.archive.repository.PaymentArchiveRepository; +import ce2team1.mentoview.archive.repository.UserArchiveRepository; +import ce2team1.mentoview.entity.Interview; +import ce2team1.mentoview.entity.Payment; +import ce2team1.mentoview.entity.Resume; +import ce2team1.mentoview.entity.Subscription; +import ce2team1.mentoview.repository.*; +import ce2team1.mentoview.service.AwsS3Service; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +@Transactional +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class UserDeletionListenerTest { + + @Autowired + private InterviewRepository interviewRepository; + @Autowired + private ResumeRepository resumeRepository; + @Autowired + private PaymentArchiveRepository paymentArchiveRepository; + @Autowired + private PaymentRepository paymentRepository; + @Autowired + private SubscriptionRepository subscriptionRepository; + @Autowired + private AwsS3Service awsS3Service; + @Autowired + private UserRepository userRepository; + @Autowired + private UserArchiveRepository userArchiveRepository ; + @Autowired + private InterviewArchiveRepository archiveRepository; + @Autowired + private ArchiveService archiveService; + @Autowired + private UserDeletionListener userDeletionListener; + + @Autowired + private ApplicationEventPublisher eventPublisher; // 이벤트를 트리거하는 역할 + + + + + + + @Test + public void archive() { + Long userId = 13L; + // 이벤트 객체 + UserDeletedEvent event = new UserDeletedEvent(userId); + + // 이벤트 발행 + eventPublisher.publishEvent(event); + + //Listener 호출 + userDeletionListener.deleteUserAfterArchiving(event); + + } + + @Test + @DisplayName("🔍 UserDeletionListener 동작 테스트 - 유저 삭제") + public void testDeleteUserAfterArchiving() { + // 유저 ID + Long userId = 29L; + + // 삭제 전 + long resumeCountBefore = resumeRepository.count(); + long interviewCountBefore = interviewRepository.count(); + long paymentCountBefore = paymentRepository.count(); + long subscriptionCountBefore = subscriptionRepository.count(); + long userCountBefore = userRepository.count(); + + System.out.println(" 삭제 전 데이터 개수"); + System.out.println("Resumes: " + resumeCountBefore); + System.out.println("Interviews: " + interviewCountBefore); + System.out.println("Payments: " + paymentCountBefore); + System.out.println("Subscriptions: " + subscriptionCountBefore); + System.out.println("Users: " + userCountBefore); + + // 삭제 이벤트 발행 + eventPublisher.publishEvent(new UserDeletedEvent(userId)); + + // (이벤트 리스너 호출 + userDeletionListener.deleteUserAfterArchiving(new UserDeletedEvent(userId)); + + // 삭제 후 개수 + long resumeCountAfter = resumeRepository.count(); + long interviewCountAfter = interviewRepository.count(); + long paymentCountAfter = paymentRepository.count(); + long subscriptionCountAfter = subscriptionRepository.count(); + long userCountAfter = userRepository.count(); + + System.out.println(" 삭제 후 데이터 개수"); + System.out.println("Resumes: " + resumeCountAfter); + System.out.println("Interviews: " + interviewCountAfter); + System.out.println("Payments: " + paymentCountAfter); + System.out.println("Subscriptions: " + subscriptionCountAfter); + System.out.println("Users: " + userCountAfter); + + //검증 + Assertions.assertEquals(resumeCountBefore - 1, resumeCountAfter, "Resume 삭제 실패"); + Assertions.assertEquals(interviewCountBefore - 1, interviewCountAfter, "Interview 삭제 실패"); + Assertions.assertEquals(paymentCountBefore - 1, paymentCountAfter, "Payment 삭제 실패"); + Assertions.assertEquals(subscriptionCountBefore - 1, subscriptionCountAfter, "Subscription 삭제 실패"); + Assertions.assertEquals(userCountBefore - 1, userCountAfter, "User 삭제 실패"); + } + +} \ No newline at end of file