diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7be8af6..1e36f40 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -27,6 +27,9 @@ services: - GOOGLE_LOGIN_CLIENT_ID=${GOOGLE_LOGIN_CLIENT_ID} - GOOGLE_LOGIN_CLIENT_SECRET=${GOOGLE_LOGIN_CLIENT_SECRET} - GOOGLE_LOGIN_REDIRECT_URI=${GOOGLE_LOGIN_REDIRECT_URI} + - KAKAO_LOGIN_CLIENT_ID=${KAKAO_LOGIN_CLIENT_ID} + - KAKAO_LOGIN_CLIENT_SECRET=${KAKAO_LOGIN_CLIENT_SECRET} + - KAKAO_LOGIN_REDIRECT_URI=${KAKAO_LOGIN_REDIRECT_URI} - SENTRY_DSN=${SENTRY_DSN} depends_on: - mysql diff --git a/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java b/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java index 6b96021..ef4e6d0 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java @@ -1,10 +1,11 @@ package com.server.bbo_gak.domain.auth.controller; import com.server.bbo_gak.domain.auth.dto.request.LoginRequest; -import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; +import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.service.AuthService; import com.server.bbo_gak.domain.auth.service.oauth.GoogleService; +import com.server.bbo_gak.domain.auth.service.oauth.KakaoService; import com.server.bbo_gak.domain.user.entity.User; import com.server.bbo_gak.global.annotation.AuthUser; import com.server.bbo_gak.global.security.jwt.dto.TokenDto; @@ -23,10 +24,10 @@ @RequiredArgsConstructor public class AuthController { + private static final String SOCIAL_TOKEN_NAME = "SOCIAL-AUTH-TOKEN"; private final AuthService authService; private final GoogleService googleService; - - private static final String SOCIAL_TOKEN_NAME = "SOCIAL-AUTH-TOKEN"; + private final KakaoService kakaoService; @PostMapping("/social-login") public ResponseEntity socialLogin( @@ -41,9 +42,14 @@ public ResponseEntity socialLogin( /** * 프론트 연결 끝나면 지우기 */ - @PostMapping("/test/access-token") + @PostMapping("/test/google/access-token") public String googleTest(@RequestParam("code") String code) { - return googleService.getToken(code); + return googleService.getGoogleToken(code); + } + + @PostMapping("/test/kakao/access-token") + public String kakaoTest(@RequestParam("code") String code) { + return kakaoService.getKakaoToken(code); } @PostMapping("/test/login") diff --git a/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java b/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java new file mode 100644 index 0000000..73d0307 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java @@ -0,0 +1,68 @@ +package com.server.bbo_gak.domain.auth.dto.response.oauth; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +public record KakaoOAuthUserInfoResponse( + Long id, + LocalDateTime connected_at, + Map properties, + KakaoAccount kakao_account +) { + + public static KakaoOAuthUserInfoResponse from(Map attributes) { + return new KakaoOAuthUserInfoResponse( + Long.valueOf(String.valueOf(attributes.get("id"))), + LocalDateTime.parse( + String.valueOf(attributes.get("connected_at")), + DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault()) + ), + (Map) attributes.get("properties"), + KakaoAccount.from((Map) attributes.get("kakao_account")) + ); + } + + public String email() { + return this.kakao_account().email(); + } + + public String nickname() { + return this.kakao_account().nickname(); + } + + public record KakaoAccount( + Boolean profileNicknameNeedsAgreement, + Profile profile, + Boolean hasEmail, + Boolean emailNeedsAgreement, + Boolean isEmailValid, + Boolean isEmailVerified, + String email + ) { + + public static KakaoAccount from(Map attributes) { + return new KakaoAccount( + Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))), + Profile.from((Map) attributes.get("profile")), + Boolean.valueOf(String.valueOf(attributes.get("has_email"))), + Boolean.valueOf(String.valueOf(attributes.get("email_needs_agreement"))), + Boolean.valueOf(String.valueOf(attributes.get("is_email_valid"))), + Boolean.valueOf(String.valueOf(attributes.get("is_email_verified"))), + String.valueOf(attributes.get("email")) + ); + } + + public String nickname() { + return this.profile().nickname(); + } + + public record Profile(String nickname) { + + public static Profile from(Map attributes) { + return new Profile(String.valueOf(attributes.get("nickname"))); + } + } + } +} diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java index 6c0823d..ecd210e 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java @@ -1,12 +1,13 @@ package com.server.bbo_gak.domain.auth.service; import com.server.bbo_gak.domain.auth.dto.request.LoginRequest; +import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; -import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; import com.server.bbo_gak.domain.auth.entity.AuthTestUser; import com.server.bbo_gak.domain.auth.entity.AuthTestUserRepository; import com.server.bbo_gak.domain.auth.service.oauth.GoogleService; +import com.server.bbo_gak.domain.auth.service.oauth.KakaoService; import com.server.bbo_gak.domain.user.entity.Job; import com.server.bbo_gak.domain.user.entity.OauthProvider; import com.server.bbo_gak.domain.user.entity.User; @@ -32,6 +33,7 @@ public class AuthServiceImpl implements AuthService { private final RefreshTokenRepository refreshTokenRepository; private final JwtTokenService jwtTokenService; private final GoogleService googleService; + private final KakaoService kakaoService; private final UserService userService; private final UserRepository userRepository; @@ -44,8 +46,7 @@ public LoginResponse socialLogin(String socialAccessToken, String provider) { // DB에서 회원 찾기 User user = userRepository.findUserByOauthInfo(oauthUserInfo.toEntity()) - .orElseGet(() -> userService.createUser(oauthUserInfo)); //DB에 회원이 없으면 회원가입 - + .orElseGet(() -> userService.createUser(oauthUserInfo)); //DB에 회원이 없으면 회원가입 if (refreshTokenRepository.existsRefreshTokenByMemberId(user.getId())) { refreshTokenRepository.deleteById(user.getId()); //기존 토큰 삭제 @@ -104,6 +105,7 @@ public void logout(User user) { private OauthUserInfoResponse getMemberInfo(String socialAccessToken, OauthProvider provider) { return switch (provider) { case GOOGLE -> googleService.getOauthUserInfo(socialAccessToken); + case KAKAO -> kakaoService.getOauthUserInfo(socialAccessToken); }; } } diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java index e5841f2..e43db74 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java @@ -36,23 +36,25 @@ public OauthUserInfoResponse getOauthUserInfo(String accessToken) { return new OauthUserInfoResponse(response.id(), response.email(), response.name(), GOOGLE); } - private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken){ + private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken) { try { RestClient restClient = RestClient.create(); return restClient.get() - .uri(GOOGLE_USER_INFO_URI) - .header(AUTHORIZATION, TOKEN_PREFIX + accessToken) - .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, - (googleRequest, googleResponse) -> { - throw new BusinessException("Client error: " + googleResponse.getStatusCode(), AUTH_GET_USER_INFO_FAILED); - }) - .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { - throw new BusinessException("Server error: " + googleResponse.getStatusCode(), AUTH_GET_USER_INFO_FAILED); + .uri(GOOGLE_USER_INFO_URI) + .header(AUTHORIZATION, TOKEN_PREFIX + accessToken) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + (googleRequest, googleResponse) -> { + throw new BusinessException("Client error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); }) - .body(GoogleOauthUserInfoResponse.class); - } catch (RestClientException e) { // RestClient 관련 에러 + .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { + throw new BusinessException("Server error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .body(GoogleOauthUserInfoResponse.class); + } catch (RestClientException e) { // RestClient 관련 에러 throw new BusinessException("RestClientException: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); } catch (Exception e) { // 그 외 일반적인 예외 throw new BusinessException("Unexpected error: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); @@ -60,7 +62,7 @@ private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken){ } // 프론트와 연결끝나면 지워도 됨. - public String getToken(String code) { + public String getGoogleToken(String code) { Map params = new LinkedHashMap<>(); params.put("client_id", googleOAuthConfig.getGoogleClientId()); @@ -79,4 +81,6 @@ public String getToken(String code) { assert response != null; return response.accessToken(); } + + } diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java new file mode 100644 index 0000000..7c77522 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java @@ -0,0 +1,99 @@ +package com.server.bbo_gak.domain.auth.service.oauth; + +import static com.server.bbo_gak.domain.user.entity.OauthProvider.KAKAO; +import static com.server.bbo_gak.global.config.oauth.GoogleOAuthConfig.AUTHORIZATION; +import static com.server.bbo_gak.global.error.exception.ErrorCode.AUTH_GET_USER_INFO_FAILED; + +import com.server.bbo_gak.domain.auth.dto.response.oauth.GoogleTokenServiceResponse; +import com.server.bbo_gak.domain.auth.dto.response.oauth.KakaoOAuthUserInfoResponse; +import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.server.bbo_gak.global.config.oauth.KakaoOAuthConfig; +import com.server.bbo_gak.global.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@RequiredArgsConstructor +@Service +public class KakaoService implements OauthService { + + private final KakaoOAuthConfig kakaoOAuthConfig; + + @Override + public OauthUserInfoResponse getOauthUserInfo(String accessToken) { + KakaoOAuthUserInfoResponse response = getKakaoOauthUserInfo(accessToken); + return new OauthUserInfoResponse(response.id().toString(), response.email(), response.nickname(), KAKAO); + } + + private KakaoOAuthUserInfoResponse getKakaoOauthUserInfo(String accessToken) { + try { + RestClient restClient = RestClient.create(); + return restClient.get() + .uri(KakaoOAuthConfig.KAKAO_USER_INFO_URI) + .header(AUTHORIZATION, KakaoOAuthConfig.TOKEN_PREFIX + accessToken) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + (googleRequest, googleResponse) -> { + throw new BusinessException("Client error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { + throw new BusinessException("Server error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .body(KakaoOAuthUserInfoResponse.class); + } catch (RestClientException e) { // RestClient 관련 에러 + throw new BusinessException("RestClientException: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); + } catch (Exception e) { // 그 외 일반적인 예외 + throw new BusinessException("Unexpected error: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); + } + + + } + + // 프론트와 연결끝나면 지워도 됨. + public String getKakaoToken(String code) { + RestTemplate restTemplate = new RestTemplate(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("client_id", kakaoOAuthConfig.getKakaoClientId()); + params.add("client_secret", kakaoOAuthConfig.getKakaoClientSecret()); + params.add("code", code); + params.add("grant_type", "authorization_code"); + params.add("redirect_uri", kakaoOAuthConfig.getKakaoRedirectUri()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // HttpEntity 생성 + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + // 요청 URL 설정 + String url = KakaoOAuthConfig.KAKAO_TOKEN_URI; + + // POST 요청 전송 및 응답 수신 + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestEntity, + GoogleTokenServiceResponse.class); + + // 응답 검증 + if (responseEntity.getStatusCode().is2xxSuccessful()) { + GoogleTokenServiceResponse response = responseEntity.getBody(); + assert response != null; + return response.accessToken(); + } else { + // 오류 처리 로직 추가 + throw new RuntimeException("Failed to retrieve token: " + responseEntity.getStatusCode()); + } + } +} diff --git a/src/main/java/com/server/bbo_gak/domain/card/controller/TagController.java b/src/main/java/com/server/bbo_gak/domain/card/controller/TagController.java index b6a3b32..d0c08c1 100644 --- a/src/main/java/com/server/bbo_gak/domain/card/controller/TagController.java +++ b/src/main/java/com/server/bbo_gak/domain/card/controller/TagController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -28,6 +29,15 @@ public ResponseEntity> getAllTagList( return ResponseEntity.ok(tagService.getAllTagList(user)); } + @GetMapping("/recruits/{recruit-id}/tags") + public ResponseEntity> getAllRecruitsTagList( + @AuthUser User user, + @PathVariable("recruit-id") Long recruitId, + @RequestParam("type") String type + ) { + return ResponseEntity.ok(tagService.getAllRecruitsTagList(user, recruitId, type)); + } + @GetMapping("/cards/{card-id}/tags") public ResponseEntity> getcCardTagList( @AuthUser User user, diff --git a/src/main/java/com/server/bbo_gak/domain/card/dao/TagRepository.java b/src/main/java/com/server/bbo_gak/domain/card/dao/TagRepository.java index 223bf3e..a7a8794 100644 --- a/src/main/java/com/server/bbo_gak/domain/card/dao/TagRepository.java +++ b/src/main/java/com/server/bbo_gak/domain/card/dao/TagRepository.java @@ -7,7 +7,7 @@ public interface TagRepository extends JpaRepository { - List findAllByJob(Job job); + List findAllByJobIsIn(List jobList); List findAllByIdIsNotIn(List idList); diff --git a/src/main/java/com/server/bbo_gak/domain/card/dto/response/CardGetResponse.java b/src/main/java/com/server/bbo_gak/domain/card/dto/response/CardGetResponse.java index bb108d6..96248d7 100644 --- a/src/main/java/com/server/bbo_gak/domain/card/dto/response/CardGetResponse.java +++ b/src/main/java/com/server/bbo_gak/domain/card/dto/response/CardGetResponse.java @@ -3,8 +3,10 @@ import com.server.bbo_gak.domain.card.entity.Card; import com.server.bbo_gak.domain.card.entity.CardTag; import com.server.bbo_gak.domain.card.entity.CardTypeValueGroup; +import com.server.bbo_gak.domain.recruit.entity.Recruit; import com.server.bbo_gak.global.utils.BaseDateTimeFormatter; import java.util.List; +import java.util.Optional; import lombok.AccessLevel; import lombok.Builder; @@ -12,7 +14,9 @@ public record CardGetResponse( String title, String content, + String createdDate, String updatedDate, + String recruitTitle, String cardTypeValueGroup, List cardTypeValueList, List tagList @@ -28,11 +32,17 @@ public static CardGetResponse of(Card card, List cardTagList) { .map(cardType -> cardType.getCardTypeValue().getValue()) .toList(); + String recruitTitle = Optional.ofNullable(card.getRecruit()) + .map(Recruit::getTitle) + .orElse(null); + return CardGetResponse.builder() .title(card.getTitle()) .content(card.getContent()) + .recruitTitle(recruitTitle) .cardTypeValueGroup( CardTypeValueGroup.findByCardTypeValue(card.getCardTypeList().getFirst().getCardTypeValue()).getValue()) + .createdDate(card.getCreatedDate().format(BaseDateTimeFormatter.getLocalDateTimeFormatter())) .updatedDate(card.getUpdatedDate().format(BaseDateTimeFormatter.getLocalDateTimeFormatter())) .cardTypeValueList(cardTypeValueList) .tagList(tagGetResponseList) diff --git a/src/main/java/com/server/bbo_gak/domain/card/service/TagService.java b/src/main/java/com/server/bbo_gak/domain/card/service/TagService.java index a21c4bc..a3b5d28 100644 --- a/src/main/java/com/server/bbo_gak/domain/card/service/TagService.java +++ b/src/main/java/com/server/bbo_gak/domain/card/service/TagService.java @@ -1,15 +1,22 @@ package com.server.bbo_gak.domain.card.service; +import com.server.bbo_gak.domain.card.dao.CardDao; import com.server.bbo_gak.domain.card.dao.CardRepository; import com.server.bbo_gak.domain.card.dao.CardTagRepository; import com.server.bbo_gak.domain.card.dao.TagRepository; import com.server.bbo_gak.domain.card.dto.response.TagGetResponse; import com.server.bbo_gak.domain.card.entity.Card; import com.server.bbo_gak.domain.card.entity.CardTag; +import com.server.bbo_gak.domain.card.entity.CardTypeValue; +import com.server.bbo_gak.domain.card.entity.CardTypeValueGroup; import com.server.bbo_gak.domain.card.entity.Tag; +import com.server.bbo_gak.domain.recruit.dao.RecruitRepository; +import com.server.bbo_gak.domain.recruit.entity.Recruit; +import com.server.bbo_gak.domain.user.entity.Job; import com.server.bbo_gak.domain.user.entity.User; import com.server.bbo_gak.global.error.exception.BusinessException; import com.server.bbo_gak.global.error.exception.ErrorCode; +import com.server.bbo_gak.global.error.exception.InvalidValueException; import com.server.bbo_gak.global.error.exception.NotFoundException; import java.util.List; import lombok.RequiredArgsConstructor; @@ -23,11 +30,31 @@ public class TagService { private final CardRepository cardRepository; private final TagRepository tagRepository; private final CardTagRepository cardTagRepository; + private final RecruitRepository recruitRepository; + private final CardTypeService cardTypeService; + private final CardDao cardDao; + @Transactional(readOnly = true) public List getAllTagList(User user) { - return tagRepository.findAllByJob(user.getJob()).stream() + return tagRepository.findAllByJobIsIn(List.of(user.getJob(), Job.ALL)).stream() + .map(TagGetResponse::from) + .toList(); + } + + @Transactional(readOnly = true) + public List getAllRecruitsTagList(User user, Long recruitId, String cardTypeValue) { + + Recruit recruit = recruitRepository.findByUserIdAndId(user.getId(), recruitId) + .orElseThrow(() -> new NotFoundException(ErrorCode.RECRUIT_NOT_FOUND)); + + List cards = cardDao.findAllByUserIdAndCardTypeValue(user, CardTypeValue.findByValue(cardTypeValue), + recruit.getId()); + + return cards.stream() + .flatMap(card -> card.getCardTagList().stream().map(CardTag::getTag)) + .distinct() .map(TagGetResponse::from) .toList(); } @@ -83,4 +110,15 @@ private void validateTagDuplicated(Long tagId, Card card) { } } + private CardTypeValueGroup getRecruitCardTypeValueGroup(String type) { + CardTypeValueGroup cardTypeValueGroup = CardTypeValueGroup.findByCardTypeValue(CardTypeValue.findByValue(type)); + + if (cardTypeValueGroup.equals(CardTypeValueGroup.RECRUIT)) { + throw new InvalidValueException(ErrorCode.RECRUIT_CARD_TYPE_NOT_MATCHED); + } + + return cardTypeValueGroup; + } + + } diff --git a/src/main/java/com/server/bbo_gak/domain/recruit/dto/request/RecruitCreateRequest.java b/src/main/java/com/server/bbo_gak/domain/recruit/dto/request/RecruitCreateRequest.java index a07f655..e24a4f1 100644 --- a/src/main/java/com/server/bbo_gak/domain/recruit/dto/request/RecruitCreateRequest.java +++ b/src/main/java/com/server/bbo_gak/domain/recruit/dto/request/RecruitCreateRequest.java @@ -18,7 +18,7 @@ public Recruit toEntity(User user, Season season) { .season(season) .title(title) .siteUrl(siteUrl) - .recruitStatus(RecruitStatus.APPLICATION_COMPLETED) + .recruitStatus(RecruitStatus.PREPARATION_IN_PROGRESS) .user(user) .build(); } diff --git a/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatus.java b/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatus.java index a4fa5e3..6546f1a 100644 --- a/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatus.java +++ b/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatus.java @@ -15,7 +15,7 @@ public enum RecruitStatus { DOCUMENT_PASSED("서류 통과"), DOCUMENT_REJECTION("서류 탈락"), INTERVIEW_PASSED("면접 통과"), - INTERVIEW_REJECTION("서류 탈락"), + INTERVIEW_REJECTION("면접 탈락"), FINAL_ACCEPTANCE("최종 합격"), FINAL_REJECTED("최종 탈락"); diff --git a/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatusCategory.java b/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatusCategory.java index 3e5447f..6bc3588 100644 --- a/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatusCategory.java +++ b/src/main/java/com/server/bbo_gak/domain/recruit/entity/RecruitStatusCategory.java @@ -25,7 +25,7 @@ public enum RecruitStatusCategory { private final List statuses; - public static boolean isRejectionStatus(RecruitStatus status) { - return REJECTION_STATUS.getStatuses().contains(status); + public static boolean isRejectionStatusOrFinalAcceptance(RecruitStatus status) { + return REJECTION_STATUS.getStatuses().contains(status) || RecruitStatus.FINAL_ACCEPTANCE.equals(status); } } diff --git a/src/main/java/com/server/bbo_gak/domain/recruit/service/RecruitService.java b/src/main/java/com/server/bbo_gak/domain/recruit/service/RecruitService.java index ba1f102..99267b8 100644 --- a/src/main/java/com/server/bbo_gak/domain/recruit/service/RecruitService.java +++ b/src/main/java/com/server/bbo_gak/domain/recruit/service/RecruitService.java @@ -87,7 +87,7 @@ private LocalDate getNearestUpcomingDate(Recruit recruit) { private Map> partitionRecruits(List recruits) { return recruits.stream() - .filter(recruit -> !RecruitStatusCategory.isRejectionStatus( + .filter(recruit -> !RecruitStatusCategory.isRejectionStatusOrFinalAcceptance( recruit.getRecruitStatus())) // 불합격 상태 필터링 .collect(Collectors.partitioningBy(this::isNeedsScheduleUpdate)); } diff --git a/src/main/java/com/server/bbo_gak/domain/user/controller/UserController.java b/src/main/java/com/server/bbo_gak/domain/user/controller/UserController.java index a9c63b3..e579a84 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/controller/UserController.java +++ b/src/main/java/com/server/bbo_gak/domain/user/controller/UserController.java @@ -1,7 +1,9 @@ package com.server.bbo_gak.domain.user.controller; import com.server.bbo_gak.domain.user.dto.request.UserJobUpdateRequest; +import com.server.bbo_gak.domain.user.dto.request.UserOnboardStatusUpdateRequest; import com.server.bbo_gak.domain.user.dto.response.UserInfoResponse; +import com.server.bbo_gak.domain.user.dto.response.UserOnboardStatusGetResponse; import com.server.bbo_gak.domain.user.entity.User; import com.server.bbo_gak.domain.user.service.UserService; import com.server.bbo_gak.global.annotation.AuthUser; @@ -34,4 +36,20 @@ public ResponseEntity updateMemberJob( return ResponseEntity.ok(null); } + @GetMapping("/onboard-status") + public ResponseEntity getMemberOnboardStatus( + @AuthUser User user + ) { + return ResponseEntity.ok(userService.getUserOnboardStatus(user)); + } + + @PutMapping("/onboard-status") + public ResponseEntity updateMemberOnboardStatus( + @AuthUser User user, + @RequestBody UserOnboardStatusUpdateRequest request + ) { + userService.updateUserOnboardStatus(user, request.isOnboardComplete()); + return ResponseEntity.ok(null); + } + } diff --git a/src/main/java/com/server/bbo_gak/domain/user/dto/request/UserOnboardStatusUpdateRequest.java b/src/main/java/com/server/bbo_gak/domain/user/dto/request/UserOnboardStatusUpdateRequest.java new file mode 100644 index 0000000..5ce7401 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/user/dto/request/UserOnboardStatusUpdateRequest.java @@ -0,0 +1,4 @@ +package com.server.bbo_gak.domain.user.dto.request; + +public record UserOnboardStatusUpdateRequest(Boolean isOnboardComplete) { +} diff --git a/src/main/java/com/server/bbo_gak/domain/user/dto/response/UserOnboardStatusGetResponse.java b/src/main/java/com/server/bbo_gak/domain/user/dto/response/UserOnboardStatusGetResponse.java new file mode 100644 index 0000000..5cc2b36 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/user/dto/response/UserOnboardStatusGetResponse.java @@ -0,0 +1,7 @@ +package com.server.bbo_gak.domain.user.dto.response; + +public record UserOnboardStatusGetResponse( + Boolean isOnboardComplete +) { + +} diff --git a/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java b/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java index 09bebf0..4d6b237 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java +++ b/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java @@ -5,7 +5,8 @@ import java.util.Arrays; public enum OauthProvider { - GOOGLE; + GOOGLE, + KAKAO; public static OauthProvider findByName(String name) { return Arrays.stream(OauthProvider.values()) diff --git a/src/main/java/com/server/bbo_gak/domain/user/entity/OnboardStatus.java b/src/main/java/com/server/bbo_gak/domain/user/entity/OnboardStatus.java new file mode 100644 index 0000000..d373560 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/user/entity/OnboardStatus.java @@ -0,0 +1,23 @@ +package com.server.bbo_gak.domain.user.entity; + +import com.server.bbo_gak.global.error.exception.ErrorCode; +import com.server.bbo_gak.global.error.exception.NotFoundException; +import java.util.Arrays; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum OnboardStatus { + NOT_STARTED(false), + COMPLETED(true); + + private final Boolean value; // 온보딩 수행 여부 (true면 온보딩 완료) + + public static OnboardStatus findByValue(Boolean isOnboardComplete) { + return Arrays.stream(OnboardStatus.values()) + .filter(onboardStatus -> onboardStatus.getValue().equals(isOnboardComplete)) + .findFirst() + .orElseThrow((() -> new NotFoundException(ErrorCode.ONBOARD_STATUS_NOT_FOUND))); + } +} diff --git a/src/main/java/com/server/bbo_gak/domain/user/entity/User.java b/src/main/java/com/server/bbo_gak/domain/user/entity/User.java index 3c04048..5ad67a3 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/entity/User.java +++ b/src/main/java/com/server/bbo_gak/domain/user/entity/User.java @@ -40,16 +40,24 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Job job; + @Enumerated(EnumType.STRING) + private OnboardStatus onboardStatus; + // User 생성 팩토리 메서드 public static User from(OauthInfo oauthInfo) { return User.builder() .role(UserRole.USER) .oauthInfo(oauthInfo) .job(Job.UNDEFINE) + .onboardStatus(OnboardStatus.NOT_STARTED) .build(); } public void updateJob(Job job) { this.job = job; } + + public void updateOnboardStatus(OnboardStatus onboardStatus) { + this.onboardStatus = onboardStatus; + } } diff --git a/src/main/java/com/server/bbo_gak/domain/user/service/UserService.java b/src/main/java/com/server/bbo_gak/domain/user/service/UserService.java index bef4c4b..3353ea1 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/service/UserService.java +++ b/src/main/java/com/server/bbo_gak/domain/user/service/UserService.java @@ -1,6 +1,7 @@ package com.server.bbo_gak.domain.user.service; import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.server.bbo_gak.domain.user.dto.response.UserOnboardStatusGetResponse; import com.server.bbo_gak.domain.user.entity.User; public interface UserService { @@ -11,8 +12,12 @@ public interface UserService { void updateUserJob(User user, String job); + void updateUserOnboardStatus(User user, Boolean isOnboardComplete); + void getUser(); + UserOnboardStatusGetResponse getUserOnboardStatus(User user); + void deleteUser(); diff --git a/src/main/java/com/server/bbo_gak/domain/user/service/UserServiceImpl.java b/src/main/java/com/server/bbo_gak/domain/user/service/UserServiceImpl.java index 264069d..a2fd14d 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/server/bbo_gak/domain/user/service/UserServiceImpl.java @@ -1,7 +1,9 @@ package com.server.bbo_gak.domain.user.service; import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.server.bbo_gak.domain.user.dto.response.UserOnboardStatusGetResponse; import com.server.bbo_gak.domain.user.entity.Job; +import com.server.bbo_gak.domain.user.entity.OnboardStatus; import com.server.bbo_gak.domain.user.entity.User; import com.server.bbo_gak.domain.user.entity.UserRepository; import lombok.RequiredArgsConstructor; @@ -34,11 +36,22 @@ public void updateUserJob(User user, String job) { userRepository.save(user); } + @Transactional + public void updateUserOnboardStatus(User user, Boolean isOnboardComplete){ + user.updateOnboardStatus(OnboardStatus.findByValue(isOnboardComplete)); + userRepository.save(user); + } + @Override public void getUser() { } + @Override + public UserOnboardStatusGetResponse getUserOnboardStatus(User user) { + return new UserOnboardStatusGetResponse(user.getOnboardStatus().getValue()); + } + @Override public void deleteUser() { diff --git a/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java b/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java new file mode 100644 index 0000000..79cfa0b --- /dev/null +++ b/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java @@ -0,0 +1,25 @@ +package com.server.bbo_gak.global.config.oauth; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Getter +@Configuration +public class KakaoOAuthConfig { + + public static final String AUTHORIZATION = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; + public static final String KAKAO_CODE_URI = "https://kauth.kakao.com/oauth/authorize"; + public static final String KAKAO_TOKEN_URI = "https://kauth.kakao.com/oauth/token"; + public static final String KAKAO_USER_INFO_URI = "https://kapi.kakao.com/v2/user/me"; + + @Value("${kakao.login.client_id}") + private String kakaoClientId; + + @Value("${kakao.login.client_secret}") + private String kakaoClientSecret; + + @Value("${kakao.login.redirect_uri}") + private String kakaoRedirectUri; +} diff --git a/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java b/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java index 6de60df..55d2bd6 100644 --- a/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java +++ b/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java @@ -28,7 +28,7 @@ public class SecurityConfig { private final JwtTokenService jwtTokenService; private String[] allowUrls = {"/", "/api/v1/users/test/login", "/docs/**", "/v3/**", "/favicon.ico", - "/api/v1/users/refreshToken", "/api/v1/users/social-login", "/api/v1/users/test/access-token", + "/api/v1/users/refreshToken", "/api/v1/users/social-login", "/api/v1/users/test/**", "/api/docs/**", "/api/v3/**", "/api/health-check/**"}; @Bean diff --git a/src/main/java/com/server/bbo_gak/global/error/exception/ErrorCode.java b/src/main/java/com/server/bbo_gak/global/error/exception/ErrorCode.java index 98d8633..7ac36a6 100644 --- a/src/main/java/com/server/bbo_gak/global/error/exception/ErrorCode.java +++ b/src/main/java/com/server/bbo_gak/global/error/exception/ErrorCode.java @@ -33,6 +33,7 @@ public enum ErrorCode { //User USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 직업을 찾을 수 없습니다"), + ONBOARD_STATUS_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 온보딩 상태를 찾을 수 없습니다. (잘못된 온보딩 상태를 입력하셨습니다)"), //Image IMAGE_FILE_EXTENSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "지원하지 않는 파일 확장자입니다."), @@ -44,6 +45,7 @@ public enum ErrorCode { CARD_TYPE_VALUE_GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 카드 타입 그룹(내 정보 or 공고 or 공고_복사본_제외)을 찾을 수 없습니다."), CARD_TYPE_NOT_MATCHED(HttpStatus.BAD_REQUEST, "카드 타입이 맞지 않습니다"), MY_INFO_CARD_TYPE_OVERSIZE(HttpStatus.BAD_REQUEST, "내 정보에서는 하나의 카드 타입만 할당 가능합니다."), + RECRUIT_CARD_TYPE_NOT_MATCHED(HttpStatus.BAD_REQUEST, "공고의 카드 타입으로만 가능합니다"), //Tag TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 태그를 찾을 수 없습니다"), diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml index c58a09b..7af77de 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -10,3 +10,9 @@ google: client_id: ${GOOGLE_LOGIN_CLIENT_ID} client_secret: ${GOOGLE_LOGIN_CLIENT_SECRET} redirect_uri: ${GOOGLE_LOGIN_REDIRECT_URI} + +kakao: + login: + client_id: ${KAKAO_LOGIN_CLIENT_ID} + client_secret: ${KAKAO_LOGIN_CLIENT_SECRET} + redirect_uri: ${KAKAO_LOGIN_REDIRECT_URI} \ No newline at end of file diff --git a/src/test/java/com/server/bbo_gak/domain/card/controller/CardControllerTest.java b/src/test/java/com/server/bbo_gak/domain/card/controller/CardControllerTest.java index 185d78c..1c21fab 100644 --- a/src/test/java/com/server/bbo_gak/domain/card/controller/CardControllerTest.java +++ b/src/test/java/com/server/bbo_gak/domain/card/controller/CardControllerTest.java @@ -96,7 +96,9 @@ class 카드_단건_조회 { .responseSchema(Schema.schema("CardGetResponse")) .responseFields(fieldWithPath("title").type(JsonFieldType.STRING).description("Card 제목"), fieldWithPath("content").type(JsonFieldType.STRING).description("Card 내용"), + fieldWithPath("recruitTitle").type(JsonFieldType.STRING).optional().description("공고 제목"), fieldWithPath("cardTypeValueList").type(JsonFieldType.ARRAY).description("Card 타입값 리스트"), + fieldWithPath("createdDate").type(JsonFieldType.STRING).description("Card 생성일시"), fieldWithPath("updatedDate").type(JsonFieldType.STRING).description("Card 수정일시"), fieldWithPath("cardTypeValueGroup").type(JsonFieldType.STRING).description("Card 그룹 이름"), fieldWithPath("tagList.[].id").type(JsonFieldType.NUMBER).description("태그 ID"), diff --git a/src/test/java/com/server/bbo_gak/domain/card/controller/TagControllerTest.java b/src/test/java/com/server/bbo_gak/domain/card/controller/TagControllerTest.java index d592955..12b438f 100644 --- a/src/test/java/com/server/bbo_gak/domain/card/controller/TagControllerTest.java +++ b/src/test/java/com/server/bbo_gak/domain/card/controller/TagControllerTest.java @@ -75,6 +75,46 @@ private ResourceSnippetParameters getSuccessResponseResource() { } } + @Nested + class 전체_태그_목록_조회_공고에서 { + + @Test + @Transactional + public void 성공() throws Exception { + + // TEST + ResultActions resultActions = mockMvc.perform(getRequest()) + .andExpect(status().isOk()); + + // DOCS + resultActions.andDo(document("[전체_태그_목록_조회_공고에서] 성공", resource(getSuccessResponseResource()))); + } + + private MockHttpServletRequestBuilder getRequest() { + return get(DEFAULT_URL + "/recruits/{recruit-id}/tags", 1L) + .queryParam("type", "인터뷰_준비") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON); + } + + private ResourceSnippetParameters getSuccessResponseResource() { + return ResourceSnippetParameters.builder() + .description("전체 카드 태그 목록").tags("Tag") + .pathParameters( + parameterWithName("recruit-id").description("recruit-id") + ) + .queryParameters( + parameterWithName("type").description("type") + ) + .responseSchema(Schema.schema("TagGetResponse")) + .responseFields( + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("태그 ID"), + fieldWithPath("[].name").type(JsonFieldType.STRING).description("태그 이름"), + fieldWithPath("[].type").type(JsonFieldType.STRING).description("태그 타입")) + .build(); + } + } + @Nested class 카드_태그_목록_조회 { diff --git a/src/test/java/com/server/bbo_gak/domain/user/controller/UserControllerTest.java b/src/test/java/com/server/bbo_gak/domain/user/controller/UserControllerTest.java index e2ae63f..6bf1cf3 100644 --- a/src/test/java/com/server/bbo_gak/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/server/bbo_gak/domain/user/controller/UserControllerTest.java @@ -3,6 +3,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.server.bbo_gak.domain.user.dto.request.UserJobUpdateRequest; +import com.server.bbo_gak.domain.user.dto.request.UserOnboardStatusUpdateRequest; +import com.server.bbo_gak.domain.user.dto.response.UserOnboardStatusGetResponse; import com.server.bbo_gak.global.AbstractRestDocsTests; import com.server.bbo_gak.global.RestDocsFactory; import org.junit.jupiter.api.Nested; @@ -40,4 +42,54 @@ class 직군선택 { } } + + @Nested + class 온보딩상태_조회 { + + @Test + public void 성공_온보딩_완료_상태() throws Exception { + + //given + UserOnboardStatusGetResponse response = new UserOnboardStatusGetResponse(true); + + //then + mockMvc.perform(restDocsFactory.createRequest(DEFAULT_URL + "/onboard-status", null, HttpMethod.GET, + objectMapper)) + .andExpect(status().isOk()) + .andDo(restDocsFactory.getSuccessResource("[온보딩상태_조회] 성공-온보딩 완료 상태", "온보딩상태 조회", "user", null, response)); + + } + + @Test + public void 성공_온보딩_미완료_상태() throws Exception { + + //given + UserOnboardStatusGetResponse response = new UserOnboardStatusGetResponse(false); + + //then + mockMvc.perform(restDocsFactory.createRequest(DEFAULT_URL + "/onboard-status", null, HttpMethod.GET, + objectMapper)) + .andExpect(status().isOk()) + .andDo(restDocsFactory.getSuccessResource("[온보딩상태_조회] 성공-온보딩 미완료 상태", "온보딩상태 조회", "user", null, response)); + + } + } + + @Nested + class 온보딩상태_업데이트 { + + @Test + public void 성공() throws Exception { + + //given + UserOnboardStatusUpdateRequest request = new UserOnboardStatusUpdateRequest(true); + + //then + mockMvc.perform(restDocsFactory.createRequest(DEFAULT_URL + "/onboard-status", request, HttpMethod.PUT, + objectMapper)) + .andExpect(status().isOk()) + .andDo(restDocsFactory.getSuccessResource("[온보딩상태_업데이트] 성공", "온보딩상태 업데이트", "user", request, null)); + + } + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 075d1ee..443db87 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -44,6 +44,12 @@ google: client_secret: test redirect_uri: test +kakao: + login: + client_id: test + client_secret: test + redirect_uri: test + scheduler: thread: pool: diff --git a/src/test/resources/user-test-data.sql b/src/test/resources/user-test-data.sql index b39437a..894d2c1 100644 --- a/src/test/resources/user-test-data.sql +++ b/src/test/resources/user-test-data.sql @@ -17,12 +17,14 @@ from recruit_season; delete from users; -INSERT INTO users (deleted, created_at, update_at, user_id, dtype, email, login_id, name, password, role, job) +INSERT INTO users (deleted, created_at, update_at, user_id, dtype, email, login_id, name, password, role, job, + onboard_status) VALUES (false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 1, 'AuthTestUser', 'email', 'test', 'test', - 'test123', 'USER', 'UNDEFINE'); + 'test123', 'USER', 'UNDEFINE', 'NOT_STARTED'); -INSERT INTO users( deleted, created_at, update_at, user_id, dtype, role, oauth_id, name, email, provider, job) -VALUES(false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 2, 'User', 'USER', 'oauthId', 'name', 'email', 'GOOGLE', 'UNDEFINE'); +INSERT INTO users( deleted, created_at, update_at, user_id, dtype, role, oauth_id, name, email, provider, job, onboard_status) +VALUES(false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 2, 'User', 'USER', 'oauthId', 'name', + 'email', 'GOOGLE', 'DEVELOPER', 'COMPLETED'); INSERT INTO refresh_token (id, token) VALUES (1, 'abcd1234');