diff --git a/src/main/java/cloudcomputinginha/demo/DemoApplication.java b/src/main/java/cloudcomputinginha/demo/DemoApplication.java index bea335e..62a7926 100644 --- a/src/main/java/cloudcomputinginha/demo/DemoApplication.java +++ b/src/main/java/cloudcomputinginha/demo/DemoApplication.java @@ -4,9 +4,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJpaAuditing +@EnableScheduling @EnableAsync public class DemoApplication { diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/GuestAccountCleaner.java b/src/main/java/cloudcomputinginha/demo/config/auth/GuestAccountCleaner.java new file mode 100644 index 0000000..afc964f --- /dev/null +++ b/src/main/java/cloudcomputinginha/demo/config/auth/GuestAccountCleaner.java @@ -0,0 +1,21 @@ +package cloudcomputinginha.demo.config.auth; + +import cloudcomputinginha.demo.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class GuestAccountCleaner { + private final MemberRepository memberRepository; + + @Scheduled(fixedDelay = 3600000) + @Transactional + public void deleteOldGuestAccounts() { + LocalDateTime threshold = LocalDateTime.now().minusHours(1); + memberRepository.deleteByIsGuestTrueAndCreatedAtBefore(threshold); + } +} diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/JwtProvider.java b/src/main/java/cloudcomputinginha/demo/config/auth/JwtProvider.java index f41b556..98434d0 100644 --- a/src/main/java/cloudcomputinginha/demo/config/auth/JwtProvider.java +++ b/src/main/java/cloudcomputinginha/demo/config/auth/JwtProvider.java @@ -20,12 +20,13 @@ public class JwtProvider { private final long ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 60; // 1시간 private final long REFRESH_TOKEN_EXPIRATION = 1000L * 60 * 60 * 24 * 14; // 2주 - public String generateAccessToken(Long memberId) { + public String generateAccessToken(Long memberId, boolean isGuest) { Date now = new Date(); SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes()); return Jwts.builder() .setSubject(String.valueOf(memberId)) + .claim("isGuest", isGuest) .setIssuedAt(now) .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION)) .signWith(key, SignatureAlgorithm.HS256) @@ -67,6 +68,9 @@ public Long getMemberIdFromToken(String token) { } public Map reissueTokens(String oldRefreshToken, Member member) { + if (member.isGuest()) { + throw new JwtException("게스트는 토큰 재발급이 불가능합니다."); + } if (!validateToken(oldRefreshToken)) { throw new JwtException("유효하지 않은 refresh token 입니다."); } @@ -74,7 +78,7 @@ public Map reissueTokens(String oldRefreshToken, Member member) throw new JwtException("저장된 refresh token과 일치하지 않습니다."); } - String newAccessToken = generateAccessToken(member.getId()); + String newAccessToken = generateAccessToken(member.getId(), false); String newRefreshToken = generateRefreshToken(member.getId()); member.setRefreshToken(newRefreshToken); diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/SecurityConfig.java b/src/main/java/cloudcomputinginha/demo/config/auth/SecurityConfig.java index 57d2a1f..5f44264 100644 --- a/src/main/java/cloudcomputinginha/demo/config/auth/SecurityConfig.java +++ b/src/main/java/cloudcomputinginha/demo/config/auth/SecurityConfig.java @@ -71,7 +71,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { //경로별 인가 작업 .authorizeHttpRequests((auth) -> auth - .requestMatchers("/auth/GOOGLE", "/auth/google/callback").permitAll() + .requestMatchers("/auth/GOOGLE", "/auth/google/callback", "/auth/guest").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/controller/OauthController.java b/src/main/java/cloudcomputinginha/demo/config/auth/controller/OauthController.java index 5db68b8..79bcd06 100644 --- a/src/main/java/cloudcomputinginha/demo/config/auth/controller/OauthController.java +++ b/src/main/java/cloudcomputinginha/demo/config/auth/controller/OauthController.java @@ -2,6 +2,7 @@ import cloudcomputinginha.demo.apiPayload.ApiResponse; import cloudcomputinginha.demo.config.auth.JwtProvider; +import cloudcomputinginha.demo.config.auth.dto.GuestLoginResponse; import cloudcomputinginha.demo.config.auth.dto.TokenReissueRequestDto; import cloudcomputinginha.demo.config.auth.dto.TokenReissueResponseDto; import cloudcomputinginha.demo.config.auth.service.OauthService; @@ -80,4 +81,10 @@ public ApiResponse reissueToken(@RequestBody TokenReiss return ApiResponse.onSuccess(responseDto); } + + @PostMapping("/guest") + @Operation(summary = "게스트 로그인 API", description = "게스트 사용자 전용 로그인 기능입니다.") + public ApiResponse guestLogin() { + return ApiResponse.onSuccess(oauthService.loginAsGuest()); + } } diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/dto/GuestLoginResponse.java b/src/main/java/cloudcomputinginha/demo/config/auth/dto/GuestLoginResponse.java new file mode 100644 index 0000000..b794bcd --- /dev/null +++ b/src/main/java/cloudcomputinginha/demo/config/auth/dto/GuestLoginResponse.java @@ -0,0 +1,16 @@ +package cloudcomputinginha.demo.config.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GuestLoginResponse { + private String accessToken; + private Long memberId; + private String randomName; +} diff --git a/src/main/java/cloudcomputinginha/demo/config/auth/service/OauthService.java b/src/main/java/cloudcomputinginha/demo/config/auth/service/OauthService.java index 7c42f47..e0251e3 100644 --- a/src/main/java/cloudcomputinginha/demo/config/auth/service/OauthService.java +++ b/src/main/java/cloudcomputinginha/demo/config/auth/service/OauthService.java @@ -2,6 +2,7 @@ import cloudcomputinginha.demo.config.auth.JwtProvider; import cloudcomputinginha.demo.config.auth.domain.GoogleUser; +import cloudcomputinginha.demo.config.auth.dto.GuestLoginResponse; import cloudcomputinginha.demo.domain.Member; import cloudcomputinginha.demo.domain.enums.SocialProvider; import cloudcomputinginha.demo.repository.MemberRepository; @@ -11,6 +12,7 @@ import org.springframework.stereotype.Service; import java.io.IOException; import java.util.List; +import java.util.Random; @Service @RequiredArgsConstructor @@ -51,7 +53,7 @@ public void oauthLoginCallback(SocialProvider socialProvider, String code) { return memberRepository.save(newMember); }); - String accessToken = jwtProvider.generateAccessToken(member.getId()); + String accessToken = jwtProvider.generateAccessToken(member.getId(), false); String refreshToken = jwtProvider.generateRefreshToken(member.getId()); member.setRefreshToken(refreshToken); @@ -72,4 +74,15 @@ private SocialOauth findSocialOauthByType(SocialProvider socialProvider) { .findFirst() .orElseThrow(() -> new IllegalArgumentException("알 수 없는 SocialLoginType 입니다.")); } + + // 게스트 사용자 + public GuestLoginResponse loginAsGuest() { + String name = "게스트" + new Random().nextInt(10000); + Member guest = Member.createGuest(name); + memberRepository.save(guest); + + String accessToken = jwtProvider.generateAccessToken(guest.getId(), true); + + return new GuestLoginResponse(accessToken, guest.getId(), guest.getName()); + } } diff --git a/src/main/java/cloudcomputinginha/demo/converter/InterviewConverter.java b/src/main/java/cloudcomputinginha/demo/converter/InterviewConverter.java index 28b4695..d444ef7 100644 --- a/src/main/java/cloudcomputinginha/demo/converter/InterviewConverter.java +++ b/src/main/java/cloudcomputinginha/demo/converter/InterviewConverter.java @@ -47,7 +47,7 @@ public static Interview toInterview(InterviewRequestDTO.InterviewCreateDTO reque .startedAt(combineStartAt(request)) .isOpen(request.getIsOpen() != null ? request.getIsOpen() : false) .maxParticipants(request.getMaxParticipants()) - .hostId(member.getId()) + .host(member) .build(); } @@ -130,7 +130,7 @@ public static InterviewResponseDTO.GroupInterviewDetailDTO toInterviewGroupDetai .startedAt(interview.getStartedAt()) .hostName( memberInterviewList.stream() - .filter(mi -> mi.getMember().getId().equals(interview.getHostId())) + .filter(mi -> mi.getMember().getId().equals(interview.getHost().getId())) .findFirst() .map(mi -> mi.getMember().getName()) .orElse("호스트의 이름을 알 수 없습니다.") @@ -140,7 +140,7 @@ public static InterviewResponseDTO.GroupInterviewDetailDTO toInterviewGroupDetai .map(mi -> InterviewResponseDTO.GroupInterviewParticipantDTO.builder() .memberId(mi.getMember().getId()) .name(mi.getMember().getName()) - .isHost(mi.getMember().getId().equals(interview.getHostId())) + .isHost(mi.getMember().getId().equals(interview.getHost().getId())) .isSubmitted(mi.getResume() != null && mi.getCoverletter() != null) .build() ).toList() diff --git a/src/main/java/cloudcomputinginha/demo/domain/Coverletter.java b/src/main/java/cloudcomputinginha/demo/domain/Coverletter.java index 64c7a7f..274e83d 100644 --- a/src/main/java/cloudcomputinginha/demo/domain/Coverletter.java +++ b/src/main/java/cloudcomputinginha/demo/domain/Coverletter.java @@ -5,6 +5,8 @@ import cloudcomputinginha.demo.domain.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -21,6 +23,9 @@ public class Coverletter extends BaseEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; + @OneToMany(mappedBy = "coverletter", cascade = CascadeType.ALL, orphanRemoval = true) + private List qnas = new ArrayList<>(); + @Column(length = 100) private String corporateName; diff --git a/src/main/java/cloudcomputinginha/demo/domain/Interview.java b/src/main/java/cloudcomputinginha/demo/domain/Interview.java index b7279dc..83502e8 100644 --- a/src/main/java/cloudcomputinginha/demo/domain/Interview.java +++ b/src/main/java/cloudcomputinginha/demo/domain/Interview.java @@ -10,6 +10,8 @@ import org.hibernate.annotations.DynamicUpdate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -42,12 +44,17 @@ public class Interview extends BaseEntity { @Column(columnDefinition = "VARCHAR(20)") private StartType startType; - private Long hostId; +// private Long hostId; @Builder.Default @Column(nullable = false) private Integer currentParticipants = 1; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "host_id", nullable = false) + private Member host; + + @Builder.Default private Integer maxParticipants = 1; //일대일 면접을 기준으로 초기화 @@ -58,10 +65,14 @@ public class Interview extends BaseEntity { private LocalDateTime endedAt; - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) @JoinColumn(name = "interview_option_id", unique = true, nullable = false) private InterviewOption interviewOption; + @OneToMany(mappedBy = "interview", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List memberInterviews = new ArrayList<>(); + + public void updateStartedAt(LocalDateTime startedAt) { this.startedAt = startedAt; } diff --git a/src/main/java/cloudcomputinginha/demo/domain/InterviewOption.java b/src/main/java/cloudcomputinginha/demo/domain/InterviewOption.java index ca909d0..b4d4b28 100644 --- a/src/main/java/cloudcomputinginha/demo/domain/InterviewOption.java +++ b/src/main/java/cloudcomputinginha/demo/domain/InterviewOption.java @@ -40,7 +40,7 @@ public class InterviewOption extends BaseEntity { private Integer answerTime; - @OneToOne(mappedBy = "interviewOption", cascade = CascadeType.ALL) + @OneToOne(mappedBy = "interviewOption") private Interview interview; public void updateVoiceType(VoiceType voiceType) { diff --git a/src/main/java/cloudcomputinginha/demo/domain/Member.java b/src/main/java/cloudcomputinginha/demo/domain/Member.java index 0f6588c..dcdc215 100644 --- a/src/main/java/cloudcomputinginha/demo/domain/Member.java +++ b/src/main/java/cloudcomputinginha/demo/domain/Member.java @@ -3,8 +3,10 @@ import cloudcomputinginha.demo.domain.common.BaseEntity; import cloudcomputinginha.demo.domain.enums.SocialProvider; import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; import lombok.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; @Entity @Getter @@ -33,15 +35,28 @@ public class Member extends BaseEntity { private String introduction; @Enumerated(EnumType.STRING) - @Column(length = 10, nullable = false) + @Column(length = 10) private SocialProvider socialProvider; - @Column(length = 100, nullable = false) + @Column(length = 100) private String providerId; @Column(columnDefinition = "TEXT") private String refreshToken; + @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List coverLetters = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List resumes = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List memberInterviews = new ArrayList<>(); + + @OneToMany(mappedBy = "host", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List hostedInterviews = new ArrayList<>(); + + public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } @@ -58,4 +73,15 @@ public void updateInfo(String name, String phone, String jobType, String introdu this.jobType = jobType; this.introduction = introduction; } + + // 게스트 로그인 + private boolean isGuest; + + public static Member createGuest(String randomName) { + Member member = new Member(); + member.email = "guest_" + UUID.randomUUID().toString().substring(0, 5)+ "@guest.com"; + member.name = randomName; + member.isGuest = true; + return member; + } } diff --git a/src/main/java/cloudcomputinginha/demo/repository/MemberRepository.java b/src/main/java/cloudcomputinginha/demo/repository/MemberRepository.java index 5b28391..2ce53be 100644 --- a/src/main/java/cloudcomputinginha/demo/repository/MemberRepository.java +++ b/src/main/java/cloudcomputinginha/demo/repository/MemberRepository.java @@ -2,9 +2,10 @@ import cloudcomputinginha.demo.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; - +import java.time.LocalDateTime; import java.util.Optional; public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + void deleteByIsGuestTrueAndCreatedAtBefore(LocalDateTime threshold); } diff --git a/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandService.java b/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandService.java index 8972df0..58056d7 100644 --- a/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandService.java +++ b/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandService.java @@ -9,7 +9,7 @@ public interface InterviewCommandService { InterviewResponseDTO.InterviewCreateResultDTO createInterview(InterviewRequestDTO.InterviewCreateDTO request, Long memberId); - public InterviewResponseDTO.InterviewStartResponseDTO startInterview(Long memberId, Long interviewId, Boolean isAutoMaticStart); + InterviewResponseDTO.InterviewStartResponseDTO startInterview(Long memberId, Long interviewId, Boolean isAutoMaticStart); InterviewResponseDTO.InterviewUpdateResponseDTO updateInterview(Long memberId, Long interviewId, InterviewRequestDTO.InterviewUpdateDTO request); } diff --git a/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandServiceImpl.java b/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandServiceImpl.java index a643459..449f614 100644 --- a/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandServiceImpl.java +++ b/src/main/java/cloudcomputinginha/demo/service/interview/InterviewCommandServiceImpl.java @@ -136,7 +136,7 @@ public InterviewResponseDTO.InterviewUpdateResponseDTO updateInterview(Long memb Interview interview = interviewRepository.findById(interviewId) .orElseThrow(() -> new InterviewHandler(ErrorStatus.INTERVIEW_NOT_FOUND)); - if (!interview.getHostId().equals(memberId)) { + if (!interview.getHost().getId().equals(memberId)) { throw new InterviewHandler(ErrorStatus.INTERVIEW_NO_PERMISSION); } diff --git a/src/main/java/cloudcomputinginha/demo/service/interviewOption/InterviewOptionCommandServiceImpl.java b/src/main/java/cloudcomputinginha/demo/service/interviewOption/InterviewOptionCommandServiceImpl.java index 62d717f..c3cc2bb 100644 --- a/src/main/java/cloudcomputinginha/demo/service/interviewOption/InterviewOptionCommandServiceImpl.java +++ b/src/main/java/cloudcomputinginha/demo/service/interviewOption/InterviewOptionCommandServiceImpl.java @@ -28,7 +28,7 @@ public InterviewOptionResponseDTO.InterviewOptionUpdateResponseDTO updateIntervi Interview interview = interviewRepository.findById(interviewId) .orElseThrow(() -> new InterviewHandler(ErrorStatus.INTERVIEW_NOT_FOUND)); - if (!interview.getHostId().equals(memberId)) { + if (!interview.getHost().getId().equals(memberId)) { throw new InterviewHandler(ErrorStatus.INTERVIEW_NO_PERMISSION); }