diff --git a/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingApi.java b/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingApi.java new file mode 100644 index 00000000..a565cb4f --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingApi.java @@ -0,0 +1,45 @@ +package org.sopt.kareer.domain.jobposting.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.jobposting.dto.response.JobPostingCrawlListResponse; +import org.sopt.kareer.domain.jobposting.dto.response.JobPostingListResponse; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; + +@Tag(name = "채용 공고 관련 API") +public interface JobPostingApi { + + @GetMapping("crawl") + @Operation(summary = "채용 공고 크롤링 (Server Only)") + ResponseEntity> crawlJobPostings(@RequestParam(defaultValue = "5") int limit); + + @PostMapping(value = "recommend", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "채용 공고 추천", description = "사용자가 업로드한 이력서/자소서, 사용자 정보 기반으로 채용 공고를 추천합니다.") + @CustomExceptionDescription(RECOMMEND_JOBPOSTING) + ResponseEntity> recommendJobPostings( + @AuthenticationPrincipal Long memberId, + @RequestPart(value = "files", required = false) List files, + @RequestParam(value = "includeCompletedTodo", defaultValue = "false") boolean includeCompletedTodos); + + @PostMapping("{jobPostingId}/bookmarks") + @Operation(summary = "채용 공고 북마크 추가/삭제", description = "사용자가 추천된 채용 공고를 추가하거나 삭제합니다.") + @CustomExceptionDescription(CREATE_BOOKMARK) + ResponseEntity> createJobPostingBookmark( + @AuthenticationPrincipal Long memberId, + @PathVariable Long jobPostingId); + + @GetMapping("bookmarks") + @Operation(summary = "채용 공고 북마크 조회", description = "사용자가 북마크한 채용 공고를 조회합니다.") + @CustomExceptionDescription(GET_BOOKMARK) + ResponseEntity> getJobPostingBookmarks(@AuthenticationPrincipal Long memberId); +} diff --git a/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java b/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java index f295f0d5..379e7da4 100644 --- a/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java +++ b/src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java @@ -1,16 +1,12 @@ package org.sopt.kareer.domain.jobposting.controller; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sopt.kareer.domain.jobposting.dto.response.JobPostingCrawlListResponse; import org.sopt.kareer.domain.jobposting.dto.response.JobPostingListResponse; import org.sopt.kareer.domain.jobposting.service.JobPostingCrawler; import org.sopt.kareer.domain.jobposting.service.JobPostingService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -18,56 +14,41 @@ import java.util.List; -import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; - @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/job-postings") -@Tag(name = "채용 공고 관련 API") -public class JobPostingController { +public class JobPostingController implements JobPostingApi { private final JobPostingCrawler jobPostingCrawler; private final JobPostingService jobPostingService; - @Operation(summary = "채용 공고 크롤링 (Server Only)") - @GetMapping("crawl") + @Override public ResponseEntity> crawlJobPostings(@RequestParam(defaultValue = "5") int limit) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(jobPostingCrawler.crawlJobPostingForTest(limit), "채용 공고 크롤링에 성공하였습니다.")); } - @Operation(summary = "채용 공고 추천", description = "사용자가 업로드한 이력서/자소서, 사용자 정보 기반으로 채용 공고를 추천합니다.") - @CustomExceptionDescription(RECOMMEND_JOBPOSTING) - @PostMapping(value = "recommend", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Override public ResponseEntity> recommendJobPostings( @AuthenticationPrincipal Long memberId, @RequestPart(value = "files", required = false) List files, - @RequestParam(value = "includeCompletedTodo", defaultValue = "false") boolean includeCompletedTodos - ){ + @RequestParam(value = "includeCompletedTodo", defaultValue = "false") boolean includeCompletedTodos) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(jobPostingService.recommend(memberId, files, includeCompletedTodos), "채용 공고 추천에 성공하였습니다.")); } - @Operation(summary = "채용 공고 북마크 추가/삭제", description = "사용자가 추천된 채용 공고를 추가하거나 삭제합니다.") - @CustomExceptionDescription(CREATE_BOOKMARK) - @PostMapping("{jobPostingId}/bookmarks") + @Override public ResponseEntity> createJobPostingBookmark( @AuthenticationPrincipal Long memberId, - @PathVariable Long jobPostingId){ + @PathVariable Long jobPostingId) { jobPostingService.createOrDeleteBookmark(memberId, jobPostingId); - return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok("채용 공고 북마크 추가 / 삭제에 성공했습니다.")); } - @Operation(summary = "채용 공고 북마크 조회", description = "사용자가 북마크한 채용 공고를 조회합니다.") - @CustomExceptionDescription(GET_BOOKMARK) - @GetMapping("bookmarks") - public ResponseEntity> getJobPostingBookmarks( - @AuthenticationPrincipal Long memberId - ){ + @Override + public ResponseEntity> getJobPostingBookmarks(@AuthenticationPrincipal Long memberId) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(jobPostingService.getJobPostingBookmarks(memberId), "북마크 채용 공고 조회에 성공하였습니다.")); } - } diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberApi.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberApi.java new file mode 100644 index 00000000..7df5e69a --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberApi.java @@ -0,0 +1,87 @@ +package org.sopt.kareer.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest; +import org.sopt.kareer.domain.member.dto.request.MypageRequest; +import org.sopt.kareer.domain.member.dto.response.*; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Member API") +public interface MemberApi { + + @GetMapping("/me") + @Operation(summary = "회원 정보 조회", description = "로그인한 회원의 정보를 조회합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.MEMBER_INFO) + ResponseEntity> getMemberInfo(@AuthenticationPrincipal Long memberId); + + @GetMapping("/onboard/universities") + @Operation(summary = "온보딩 대학교 목록 조회", description = "회원 온보딩 시 선택할 수 있는 대학교 목록을 조회합니다.") + ResponseEntity> getOnboardUniversities(); + + @GetMapping("/onboard/countries") + @Operation(summary = "온보딩 국가 목록 조회", description = "회원 온보딩 시 선택할 수 있는 국가 목록을 조회합니다.") + ResponseEntity> getOnboardCountries(); + + @GetMapping("/onboard/majors") + @Operation(summary = "온보딩 전공 목록 조회", description = "회원 온보딩 시 선택할 수 있는 전공 목록을 조회합니다.") + ResponseEntity> getOnboardMajors(); + + @GetMapping("/onboard/fields") + @Operation(summary = "온보딩 관심 분야 목록 조회", description = "회원 온보딩 시 선택할 수 있는 관심 분야 목록을 조회합니다.") + ResponseEntity> getOnboardFields(); + + @GetMapping("me/status") + @Operation(summary = "유저 상태 조회", description = "사용자의 비자 정보, 졸업 정보를 조회합니다.") + @CustomExceptionDescription(USER_STATUS) + ResponseEntity> getMemberStatus(@AuthenticationPrincipal Long memberId); + + @GetMapping("mypage") + @Operation(summary = "마이페이지 조회", description = "마이페이지에서 유저 정보를 조회합니다.") + @CustomExceptionDescription(GET_MYPAGE) + ResponseEntity> getMypage(@AuthenticationPrincipal Long memberId); + + @PutMapping("mypage") + @Operation(summary = "마이페이지 수정", description = "마이페이지에서 유저 프로필을 수정합니다.") + @CustomExceptionDescription(UPDATE_MYPAGE) + ResponseEntity> updateMypage(@AuthenticationPrincipal Long memberId, + @Valid @RequestBody MypageRequest request); + + @DeleteMapping("/me") + @Operation(summary = "회원 탈퇴", description = "회원 탈퇴를 진행합니다.") + @CustomExceptionDescription(MEMBER_DELETE) + ResponseEntity> deleteMember(@AuthenticationPrincipal Long memberId, + HttpServletRequest request, + HttpServletResponse response); + + @PostMapping(value = "/onboard/ocr/visa", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "온보딩 비자 OCR API", description = "온보딩 과정에서 유저의 비자 문서를 분석하여 정보를 추출합니다.") + ResponseEntity> getVisaInfo(@RequestPart("file") MultipartFile file); + + @PostMapping(value = "/onboard/ocr/passport", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "온보딩 여권 OCR API", description = "온보딩 과정에서 유저의 여권을 분석하여 정보를 추출합니다.") + ResponseEntity> getPassportInfo(@RequestPart("file") MultipartFile file); + + @PostMapping("/term-agreements") + @Operation(summary = "약관 동의", description = "약관에 동의합니다.") + @CustomExceptionDescription(TERM_AGREE) + ResponseEntity> agreeTerms(@AuthenticationPrincipal Long memberId, + @RequestBody @Valid MemberTermsRequest request); + + @GetMapping("completion") + @Operation(summary = "온보딩, 약관동의 여부 조회") + @CustomExceptionDescription(GET_COMPLETION) + ResponseEntity> getMemberCompletionStatus(@AuthenticationPrincipal Long memberId); +} diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberApiV2.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberApiV2.java new file mode 100644 index 00000000..91c5914e --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberApiV2.java @@ -0,0 +1,23 @@ +package org.sopt.kareer.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Member API V2", description = "회원 API 버전 2") +public interface MemberApiV2 { + + @PostMapping("/onboard") + @Operation(summary = "회원 온보딩 V2", description = "PENDING 상태의 회원의 온보딩 결과를 저장합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.MEMBER_ONBOARD) + ResponseEntity> onboardMember(@AuthenticationPrincipal Long memberId, + @Valid @RequestBody MemberOnboardV2Request request); +} diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java index 3345fb34..67b0084d 100644 --- a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java @@ -1,144 +1,77 @@ package org.sopt.kareer.domain.member.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest; import org.sopt.kareer.domain.member.dto.request.MypageRequest; import org.sopt.kareer.domain.member.dto.response.*; import org.sopt.kareer.domain.member.service.LocalizedOnboardQueryService; import org.sopt.kareer.domain.member.service.MemberService; -import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; -import org.sopt.kareer.domain.roadmap.service.RoadMapService; -import org.sopt.kareer.domain.roadmap.service.RoadmapTranslationService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; import org.sopt.kareer.global.auth.service.AuthService; -import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; - @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/members") -@Tag(name = "Member API") -public class MemberController { +public class MemberController implements MemberApi { private final MemberService memberService; - private final RoadMapService roadMapService; - private final RoadmapTranslationService roadmapTranslationService; private final AuthService authService; private final LocalizedOnboardQueryService localizedOnboardQueryService; - @GetMapping("/me") - @Operation(summary = "회원 정보 조회", description = "로그인한 회원의 정보를 조회합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.MEMBER_INFO) + @Override public ResponseEntity> getMemberInfo(@AuthenticationPrincipal Long memberId) { - return ResponseEntity - .status(HttpStatus.OK) + return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(memberService.getMemberInfo(memberId), "회원 정보 조회에 성공하였습니다.")); } - @PostMapping("/onboard") - @Operation(summary = "회원 온보딩", description = "PENDING 상태의 회원의 온보딩 결과를 저장합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.MEMBER_ONBOARD) - public ResponseEntity> onboardMember(@AuthenticationPrincipal Long memberId, - @Valid @RequestBody MemberOnboardRequest request) { - memberService.onboardMember(request, memberId); - return ResponseEntity - .status(HttpStatus.OK) - .body(BaseResponse.ok("회원 온보딩이 완료되었습니다.")); - } - - @GetMapping("/onboard/universities") - @Operation(summary = "온보딩 대학교 목록 조회", description = "회원 온보딩 시 선택할 수 있는 대학교 목록을 조회합니다.") + @Override public ResponseEntity> getOnboardUniversities() { - return ResponseEntity - .status(HttpStatus.OK) - .body(BaseResponse.ok(localizedOnboardQueryService.getUniversities(), - "온보딩 대학교 목록 조회에 성공하였습니다.")); + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok(localizedOnboardQueryService.getUniversities(), "온보딩 대학교 목록 조회에 성공하였습니다.")); } - @GetMapping("/onboard/countries") - @Operation(summary = "온보딩 국가 목록 조회", description = "회원 온보딩 시 선택할 수 있는 국가 목록을 조회합니다.") + @Override public ResponseEntity> getOnboardCountries() { - return ResponseEntity - .status(HttpStatus.OK) - .body(BaseResponse.ok( - localizedOnboardQueryService.getCountries(), - "온보딩 국가 목록 조회에 성공하였습니다.")); + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok(localizedOnboardQueryService.getCountries(), "온보딩 국가 목록 조회에 성공하였습니다.")); } - @GetMapping("/onboard/majors") - @Operation(summary = "온보딩 전공 목록 조회", description = "회원 온보딩 시 선택할 수 있는 전공 목록을 조회합니다.") + @Override public ResponseEntity> getOnboardMajors() { - return ResponseEntity - .status(HttpStatus.OK) + return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(localizedOnboardQueryService.getMajors(), "온보딩 전공 목록 조회에 성공하였습니다.")); } - @GetMapping("/onboard/fields") - @Operation(summary = "온보딩 관심 분야 목록 조회", description = "회원 온보딩 시 선택할 수 있는 관심 분야 목록을 조회합니다.") + @Override public ResponseEntity> getOnboardFields() { - return ResponseEntity - .status(HttpStatus.OK) - .body(BaseResponse.ok(localizedOnboardQueryService.getFields(), "온보딩 관심 분야 목록 조회에 성공하였습니다.")); - } - - @Operation(summary = "AI 로드맵 생성 API", description = "사용자가 온보딩에 입력한 정보를 통해 로드맵을 생성합니다.") - @CustomExceptionDescription(CREATE_ROADMAP) - @PostMapping("roadmap") - public ResponseEntity> generateRoadmap( - @AuthenticationPrincipal Long memberId) { - - var target = roadMapService.createRoadmap(memberId); - - roadmapTranslationService.translateAllLanguages(target); - - return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok("AI 로드맵 생성에 성공하였습니다.")); - } - - @Operation(summary = "AI 로드맵 생성 테스트용 API (Server Only)", description = "사용자가 온보딩에 입력한 정보를 통해 로드맵을 생성합니다.") - @CustomExceptionDescription(CREATE_ROADMAP) - @PostMapping("roadmap/test") - public ResponseEntity> generateRoadmapForTest( - @AuthenticationPrincipal Long memberId) { - return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(roadMapService.createRoadmapTest(memberId), "AI 로드맵 생성에 성공하였습니다.")); + .body(BaseResponse.ok(localizedOnboardQueryService.getFields(), "온보딩 관심 분야 목록 조회에 성공하였습니다.")); } - @Operation(summary = "유저 상태 조회", description = "사용자의 비자 정보, 졸업 정보를 조회합니다.") - @CustomExceptionDescription(USER_STATUS) - @GetMapping("me/status") + @Override public ResponseEntity> getMemberStatus(@AuthenticationPrincipal Long memberId) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(memberService.getMemberStatus(memberId), "My status 조회에 성공하였습니다.")); } - @Operation(summary = "마이페이지 조회", description = "마이페이지에서 유저 정보를 조회합니다.") - @CustomExceptionDescription(GET_MYPAGE) - @GetMapping("mypage") + @Override public ResponseEntity> getMypage(@AuthenticationPrincipal Long memberId) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(memberService.getMypage(memberId), "마이페이지 조회에 성공하였습니다.")); } - @Operation(summary = "마이페이지 수정", description = "마이페이지에서 유저 프로필을 수정합니다.") - @CustomExceptionDescription(UPDATE_MYPAGE) - @PutMapping("mypage") + @Override public ResponseEntity> updateMypage(@AuthenticationPrincipal Long memberId, @Valid @RequestBody MypageRequest request) { memberService.updateMypage(memberId, request.toCommand()); @@ -146,9 +79,7 @@ public ResponseEntity> updateMypage(@AuthenticationPrincipal .body(BaseResponse.ok("마이페이지 수정에 성공하였습니다.")); } - @Operation(summary = "회원 탈퇴", description = "회원 탈퇴를 진행합니다.") - @CustomExceptionDescription(MEMBER_DELETE) - @DeleteMapping("/me") + @Override public ResponseEntity> deleteMember(@AuthenticationPrincipal Long memberId, HttpServletRequest request, HttpServletResponse response) { @@ -158,43 +89,29 @@ public ResponseEntity> deleteMember(@AuthenticationPrincipal .body(BaseResponse.ok("회원 탈퇴에 성공하였습니다.")); } - @Operation(summary = "온보딩 비자 OCR API", description = "온보딩 과정에서 유저의 비자 문서를 분석하여 정보를 추출합니다.") - @PostMapping(value = "/onboard/ocr/visa", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> getVisaInfo( - @RequestPart("file") MultipartFile file){ + @Override + public ResponseEntity> getVisaInfo(@RequestPart("file") MultipartFile file) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(memberService.getVisaOcr(file), "사용자 비자 정보 추출에 성공했습니다.")); } - @Operation(summary = "온보딩 여권 OCR API", description = "온보딩 과정에서 유저의 여권을 분석하여 정보를 추출합니다.") - @PostMapping(value = "/onboard/ocr/passport", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> getPassportInfo( - @RequestPart("file") MultipartFile file){ + @Override + public ResponseEntity> getPassportInfo(@RequestPart("file") MultipartFile file) { return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(memberService.getPassportOcr(file), "사용자 여권 정보 추출에 성공했습니다.")); } - - @Operation(summary = "약관 동의", description = "약관에 동의합니다.") - @CustomExceptionDescription(TERM_AGREE) - @PostMapping("/term-agreements") - public ResponseEntity> agreeTerms( - @AuthenticationPrincipal Long memberId, - @RequestBody @Valid MemberTermsRequest request - ) { + @Override + public ResponseEntity> agreeTerms(@AuthenticationPrincipal Long memberId, + @RequestBody @Valid MemberTermsRequest request) { memberService.agreeTerms(memberId, request); return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok("약관 동의 저장에 성공했습니다.")); } - @Operation(summary = "온보딩, 약관동의 여부 조회") - @CustomExceptionDescription(GET_COMPLETION) - @GetMapping("completion") - public ResponseEntity> getMemberCompletionStatus( - @AuthenticationPrincipal Long memberId - ){ + @Override + public ResponseEntity> getMemberCompletionStatus(@AuthenticationPrincipal Long memberId) { return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(memberService.getCompletion(memberId), "온보딩/약관동의 여부 조회에 성공하였습니다,")); + .body(BaseResponse.ok(memberService.getCompletion(memberId), "온보딩/약관동의 여부 조회에 성공하였습니다.")); } - } diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java index c4bb16c9..26eed44a 100644 --- a/src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java @@ -1,34 +1,27 @@ package org.sopt.kareer.domain.member.controller; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.*; +import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; import org.sopt.kareer.domain.member.service.MemberService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; -import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; -import org.springframework.http.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api/v2/members") -@Tag(name = "Member API V2", description = "회원 API 버전 2") -public class MemberControllerV2 { +public class MemberControllerV2 implements MemberApiV2 { private final MemberService memberService; - @PostMapping("/onboard") - @Operation(summary = "회원 온보딩 V2", description = "PENDING 상태의 회원의 온보딩 결과를 저장합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.MEMBER_ONBOARD) + @Override public ResponseEntity> onboardMember(@AuthenticationPrincipal Long memberId, @Valid @RequestBody MemberOnboardV2Request request) { memberService.onboardMemberV2(request, memberId); - return ResponseEntity - .status(HttpStatus.OK) + return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok("회원 온보딩이 완료되었습니다.")); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java index dcfae8bc..9bdbf167 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java @@ -74,46 +74,7 @@ public class Member extends BaseEntity { private String preparationStatus; private String fieldsOfInterest; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private RoadmapStatus roadmapStatus; - - // 프론트 온보딩 구현 완료 후 삭제 예정 - public void updateInfo(String name, - LocalDate birthDate, - Country country, - String university, - EnglishLevel englishLevel, - String fieldsOfInterests, - String preparationStatuses, - LanguageLevel languageLevel, - Degree degree, - LocalDate expectedGraduationDate, - String primaryMajor, - String secondaryMajor, - String targetJob, - String targetJobSkill, - String personalBackground) { - assertPendingStatus(); - this.name = name; - this.birthDate = birthDate; - this.countryCode = country.getCountryName(); - this.universityCode = university; - this.englishLevelCode = englishLevel.getDescription(); - this.fieldsOfInterest = fieldsOfInterests; - this.preparationStatus = preparationStatuses; - this.languageLevel = languageLevel; - this.degreeCode = degree.getDescription(); - this.expectedGraduationDate = expectedGraduationDate; - this.primaryMajorCode = primaryMajor; - this.secondaryMajor = secondaryMajor; - this.targetJob = targetJob; - this.targetJobSkill = targetJobSkill; - this.status = MemberStatus.ACTIVE; - this.personalBackground = personalBackground; - } - + public void updateInfoV2(String name, LocalDate birthDate, String countryCode, @@ -160,7 +121,6 @@ public static Member createOAuthMember(String name, .provider(provider) .providerId(providerId) .profileImageUrl(profileImageUrl) - .roadmapStatus(RoadmapStatus.NOT_STARTED) .build(); } @@ -176,19 +136,6 @@ private void assertPendingStatus() { } - public void assertCanStartRoadmap() { - if (roadmapStatus == RoadmapStatus.IN_PROGRESS) { - throw new MemberException(MemberErrorCode.ROADMAP_IN_PROGRESS); - } - if (roadmapStatus == RoadmapStatus.DONE) { - throw new MemberException(MemberErrorCode.ROADMAP_ALREADY_GENERATED); - } - } - - public void markRoadmapInProgress() { this.roadmapStatus = RoadmapStatus.IN_PROGRESS; } - public void markRoadmapDone() { this.roadmapStatus = RoadmapStatus.DONE; } - public void markRoadmapFailed() { this.roadmapStatus = RoadmapStatus.FAILED; } - public void updateProfile( String targetJob, LocalDate birthDate, diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java b/src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java deleted file mode 100644 index 06bb405f..00000000 --- a/src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.sopt.kareer.domain.member.entity.enums; - -public enum RoadmapStatus { - NOT_STARTED, IN_PROGRESS, DONE, FAILED -} diff --git a/src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java b/src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java index cf0921be..308a48ca 100644 --- a/src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java @@ -12,9 +12,7 @@ public enum MemberErrorCode implements ErrorCode { ONBOARDING_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST.value(), "이미 온보딩이 완료된 회원입니다."), INVALID_VISA_POINT(HttpStatus.BAD_REQUEST.value(), "visaPoint는 D10 비자인 경우에만 입력할 수 있습니다."), INVALID_COUNTRY(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 국가입니다."), - VISA_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "비자 정보가 존재하지 않습니다."), - ROADMAP_IN_PROGRESS(HttpStatus.BAD_REQUEST.value(), "로드맵이 생성 중입니다."), - ROADMAP_ALREADY_GENERATED(HttpStatus.CONFLICT.value(), "이미 로드맵을 생성하였습니다."); + VISA_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "비자 정보가 존재하지 않습니다."); private final int httpStatus; private final String message; diff --git a/src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java b/src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java index 50df62f3..741cc272 100644 --- a/src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java +++ b/src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java @@ -3,22 +3,10 @@ import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.member.entity.enums.OAuthProvider; 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 java.util.Optional; public interface MemberRepository extends JpaRepository { Optional findByProviderAndProviderId(OAuthProvider provider, String providerId); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query(""" - update Member m - set m.roadmapStatus = 'IN_PROGRESS' - where m.id = :memberId - and m.roadmapStatus in ('NOT_STARTED','FAILED') - """) - int tryMarkRoadmapInProgress(@Param("memberId") Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java index c7321d38..5a579df4 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java @@ -16,6 +16,7 @@ import org.sopt.kareer.domain.roadmap.repository.PhaseActionTranslationRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseTranslationRepository; +import org.sopt.kareer.domain.roadmap.repository.RoadmapRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,22 +38,24 @@ public class MemberDeletionService { private final PhaseActionMistakeTranslationRepository phaseActionMistakeTranslationRepository; private final ActionItemRepository actionItemRepository; private final ActionItemTranslationRepository actionItemTranslationRepository; + private final RoadmapRepository roadmapRepository; @Transactional public void deleteMember(Member member) { Long memberId = member.getId(); - phaseActionGuidelineTranslationRepository.deleteAllByGuideline_PhaseAction_Phase_Member_Id(memberId); - phaseActionMistakeTranslationRepository.deleteAllByMistake_PhaseAction_Phase_Member_Id(memberId); + phaseActionGuidelineTranslationRepository.deleteAllByGuideline_PhaseAction_Phase_Roadmap_Member_Id(memberId); + phaseActionMistakeTranslationRepository.deleteAllByMistake_PhaseAction_Phase_Roadmap_Member_Id(memberId); actionItemTranslationRepository.deleteAllByActionItem_Member_Id(memberId); - phaseActionTranslationRepository.deleteAllByPhaseAction_Phase_Member_Id(memberId); - phaseTranslationRepository.deleteAllByPhase_Member_Id(memberId); + phaseActionTranslationRepository.deleteAllByPhaseAction_Phase_Roadmap_Member_Id(memberId); + phaseTranslationRepository.deleteAllByPhase_Roadmap_Member_Id(memberId); - phaseActionGuidelineRepository.deleteAllByPhaseAction_Phase_Member_Id(memberId); - phaseActionMistakeRepository.deleteAllByPhaseAction_Phase_Member_Id(memberId); + phaseActionGuidelineRepository.deleteAllByPhaseAction_Phase_Roadmap_Member_Id(memberId); + phaseActionMistakeRepository.deleteAllByPhaseAction_Phase_Roadmap_Member_Id(memberId); actionItemRepository.deleteAllByMemberId(memberId); - phaseActionRepository.deleteAllByPhase_Member_Id(memberId); - phaseRepository.deleteAllByMember_Id(memberId); + phaseActionRepository.deleteAllByPhase_Roadmap_Member_Id(memberId); + phaseRepository.deleteAllByRoadmap_Member_Id(memberId); + roadmapRepository.deleteAllByMember_Id(memberId); jobPostingBookmarkRepository.deleteAllByMemberId(memberId); memberTermRepository.deleteAllByMemberId(memberId); memberVisaRepository.deleteAllByMemberId(memberId); diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java index e8235993..4ce56024 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java @@ -1,7 +1,6 @@ package org.sopt.kareer.domain.member.service; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest; import org.sopt.kareer.domain.member.dto.response.*; @@ -107,37 +106,6 @@ public MemberInfoResponse getMemberInfo(Long memberId) { ); } - // 프론트 온보딩 구현 완료 후 삭제 예정 - @Transactional - public void onboardMember(MemberOnboardRequest request, Long memberId) { - Member member = getById(memberId); - member.updateInfo( - request.name(), - request.birthDate(), - request.country(), - null, - null, - null, - null, - request.languageLevel(), - request.degree(), - request.expectedGraduationDate(), - request.primaryMajor(), - request.secondaryMajor(), - request.targetJob(), - request.targetJobSkill(), - request.personalBackground() - ); - - MemberVisa memberVisa = MemberVisa.createMemberVisa( - member, - request.visaType(), - request.visaExpiredAt(), - request.visaStartDate() - ); - memberVisaRepository.save(memberVisa); - } - @Transactional public void onboardMemberV2(MemberOnboardV2Request request, Long memberId) { Member member = getById(memberId); diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemApi.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemApi.java new file mode 100644 index 00000000..de10a0c7 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemApi.java @@ -0,0 +1,29 @@ +package org.sopt.kareer.domain.roadmap.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.roadmap.dto.response.ActionItemListResponse; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "Action Item API", description = "Action Item 관련 API") +public interface ActionItemApi { + + @Operation(summary = "액션 아이템 완료 상태 토글", description = "특정 액션 아이템의 완료 상태를 토글합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.TOGGLE_ACTION_ITEM_COMPLETION) + @PatchMapping("/{actionItemId}/completed") + ResponseEntity> toggleActionItemCompletion( + @AuthenticationPrincipal Long memberId, + @PathVariable Long actionItemId); + + @Operation(summary = "액션 아이템 전체 조회", description = "로그인한 회원의 활성화된 모든 액션 아이템을 조회합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.GET_ALL_ACTION_ITEMS) + @GetMapping + ResponseEntity> getAllActionItems(@AuthenticationPrincipal Long memberId); +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java index 5232496e..6b756551 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java @@ -1,45 +1,36 @@ package org.sopt.kareer.domain.roadmap.controller; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sopt.kareer.domain.roadmap.dto.response.ActionItemListResponse; import org.sopt.kareer.domain.roadmap.service.ActionItemService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; -import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Action Item API", description = "Action Item 관련 API") @RestController -@RequestMapping("/api/v1/action-items") @RequiredArgsConstructor -public class ActionItemController { +@RequestMapping("/api/v1/roadmap/action-items") +public class ActionItemController implements ActionItemApi { private final ActionItemService actionItemService; - @PatchMapping("/{actionItemId}/completed") - @Operation(summary = "액션 아이템 완료 상태 토글", description = "특정 액션 아이템의 완료 상태를 토글합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.TOGGLE_ACTION_ITEM_COMPLETION) - public ResponseEntity> toggleActionItemCompletion(@AuthenticationPrincipal Long memberId, - @PathVariable Long actionItemId) { + @Override + public ResponseEntity> toggleActionItemCompletion( + @AuthenticationPrincipal Long memberId, + @PathVariable Long actionItemId) { + actionItemService.toggleCompletion(memberId, actionItemId); return ResponseEntity.ok(BaseResponse.ok("액션 아이템 완료 상태가 토글되었습니다.")); } - @GetMapping("") - @Operation(summary = "액션 아이템 전체 조회", description = "로그인한 회원의 활성화된 모든 액션 아이템을 조회합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.GET_ALL_ACTION_ITEMS) + @Override public ResponseEntity> getAllActionItems( @AuthenticationPrincipal Long memberId) { + return ResponseEntity.ok( - BaseResponse.ok(actionItemService.getAllActionItems(memberId), "모든 액션 아이템이 조회되었습니다.") - ); + BaseResponse.ok(actionItemService.getAllActionItems(memberId), "모든 액션 아이템이 조회되었습니다.")); } } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionApi.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionApi.java new file mode 100644 index 00000000..c6773c8a --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionApi.java @@ -0,0 +1,33 @@ +package org.sopt.kareer.domain.roadmap.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.roadmap.dto.response.AiGuideResponse; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.CREATE_TODO; + +@Tag(name = "Phase Action API", description = "Phase Action 관련 API") +public interface PhaseActionApi { + + @Operation(summary = "Phase Action 기반 Todo 생성", description = "특정 Phase Action을 기반으로 Todo를 생성합니다.") + @CustomExceptionDescription(CREATE_TODO) + @PostMapping("/{phaseActionId}/todo") + ResponseEntity> createPhaseActionTodo( + @AuthenticationPrincipal Long memberId, + @PathVariable Long phaseActionId); + + @Operation(summary = "AI 가이드 조회", description = "AI 가이드를 조회합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.AI_GUIDE) + @GetMapping("/{phaseActionId}/guide") + ResponseEntity> getAiGuide( + @AuthenticationPrincipal Long memberId, + @PathVariable Long phaseActionId); +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java index af216e73..4fc864a9 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java @@ -1,52 +1,38 @@ package org.sopt.kareer.domain.roadmap.controller; -import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sopt.kareer.domain.roadmap.dto.response.AiGuideResponse; import org.sopt.kareer.domain.roadmap.service.PhaseActionService; -import org.sopt.kareer.domain.roadmap.service.PhaseService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; -import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Phase Action API", description = "Phase Action 관련 API") @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/phase-actions") -public class PhaseActionController { +@RequestMapping("/api/v1/roadmap/phase-actions") +public class PhaseActionController implements PhaseActionApi { private final PhaseActionService phaseActionService; - private final PhaseService phaseService; - @PostMapping("/{phaseActionId}/todo") - @Operation(summary = "Phase Action 기반 Todo 생성", description = "특정 Phase Action을 기반으로 Todo를 생성합니다.") - @CustomExceptionDescription(CREATE_TODO) + @Override public ResponseEntity> createPhaseActionTodo( @AuthenticationPrincipal Long memberId, - @PathVariable Long phaseActionId - ) { + @PathVariable Long phaseActionId) { + phaseActionService.createPhaseActionTodo(memberId, phaseActionId); return ResponseEntity.ok(BaseResponse.ok("Phase Action 기반 Todo가 생성되었습니다.")); } - @GetMapping("/{phaseActionId}/guide") - @Operation(summary = "AI 가이드 조회", description = "AI 가이드를 조회합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.AI_GUIDE) + @Override public ResponseEntity> getAiGuide( @AuthenticationPrincipal Long memberId, - @PathVariable Long phaseActionId - ) { - AiGuideResponse response = phaseActionService.getAiGuide(memberId, phaseActionId); + @PathVariable Long phaseActionId) { return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(response, "AI 가이드가 조회되었습니다.") - ); + .body(BaseResponse.ok(phaseActionService.getAiGuide(memberId, phaseActionId), "AI 가이드가 조회되었습니다.")); } } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseApi.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseApi.java new file mode 100644 index 00000000..3a421149 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseApi.java @@ -0,0 +1,36 @@ +package org.sopt.kareer.domain.roadmap.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.roadmap.dto.response.HomePhaseDetailResponse; +import org.sopt.kareer.domain.roadmap.dto.response.PhaseListResponse; +import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "Phase API", description = "Phase 관련 API") +public interface PhaseApi { + + @Operation(summary = "Phase 리스트 조회", description = "Phase 리스트를 조회합니다.") + @GetMapping + ResponseEntity> getPhaseList(@AuthenticationPrincipal Long memberId); + + @Operation(summary = "로드맵 Phase 상세정보 조회", description = "로드맵 Phase 상세조회를 조회합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.ROADMAP_PHASE_LIST_DETAIL) + @GetMapping("/{phaseId}") + ResponseEntity> getRoadmapPhaseDetail( + @AuthenticationPrincipal Long memberId, + @PathVariable Long phaseId); + + @Operation(summary = "홈 Phase 상세정보 조회", description = "홈 Phase 상세조회를 조회합니다.") + @CustomExceptionDescription(SwaggerResponseDescription.HOME_PHASE_LIST_DETAIL) + @GetMapping("/{phaseId}/home") + ResponseEntity> getHomePhaseDetail( + @AuthenticationPrincipal Long memberId, + @PathVariable Long phaseId); +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java index 8424a7ee..ca28ef3a 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java @@ -1,68 +1,48 @@ package org.sopt.kareer.domain.roadmap.controller; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sopt.kareer.domain.roadmap.dto.response.HomePhaseDetailResponse; import org.sopt.kareer.domain.roadmap.dto.response.PhaseListResponse; import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; import org.sopt.kareer.domain.roadmap.service.PhaseService; -import org.sopt.kareer.global.annotation.CustomExceptionDescription; -import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name="Phase API", description = "Phase 관련 API") @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/phases") -public class PhaseController { +@RequestMapping("/api/v1/roadmap/phases") +public class PhaseController implements PhaseApi { private final PhaseService phaseService; - @GetMapping - @Operation(summary = "Phase 리스트 조회", description = "Phase 리스트를 조회합니다.") + @Override public ResponseEntity> getPhaseList( - @AuthenticationPrincipal Long memberId - ) { - PhaseListResponse response = phaseService.getPhases(memberId); + @AuthenticationPrincipal Long memberId) { return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(response, "Phase 리스트가 조회되었습니다.") - ); + .body(BaseResponse.ok(phaseService.getPhases(memberId), "Phase 리스트가 조회되었습니다.")); } - @GetMapping("/{phaseId}/roadmap") - @Operation(summary = "로드맵 Phase 상세정보 조회", description = "로드맵 Phase 상세조회를 조회합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.ROADMAP_PHASE_LIST_DETAIL) + @Override public ResponseEntity> getRoadmapPhaseDetail( @AuthenticationPrincipal Long memberId, - @PathVariable Long phaseId - ) { - RoadmapPhaseDetailResponse response = phaseService.getRoadmapPhaseDetail(memberId, phaseId); + @PathVariable Long phaseId) { return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(response, "로드맵 Phase 상세정보가 조회되었습니다.") - ); + .body(BaseResponse.ok(phaseService.getRoadmapPhaseDetail(memberId, phaseId), "로드맵 Phase 상세정보가 조회되었습니다.")); } - @GetMapping("/{phaseId}/home") - @Operation(summary = "홈 Phase 상세정보 조회", description = "홈 Phase 상세조회를 조회합니다.") - @CustomExceptionDescription(SwaggerResponseDescription.HOME_PHASE_LIST_DETAIL) + @Override public ResponseEntity> getHomePhaseDetail( @AuthenticationPrincipal Long memberId, - @PathVariable Long phaseId - ) { - HomePhaseDetailResponse response = phaseService.getHomePhaseDetail(memberId, phaseId); + @PathVariable Long phaseId) { return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(response, "홈 Phase 상세정보가 조회되었습니다.") - ); + .body(BaseResponse.ok(phaseService.getHomePhaseDetail(memberId, phaseId), "홈 Phase 상세정보가 조회되었습니다.")); } } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java new file mode 100644 index 00000000..57e61558 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java @@ -0,0 +1,26 @@ +package org.sopt.kareer.domain.roadmap.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; +import org.sopt.kareer.global.annotation.CustomExceptionDescription; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; + +import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.CREATE_ROADMAP; + +@Tag(name = "Roadmap API", description = "Roadmap 관련 API") +public interface RoadmapApi { + + @Operation(summary = "AI 로드맵 생성", description = "사용자가 온보딩에 입력한 정보를 통해 로드맵을 생성합니다.") + @CustomExceptionDescription(CREATE_ROADMAP) + @PostMapping + ResponseEntity> generateRoadmap(@AuthenticationPrincipal Long memberId); + + @Operation(summary = "AI 로드맵 생성 테스트용 (Server Only)", description = "사용자가 온보딩에 입력한 정보를 통해 로드맵을 생성합니다.") + @CustomExceptionDescription(CREATE_ROADMAP) + @PostMapping("/test") + ResponseEntity> generateRoadmapForTest(@AuthenticationPrincipal Long memberId); +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java b/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java new file mode 100644 index 00000000..9a5893e6 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java @@ -0,0 +1,40 @@ +package org.sopt.kareer.domain.roadmap.controller; + +import lombok.RequiredArgsConstructor; +import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; +import org.sopt.kareer.domain.roadmap.service.RoadMapService; +import org.sopt.kareer.domain.roadmap.service.RoadmapTranslationService; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/roadmap") +public class RoadmapController implements RoadmapApi { + + private final RoadMapService roadMapService; + private final RoadmapTranslationService roadmapTranslationService; + + @Override + public ResponseEntity> generateRoadmap( + @AuthenticationPrincipal Long memberId) { + + var target = roadMapService.createRoadmap(memberId); + roadmapTranslationService.translateAllLanguages(target); + + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok("AI 로드맵 생성에 성공하였습니다.")); + } + + @Override + public ResponseEntity> generateRoadmapForTest( + @AuthenticationPrincipal Long memberId) { + + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok(roadMapService.createRoadmapTest(memberId), "AI 로드맵 생성에 성공하였습니다.")); + } +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java b/src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java index a6e7bdbb..ef0a539b 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java @@ -7,7 +7,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import org.sopt.kareer.global.entity.BaseEntity; @@ -25,8 +24,8 @@ public class Phase extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; + @JoinColumn(name = "roadmap_id", nullable = false) + private Roadmap roadmap; @Column(nullable = false) private int sequence; @@ -48,8 +47,8 @@ public class Phase extends BaseEntity { private LocalDate endDate; @Builder - private Phase(Member member, int sequence, String goal, String description, PhaseStatus status, LocalDate startDate, LocalDate endDate) { - this.member = member; + private Phase(Roadmap roadmap, int sequence, String goal, String description, PhaseStatus status, LocalDate startDate, LocalDate endDate) { + this.roadmap = roadmap; this.sequence = sequence; this.goal = goal; this.description = description; @@ -58,9 +57,9 @@ private Phase(Member member, int sequence, String goal, String description, Phas this.endDate = endDate; } - public static Phase create(Member member, int sequence, String goal, String description, PhaseStatus status, LocalDate startDate, LocalDate endDate) { + public static Phase create(Roadmap roadmap, int sequence, String goal, String description, PhaseStatus status, LocalDate startDate, LocalDate endDate) { return Phase.builder() - .member(member) + .roadmap(roadmap) .sequence(sequence) .goal(goal) .description(description) diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/entity/Roadmap.java b/src/main/java/org/sopt/kareer/domain/roadmap/entity/Roadmap.java new file mode 100644 index 00000000..e211a090 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/entity/Roadmap.java @@ -0,0 +1,44 @@ +package org.sopt.kareer.domain.roadmap.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.sopt.kareer.domain.member.entity.Member; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; +import org.sopt.kareer.global.entity.BaseEntity; + +@Entity +@Table(name = "roadmaps") +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Roadmap extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "roadmap_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RoadmapActiveStatus status; + + public static Roadmap create(Member member) { + return Roadmap.builder() + .member(member) + .status(RoadmapActiveStatus.ACTIVE) + .build(); + } + + public void deactivate() { + this.status = RoadmapActiveStatus.INACTIVE; + } +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/entity/enums/RoadmapActiveStatus.java b/src/main/java/org/sopt/kareer/domain/roadmap/entity/enums/RoadmapActiveStatus.java new file mode 100644 index 00000000..4c8293fd --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/entity/enums/RoadmapActiveStatus.java @@ -0,0 +1,5 @@ +package org.sopt.kareer.domain.roadmap.entity.enums; + +public enum RoadmapActiveStatus { + ACTIVE, INACTIVE +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java index fb2447de..b2e4687e 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java @@ -3,6 +3,8 @@ import org.sopt.kareer.domain.roadmap.entity.ActionItem; import org.sopt.kareer.domain.roadmap.entity.enums.ActionItemStatus; 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 java.util.List; @@ -21,4 +23,17 @@ public interface ActionItemRepository extends JpaRepository { Optional findByIdAndMemberId(@Param("actionItemId") Long actionItemId, @Param("memberId") Long memberId); void deleteAllByMemberId(Long memberId); + + @Modifying + @Query(""" + UPDATE ActionItem ai + SET ai.status = 'INACTIVE' + WHERE ai.phaseAction.id IN ( + SELECT pa.id FROM PhaseAction pa + WHERE pa.phase.id IN ( + SELECT p.id FROM Phase p WHERE p.roadmap.id = :roadmapId + ) + ) + """) + void deactivateAllByRoadmapId(@Param("roadmapId") Long roadmapId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepository.java index 2ec53386..cc69c6d4 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepository.java @@ -17,5 +17,5 @@ public interface PhaseActionGuidelineRepository extends JpaRepository findContentByPhaseActionId(@Param("phaseActionId") Long phaseActionId); - void deleteAllByPhaseAction_Phase_Member_Id(Long memberId); + void deleteAllByPhaseAction_Phase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java index 1cecab37..2a578d57 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java @@ -23,6 +23,6 @@ List findContentByPhaseActionIdAndLanguage(@Param("phaseActionId") Long void deleteAllByGuideline_PhaseAction_IdInAndLanguage(List phaseActionIds, String language); - void deleteAllByGuideline_PhaseAction_Phase_Member_Id(Long memberId); + void deleteAllByGuideline_PhaseAction_Phase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepository.java index 9c12ae1e..340acc79 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepository.java @@ -17,5 +17,5 @@ public interface PhaseActionMistakeRepository extends JpaRepository findContentByPhaseActionId(@Param("phaseActionId") Long phaseActionId); - void deleteAllByPhaseAction_Phase_Member_Id(Long memberId); + void deleteAllByPhaseAction_Phase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java index d6361f08..0290dbc5 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java @@ -23,6 +23,6 @@ List findContentByPhaseActionIdAndLanguage(@Param("phaseActionId") Long void deleteAllByMistake_PhaseAction_IdInAndLanguage(List phaseActionIds, String language); - void deleteAllByMistake_PhaseAction_Phase_Member_Id(Long memberId); + void deleteAllByMistake_PhaseAction_Phase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepository.java index b42063e0..5af37a71 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepository.java @@ -1,6 +1,7 @@ package org.sopt.kareer.domain.roadmap.repository; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -16,11 +17,16 @@ public interface PhaseActionRepository extends JpaRepository SELECT pa FROM PhaseAction pa JOIN FETCH pa.phase p - JOIN FETCH p.member m + JOIN FETCH p.roadmap r + JOIN FETCH r.member m WHERE pa.id = :phaseActionId AND m.id = :memberId + AND r.status = :status """) - Optional findByIdAndMemberId(@Param("phaseActionId") Long phaseActionId, @Param("memberId") Long memberId); + Optional findByIdAndMemberIdAndRoadmapStatus( + @Param("phaseActionId") Long phaseActionId, + @Param("memberId") Long memberId, + @Param("status") RoadmapActiveStatus status); - void deleteAllByPhase_Member_Id(Long memberId); + void deleteAllByPhase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java index 6f8861aa..1361c0d1 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java @@ -14,5 +14,5 @@ public interface PhaseActionTranslationRepository extends JpaRepository phaseActionIds, String language); - void deleteAllByPhaseAction_Phase_Member_Id(Long memberId); + void deleteAllByPhaseAction_Phase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java index cf4e32d9..9c28d4c9 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java @@ -1,11 +1,12 @@ package org.sopt.kareer.domain.roadmap.repository; import org.sopt.kareer.domain.roadmap.entity.Phase; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.springframework.data.jpa.repository.JpaRepository; public interface PhaseRepository extends JpaRepository, PhaseRepositoryCustom { - boolean existsByIdAndMember_Id(Long phaseId, Long memberId); + boolean existsByIdAndRoadmap_Member_IdAndRoadmap_Status(Long phaseId, Long memberId, RoadmapActiveStatus status); - void deleteAllByMember_Id(Long memberId); + void deleteAllByRoadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryCustomImpl.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryCustomImpl.java index bd249a0c..b965038a 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryCustomImpl.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryCustomImpl.java @@ -18,6 +18,7 @@ import org.sopt.kareer.domain.roadmap.entity.QPhase; import org.sopt.kareer.domain.roadmap.entity.QPhaseAction; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.springframework.stereotype.Repository; @Repository @@ -51,7 +52,8 @@ public List findPhases(Long memberId) { phase.endDate )) .from(phase) - .where(phase.member.id.eq(memberId)) + .where(phase.roadmap.member.id.eq(memberId) + .and(phase.roadmap.status.eq(RoadmapActiveStatus.ACTIVE))) .orderBy(phase.sequence.asc()) .fetch(); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java index ac2b64e5..e1bea62d 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java @@ -11,5 +11,5 @@ public interface PhaseTranslationRepository extends JpaRepository phaseIds, String language); - void deleteAllByPhase_Member_Id(Long memberId); + void deleteAllByPhase_Roadmap_Member_Id(Long memberId); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java b/src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java new file mode 100644 index 00000000..ba3ccc20 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java @@ -0,0 +1,17 @@ +package org.sopt.kareer.domain.roadmap.repository; + +import org.sopt.kareer.domain.roadmap.entity.Roadmap; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface RoadmapRepository extends JpaRepository { + + Optional findByMember_IdAndStatus(Long memberId, RoadmapActiveStatus status); + + List findAllByMember_Id(Long memberId); + + void deleteAllByMember_Id(Long memberId); +} diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java b/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java index 9199ea3d..99f694e9 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java @@ -6,6 +6,7 @@ import org.sopt.kareer.domain.roadmap.entity.ActionItem; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; import org.sopt.kareer.domain.roadmap.entity.PhaseActionTranslation; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.sopt.kareer.domain.roadmap.exception.RoadMapException; import org.sopt.kareer.domain.roadmap.exception.RoadmapErrorCode; import org.sopt.kareer.domain.roadmap.repository.ActionItemRepository; @@ -34,7 +35,7 @@ public class PhaseActionService { @Transactional public void createPhaseActionTodo(Long memberId, Long phaseActionId) { - PhaseAction phaseAction = phaseActionRepository.findByIdAndMemberId(phaseActionId, memberId) + PhaseAction phaseAction = phaseActionRepository.findByIdAndMemberIdAndRoadmapStatus(phaseActionId, memberId, RoadmapActiveStatus.ACTIVE) .orElseThrow(() -> new RoadMapException(RoadmapErrorCode.PHASE_ACTION_NOT_FOUND)); if (phaseAction.getAdded()) { @@ -51,7 +52,7 @@ public void createPhaseActionTodo(Long memberId, Long phaseActionId) { } public AiGuideResponse getAiGuide(Long memberId, Long phaseActionId) { - PhaseAction phaseAction = phaseActionRepository.findByIdAndMemberId(phaseActionId, memberId) + PhaseAction phaseAction = phaseActionRepository.findByIdAndMemberIdAndRoadmapStatus(phaseActionId, memberId, RoadmapActiveStatus.ACTIVE) .orElseThrow(() -> new RoadMapException(RoadmapErrorCode.PHASE_ACTION_NOT_FOUND)); String importance = phaseAction.getImportance(); diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java b/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java index 433bf631..22351f58 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java @@ -1,17 +1,18 @@ package org.sopt.kareer.domain.roadmap.service; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.roadmap.dto.response.*; -import org.sopt.kareer.domain.roadmap.dto.response.PhaseResponse; +import org.sopt.kareer.domain.roadmap.dto.response.HomePhaseDetailResponse; import org.sopt.kareer.domain.roadmap.dto.response.PhaseListResponse; +import org.sopt.kareer.domain.roadmap.dto.response.PhaseResponse; +import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; import org.sopt.kareer.domain.roadmap.entity.PhaseActionTranslation; import org.sopt.kareer.domain.roadmap.entity.PhaseTranslation; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.sopt.kareer.domain.roadmap.exception.RoadMapException; import org.sopt.kareer.domain.roadmap.exception.RoadmapErrorCode; import org.sopt.kareer.domain.roadmap.repository.PhaseActionRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseActionTranslationRepository; -import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; import org.sopt.kareer.domain.roadmap.repository.PhaseRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseTranslationRepository; import org.springframework.context.i18n.LocaleContextHolder; @@ -60,7 +61,7 @@ public PhaseListResponse getPhases(Long memberId) { } public RoadmapPhaseDetailResponse getRoadmapPhaseDetail(Long memberId, Long phaseId) { - if (!phaseRepository.existsByIdAndMember_Id(phaseId, memberId)) { + if (!phaseRepository.existsByIdAndRoadmap_Member_IdAndRoadmap_Status(phaseId, memberId, RoadmapActiveStatus.ACTIVE)) { throw new RoadMapException(RoadmapErrorCode.PHASE_NOT_FOUND); } Map> raw = @@ -117,7 +118,7 @@ private RoadmapPhaseDetailResponse.ActionGroupResponse wrap( } public HomePhaseDetailResponse getHomePhaseDetail(Long memberId, Long phaseId) { - if (!phaseRepository.existsByIdAndMember_Id(phaseId, memberId)) { + if (!phaseRepository.existsByIdAndRoadmap_Member_IdAndRoadmap_Status(phaseId, memberId, RoadmapActiveStatus.ACTIVE)) { throw new RoadMapException(RoadmapErrorCode.PHASE_NOT_FOUND); } diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java b/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java index 83045ca9..d8af58fb 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java @@ -5,6 +5,7 @@ import org.sopt.kareer.domain.roadmap.dto.response.RoadmapResponse; import org.sopt.kareer.domain.roadmap.dto.translation.RoadmapTranslationTarget; import org.sopt.kareer.domain.roadmap.entity.*; +import org.sopt.kareer.domain.roadmap.repository.RoadmapRepository; import org.sopt.kareer.domain.roadmap.entity.enums.ActionItemType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; @@ -31,14 +32,17 @@ public class RoadMapPersistService { private final PhaseActionGuidelineRepository phaseActionGuidelineRepository; private final PhaseActionMistakeRepository phaseActionMistakeRepository; private final ActionItemRepository actionItemRepository; + private final RoadmapRepository roadmapRepository; @Transactional public RoadmapTranslationTarget saveRoadMap(Member member, RoadmapResponse response) { + Roadmap roadmap = roadmapRepository.save(Roadmap.create(member)); + List phaseTargets = new ArrayList<>(); for (RoadmapResponse.PhasePlan phasePlan : Optional.ofNullable(response.phases()).orElse(Collections.emptyList())) { Phase savedPhase = phaseRepository.save(Phase.create( - member, + roadmap, phasePlan.sequence(), phasePlan.goal(), phasePlan.description(), diff --git a/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java b/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java index f25fa41a..e1bf5562 100644 --- a/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java +++ b/src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java @@ -11,6 +11,9 @@ import org.sopt.kareer.domain.roadmap.dto.response.RoadmapResponse; import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; import org.sopt.kareer.domain.roadmap.dto.translation.RoadmapTranslationTarget; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; +import org.sopt.kareer.domain.roadmap.repository.ActionItemRepository; +import org.sopt.kareer.domain.roadmap.repository.RoadmapRepository; import org.sopt.kareer.global.external.ai.builder.context.MemberContextBuilder; import org.sopt.kareer.global.external.ai.service.OpenAiService; import org.sopt.kareer.global.external.ai.service.PolicyDocumentRetriever; @@ -34,12 +37,20 @@ public class RoadMapService { private final RequiredDocumentRetriever requiredRetriever; private final PolicyDocumentRetriever policyDocumentRetriever; private final MemberVisaRepository memberVisaRepository; + private final RoadmapRepository roadmapRepository; + private final ActionItemRepository actionItemRepository; @Transactional public RoadmapTranslationTarget createRoadmap(Long memberId){ Member member = memberService.getById(memberId); + roadmapRepository.findByMember_IdAndStatus(memberId, RoadmapActiveStatus.ACTIVE) + .ifPresent(existing -> { + actionItemRepository.deactivateAllByRoadmapId(existing.getId()); + existing.deactivate(); + }); + var memberContext = memberContextBuilder.load(memberId); MemberVisa visa = memberVisaRepository.findActiveByMemberId(memberId).orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND)); @@ -65,11 +76,7 @@ public RoadmapTranslationTarget createRoadmap(Long memberId){ policyDocs ); - RoadmapTranslationTarget target = roadMapPersistService.saveRoadMap(member, response); - - member.markRoadmapDone(); - - return target; + return roadMapPersistService.saveRoadMap(member, response); } @Transactional @@ -109,9 +116,4 @@ public RoadmapTestResponse createRoadmapTest(Long memberId) { return RoadmapTestResponse.of(roadmap, retrieved); } - @Transactional - public void markFailed(Long memberId) { - Member member = memberService.getById(memberId); - member.markRoadmapFailed(); - } } diff --git a/src/main/java/org/sopt/kareer/domain/term/controller/TermApi.java b/src/main/java/org/sopt/kareer/domain/term/controller/TermApi.java new file mode 100644 index 00000000..60fdebd6 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/term/controller/TermApi.java @@ -0,0 +1,16 @@ +package org.sopt.kareer.domain.term.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.kareer.domain.term.dto.response.TermsResponse; +import org.sopt.kareer.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; + +@Tag(name = "Term API") +public interface TermApi { + + @GetMapping + @Operation(summary = "약관 조회", description = "약관 내용을 조회합니다.") + ResponseEntity> getTerms(); +} diff --git a/src/main/java/org/sopt/kareer/domain/term/controller/TermController.java b/src/main/java/org/sopt/kareer/domain/term/controller/TermController.java index 91d534f1..c6d45f61 100644 --- a/src/main/java/org/sopt/kareer/domain/term/controller/TermController.java +++ b/src/main/java/org/sopt/kareer/domain/term/controller/TermController.java @@ -1,30 +1,24 @@ package org.sopt.kareer.domain.term.controller; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sopt.kareer.domain.term.dto.response.TermsResponse; import org.sopt.kareer.domain.term.service.TermService; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/terms") -@Tag(name = "Term API") -public class TermController { +public class TermController implements TermApi { private final TermService termService; - @GetMapping - @Operation(summary = "약관 조회", description = "약관 내용을 조회합니다.") + @Override public ResponseEntity> getTerms() { - return ResponseEntity - .status(HttpStatus.OK) + return ResponseEntity.status(HttpStatus.OK) .body(BaseResponse.ok(termService.getTerms(), "약관 조회에 성공했습니다.")); } } diff --git a/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java b/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java index 60b0c80c..652a96e1 100644 --- a/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java @@ -2,17 +2,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.response.MemberInfoResponse; import org.sopt.kareer.domain.member.dto.response.MemberStatusResponse; import org.sopt.kareer.domain.member.dto.response.MypageResponse; import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.member.entity.MemberVisa; -import org.sopt.kareer.domain.member.entity.enums.Country; import org.sopt.kareer.domain.member.entity.enums.LanguageLevel; import org.sopt.kareer.domain.member.entity.enums.VisaType; import org.sopt.kareer.domain.member.fixture.MemberFixture; -import org.sopt.kareer.domain.member.fixture.MemberOnboardRequestFixture; import org.sopt.kareer.domain.member.fixture.MemberVisaFixture; import org.sopt.kareer.support.ControllerTestSupport; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -22,11 +19,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -46,14 +40,16 @@ void getMemberInfo() throws Exception { "test-user@example.com", "profile-image-url", LocalDate.of(2000, 4, 1), - Country.UNITED_STATES, + "United States", "Computer Science", "Math", "Backend", + "Seoul National University", LocalDate.of(2023, 2, 1), LocalDate.of(2024, 2, 1), LanguageLevel.LEVEL_4, - "Bachelor", + "outside-korea-bachelor", + "advanced", "Java", VisaType.D2 ); @@ -67,28 +63,12 @@ void getMemberInfo() throws Exception { .andExpect(jsonPath("$.data.memberId").value(response.memberId())); } - @DisplayName("회원 온보딩 정보를 저장한다.") - @Test - void onboardMember() throws Exception { - MemberOnboardRequest request = MemberOnboardRequestFixture.create(); - willDoNothing().given(memberService).onboardMember(any(), any()); - - mockMvc.perform(post("/api/v1/members/onboard") - .with(authentication(authenticatedMember())) - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("회원 온보딩이 완료되었습니다.")); - } - @DisplayName("온보딩 국가 목록을 조회한다.") @Test void getOnboardCountries() throws Exception { mockMvc.perform(get("/api/v1/members/onboard/countries")) .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.countries").isArray()); + .andExpect(status().isOk()); } @DisplayName("온보딩 전공 목록을 조회한다.") @@ -96,11 +76,9 @@ void getOnboardCountries() throws Exception { void getOnboardMajors() throws Exception { mockMvc.perform(get("/api/v1/members/onboard/majors")) .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.majors").isArray()); + .andExpect(status().isOk()); } - @DisplayName("회원 상태 정보를 조회한다.") @Test void getMemberStatus() throws Exception { @@ -122,20 +100,25 @@ void getMemberStatus() throws Exception { @DisplayName("마이페이지를 조회한다.") @Test void getMyPage() throws Exception { - //given + //given Member member = MemberFixture.getMember(); MemberVisa memberVisa = MemberVisaFixture.activeD2(member); - MypageResponse response = MypageResponse.from(member, memberVisa); - + MypageResponse response = MypageResponse.of( + member, memberVisa, + member.getCountryCode(), + member.getPrimaryMajorCode(), + member.getUniversityCode(), + member.getDegreeCode(), + member.getEnglishLevelCode() + ); given(memberService.getMypage(any())).willReturn(response); - //when && then + //when && then mockMvc.perform(get("/api/v1/members/mypage") - .with(authentication(authenticatedMember()))) + .with(authentication(authenticatedMember()))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.name").value(member.getName())); - } } diff --git a/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java b/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java new file mode 100644 index 00000000..2a5f9731 --- /dev/null +++ b/src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java @@ -0,0 +1,39 @@ +package org.sopt.kareer.domain.member.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.sopt.kareer.domain.member.fixture.MemberOnboardRequestFixture; +import org.sopt.kareer.support.ControllerTestSupport; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class MemberControllerV2Test extends ControllerTestSupport { + + private UsernamePasswordAuthenticationToken authenticatedMember() { + return new UsernamePasswordAuthenticationToken(1L, null, List.of()); + } + + @DisplayName("회원 온보딩 V2 정보를 저장한다.") + @Test + void onboardMember() throws Exception { + willDoNothing().given(memberService).onboardMemberV2(any(), any()); + + mockMvc.perform(post("/api/v2/members/onboard") + .with(authentication(authenticatedMember())) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(MemberOnboardRequestFixture.create()))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("회원 온보딩이 완료되었습니다.")); + } +} diff --git a/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java b/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java index f0b8b982..ba8f5fcb 100644 --- a/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java +++ b/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java @@ -38,6 +38,6 @@ void updateProfile(){ assertThat(member.getPrimaryMajorCode()).isEqualTo("computer-science"); assertThat(member.getSecondaryMajor()).isEqualTo("Statistic"); assertThat(member.getLanguageLevel()).isEqualTo(LanguageLevel.LEVEL_3); - assertThat(member.getUniversityCode()).isEqualTo("beginner"); + assertThat(member.getEnglishLevelCode()).isEqualTo("beginner"); } } diff --git a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberFixture.java b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberFixture.java index f18ef4e3..ad7b0e93 100644 --- a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberFixture.java +++ b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberFixture.java @@ -10,44 +10,41 @@ public class MemberFixture { public static final OAuthProvider MEMBER_OAUTH_PROVIDER = OAuthProvider.GOOGLE; public static final String MEMBER_PROVIDER_ID = "test-provider-id"; public static final String MEMBER_EMAIL = "member@example.com"; - public static final Country MEMBER_COUNTRY = Country.AFGHANISTAN; - public static final Degree MEMBER_DEGREE = Degree.DOMESTIC_ASSOCIATE; - public static final String MEMBER_UNIVERSITY = "University"; - public static final EnglishLevel MEMBER_ENGLISH_LEVEL = EnglishLevel.ADVANCED; - + public static final String MEMBER_COUNTRY_CODE = "afghanistan"; + public static final String MEMBER_DEGREE_CODE = "south-korea-associate"; + public static final String MEMBER_UNIVERSITY_CODE = "konkuk-university"; + public static final String MEMBER_ENGLISH_LEVEL_CODE = "advanced"; public static Member getMember() { return getMember(MEMBER_PROVIDER_ID); } - public static Member getMember(String providerId){ + public static Member getMember(String providerId) { return Member.builder() .name(MEMBER_NAME) .email(MEMBER_EMAIL) - .country(MEMBER_COUNTRY) + .countryCode(MEMBER_COUNTRY_CODE) .provider(MEMBER_OAUTH_PROVIDER) .providerId(providerId) .status(MemberStatus.ACTIVE) - .roadmapStatus(RoadmapStatus.NOT_STARTED) - .degree(MEMBER_DEGREE) - .englishLevel(MEMBER_ENGLISH_LEVEL) - .university(MEMBER_UNIVERSITY) + .degreeCode(MEMBER_DEGREE_CODE) + .englishLevelCode(MEMBER_ENGLISH_LEVEL_CODE) + .universityCode(MEMBER_UNIVERSITY_CODE) .build(); } - public static Member getMember(LocalDate birthDate){ + public static Member getMember(LocalDate birthDate) { return Member.builder() .name(MEMBER_NAME) .email(MEMBER_EMAIL) - .country(MEMBER_COUNTRY) + .countryCode(MEMBER_COUNTRY_CODE) .provider(MEMBER_OAUTH_PROVIDER) .providerId(MEMBER_PROVIDER_ID) .status(MemberStatus.ACTIVE) - .roadmapStatus(RoadmapStatus.NOT_STARTED) - .degree(MEMBER_DEGREE) - .englishLevel(MEMBER_ENGLISH_LEVEL) - .university(MEMBER_UNIVERSITY) + .degreeCode(MEMBER_DEGREE_CODE) + .englishLevelCode(MEMBER_ENGLISH_LEVEL_CODE) + .universityCode(MEMBER_UNIVERSITY_CODE) .birthDate(birthDate) .build(); } diff --git a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberOnboardRequestFixture.java b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberOnboardRequestFixture.java index cd276bb2..dadd5c36 100644 --- a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberOnboardRequestFixture.java +++ b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberOnboardRequestFixture.java @@ -1,28 +1,33 @@ package org.sopt.kareer.domain.member.fixture; -import java.time.LocalDate; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; -import org.sopt.kareer.domain.member.entity.enums.Country; +import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; import org.sopt.kareer.domain.member.entity.enums.Degree; +import org.sopt.kareer.domain.member.entity.enums.EnglishLevel; import org.sopt.kareer.domain.member.entity.enums.LanguageLevel; import org.sopt.kareer.domain.member.entity.enums.VisaType; +import java.time.LocalDate; +import java.util.List; + public class MemberOnboardRequestFixture { - public static MemberOnboardRequest create() { - return new MemberOnboardRequest( + public static MemberOnboardV2Request create() { + return new MemberOnboardV2Request( "test-user", LocalDate.of(2000, 4, 1), - Country.UNITED_STATES, + "konkuk-university", + "united-states", LanguageLevel.LEVEL_4, + EnglishLevel.ADVANCED, Degree.OVERSEAS_BACHELORS, - VisaType.D10, - null, - LocalDate.of(2024, 5, 1), + VisaType.D2, + LocalDate.of(2026, 2, 1), + LocalDate.of(2022, 3, 1), LocalDate.of(2026, 5, 1), - 70, - "Computer Science", - "Applied Mathematics", + "computer-science", + "applied-mathematics", + List.of("it-development", "data-science"), + List.of(), "Backend Engineer", "Java, Spring", "해외 경험을 쌓고 싶어요" diff --git a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java index 027d7e60..c42b1a8f 100644 --- a/src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java +++ b/src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java @@ -14,7 +14,6 @@ public static MemberVisa activeD2(Member member) { .visaType(VisaType.D2) .visaStatus(VisaStatus.ACTIVE) .visaExpiredAt(LocalDate.now().plusYears(1)) - .visaPoint(null) .visaStartDate(LocalDate.now().minusMonths(3)) .build(); } @@ -25,7 +24,6 @@ public static MemberVisa inactiveD2(Member member) { .visaType(VisaType.D2) .visaStatus(VisaStatus.INACTIVE) .visaExpiredAt(LocalDate.now().minusMonths(1)) - .visaPoint(null) .visaStartDate(LocalDate.now().minusYears(1)) .build(); } @@ -36,7 +34,6 @@ public static MemberVisa activeD10(Member member) { .visaType(VisaType.D10) .visaStatus(VisaStatus.ACTIVE) .visaExpiredAt(LocalDate.now().plusMonths(18)) - .visaPoint(70) .visaStartDate(LocalDate.now()) .build(); } diff --git a/src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java b/src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java index 1fabed47..2f8491e2 100644 --- a/src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java +++ b/src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.sopt.kareer.domain.member.entity.Member; -import org.sopt.kareer.domain.member.entity.enums.RoadmapStatus; import org.sopt.kareer.domain.member.fixture.MemberFixture; import org.sopt.kareer.global.config.QuerydslConfig; import org.springframework.beans.factory.annotation.Autowired; @@ -35,30 +34,4 @@ void findByProviderAndProviderId() { assertThat(found).isPresent(); assertThat(found.get().getId()).isEqualTo(member.getId()); } - - @DisplayName("로드맵 상태가 NOT_STARTED면 IN_PROGRESS로 전환된다.") - @Test - void tryMarkRoadmapInProgress() { - Member member = memberRepository.save(MemberFixture.getMember("provider-2")); - - int updatedCount = memberRepository.tryMarkRoadmapInProgress(member.getId()); - - assertThat(updatedCount).isEqualTo(1); - Member updatedMember = memberRepository.findById(member.getId()).orElseThrow(); - assertThat(updatedMember.getRoadmapStatus()).isEqualTo(RoadmapStatus.IN_PROGRESS); - } - - @DisplayName("이미 로드맵이 완료된 회원은 상태가 변경되지 않는다.") - @Test - void tryMarkRoadmapInProgressWithDoneStatus() { - Member member = memberRepository.save(MemberFixture.getMember("provider-3")); - member.markRoadmapDone(); - memberRepository.save(member); - - int updatedCount = memberRepository.tryMarkRoadmapInProgress(member.getId()); - - assertThat(updatedCount).isZero(); - Member foundMember = memberRepository.findById(member.getId()).orElseThrow(); - assertThat(foundMember.getRoadmapStatus()).isEqualTo(RoadmapStatus.DONE); - } } diff --git a/src/test/java/org/sopt/kareer/domain/member/service/MemberServiceTest.java b/src/test/java/org/sopt/kareer/domain/member/service/MemberServiceTest.java index 8d7445d4..7410072c 100644 --- a/src/test/java/org/sopt/kareer/domain/member/service/MemberServiceTest.java +++ b/src/test/java/org/sopt/kareer/domain/member/service/MemberServiceTest.java @@ -3,13 +3,15 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; +import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; import org.sopt.kareer.domain.member.dto.response.MemberInfoResponse; import org.sopt.kareer.domain.member.dto.response.MemberStatusResponse; import org.sopt.kareer.domain.member.dto.response.MypageResponse; import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.member.entity.MemberVisa; -import org.sopt.kareer.domain.member.entity.enums.*; +import org.sopt.kareer.domain.member.entity.enums.LanguageLevel; +import org.sopt.kareer.domain.member.entity.enums.MemberStatus; +import org.sopt.kareer.domain.member.entity.enums.OAuthProvider; import org.sopt.kareer.domain.member.exception.MemberErrorCode; import org.sopt.kareer.domain.member.exception.MemberException; import org.sopt.kareer.domain.member.fixture.MemberFixture; @@ -74,7 +76,7 @@ void findOrCreateByOAuthReturnsExistingMember() { @DisplayName("온보딩 요청을 저장하면 회원 정보와 비자 정보가 갱신된다.") @Test - void onboardMember() { + void onboardMemberV2() { Member member = memberRepository.save(Member.createOAuthMember( "test-user", OAuthProvider.GOOGLE, @@ -82,15 +84,15 @@ void onboardMember() { "test_image_url", "test-user@example.com" )); - MemberOnboardRequest request = MemberOnboardRequestFixture.create(); + MemberOnboardV2Request request = MemberOnboardRequestFixture.create(); - memberService.onboardMember(request, member.getId()); + memberService.onboardMemberV2(request, member.getId()); Member updated = memberRepository.findById(member.getId()).orElseThrow(); assertThat(updated.getStatus()).isEqualTo(MemberStatus.ACTIVE); assertThat(updated.getName()).isEqualTo(request.name()); - assertThat(updated.getCountry()).isEqualTo(request.country()); - assertThat(updated.getPrimaryMajor()).isEqualTo(request.primaryMajor()); + assertThat(updated.getCountryCode()).isEqualTo(request.countryCode()); + assertThat(updated.getPrimaryMajorCode()).isEqualTo(request.primaryMajorCode()); assertThat(updated.getTargetJobSkill()).isEqualTo(request.targetJobSkill()); assertThat(updated.getExpectedGraduationDate()).isEqualTo(request.expectedGraduationDate()); @@ -98,7 +100,6 @@ void onboardMember() { assertThat(visas).hasSize(1); MemberVisa savedVisa = visas.getFirst(); assertThat(savedVisa.getVisaType()).isEqualTo(request.visaType()); - assertThat(savedVisa.getVisaPoint()).isEqualTo(request.visaPoint()); } @DisplayName("회원 정보 조회 시 멤버와 비자 정보가 함께 반환된다.") @@ -110,15 +111,16 @@ void getMemberInfo() { .status(MemberStatus.ACTIVE) .provider(OAuthProvider.GOOGLE) .providerId(UUID.randomUUID().toString()) - .country(Country.AUSTRALIA) - .primaryMajor("Computer Science") + .countryCode("australia") + .primaryMajorCode("computer-science") .secondaryMajor("Mathematics") .targetJob("Backend Engineer") .graduationDate(LocalDate.of(2023, 2, 1)) .languageLevel(LanguageLevel.LEVEL_3) - .degree(Degree.OVERSEAS_BACHELORS) + .degreeCode("outside-korea-bachelor") + .englishLevelCode("advanced") + .universityCode("some-university") .targetJobSkill("Java") - .roadmapStatus(RoadmapStatus.NOT_STARTED) .build()); MemberVisa activeVisa = memberVisaRepository.save(MemberVisaFixture.activeD10(member)); @@ -126,8 +128,8 @@ void getMemberInfo() { assertThat(response.memberId()).isEqualTo(member.getId()); assertThat(response.name()).isEqualTo(member.getName()); - assertThat(response.country()).isEqualTo(member.getCountry()); - assertThat(response.degree()).isEqualTo(member.getDegree().getDescription()); + assertThat(response.country()).isEqualTo(member.getCountryCode()); + assertThat(response.degree()).isEqualTo(member.getDegreeCode()); assertThat(response.visaType()).isEqualTo(activeVisa.getVisaType()); } @@ -150,9 +152,8 @@ void getMemberStatus() { .status(MemberStatus.ACTIVE) .provider(OAuthProvider.GOOGLE) .providerId(UUID.randomUUID().toString()) - .country(Country.AUSTRALIA) - .roadmapStatus(RoadmapStatus.NOT_STARTED) - .degree(Degree.OVERSEAS_BACHELORS) + .countryCode("australia") + .degreeCode("outside-korea-bachelor") .languageLevel(LanguageLevel.LEVEL_5) .build()); MemberVisa activeVisa = memberVisaRepository.save(MemberVisaFixture.activeD2(member)); @@ -175,41 +176,39 @@ void getMemberStatusWithoutVisa() { @DisplayName("마이페이지에서 유저 정보를 조회한다.") @Test - void getMyPage(){ - //given + void getMyPage() { + //given Member member = memberRepository.save(MemberFixture.getMember()); - MemberVisa memberVisa = memberVisaRepository.save(MemberVisaFixture.activeD2(member)); - //when + memberVisaRepository.save(MemberVisaFixture.activeD2(member)); + + //when MypageResponse response = memberService.getMypage(member.getId()); //then assertThat(response.name()).isEqualTo(member.getName()); - assertThat(response.country()).isEqualTo(member.getCountry().getCountryName()); + assertThat(response.country()).isEqualTo(member.getCountryCode()); } @DisplayName("존재하지 않는 회원의 마이페이지 조회 시 예외가 발생한다.") @Test - void getMyPageWithoutMember(){ - + void getMyPageWithoutMember() { assertThatThrownBy(() -> memberService.getMypage(1L)) .isInstanceOf(MemberException.class) .hasMessage(MemberErrorCode.MEMBER_NOT_FOUND.getMessage()); - } @DisplayName("비자정보가 존재하지 않는 경우 마이페이지 조회 시 예외가 발생한다.") @Test - void getMypageWithoutMemberVisa(){ - //given + void getMypageWithoutMemberVisa() { + //given Member member = memberRepository.save(MemberFixture.getMember()); - //when && then + //when && then assertThatThrownBy(() -> memberService.getMypage(member.getId())) .isInstanceOf(MemberException.class) .hasMessage(MemberErrorCode.VISA_NOT_FOUND.getMessage()); } - private OAuthAttributes createOAuthAttributes() { String providerId = UUID.randomUUID().toString(); return new OAuthAttributes( @@ -222,6 +221,4 @@ private OAuthAttributes createOAuthAttributes() { Map.of() ); } - - } diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java index 0bf29c93..36a25825 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java @@ -40,7 +40,7 @@ void getAiGuide_success() throws Exception { .willReturn(response); // when & then - mockMvc.perform(get("/api/v1/phase-actions/{phaseActionId}/guide", phaseActionId)) + mockMvc.perform(get("/api/v1/roadmap/phase-actions/{phaseActionId}/guide", phaseActionId)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("AI 가이드가 조회되었습니다.")) @@ -62,7 +62,7 @@ void getAiGuide_notFoundPhaseAction() throws Exception { .willThrow(new RoadMapException(RoadmapErrorCode.PHASE_ACTION_NOT_FOUND)); // when & then - mockMvc.perform(get("/api/v1/phase-actions/{phaseActionId}/guide", phaseActionId)) + mockMvc.perform(get("/api/v1/roadmap/phase-actions/{phaseActionId}/guide", phaseActionId)) .andDo(print()) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value(RoadmapErrorCode.PHASE_ACTION_NOT_FOUND.getMessage())); diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseControllerTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseControllerTest.java index 1f78d2ee..555010da 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseControllerTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseControllerTest.java @@ -45,7 +45,7 @@ void getPhaseList_success() throws Exception { ); // when & then - mockMvc.perform(get("/api/v1/phases")) + mockMvc.perform(get("/api/v1/roadmap/phases")) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Phase 리스트가 조회되었습니다.")) @@ -89,7 +89,7 @@ void getRoadmapPhaseDetail_success() throws Exception { .willReturn(response); // when & then - mockMvc.perform(get("/api/v1/phases/{phaseId}/roadmap", phaseId)) + mockMvc.perform(get("/api/v1/roadmap/phases/{phaseId}", phaseId)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("로드맵 Phase 상세정보가 조회되었습니다.")) @@ -108,7 +108,7 @@ void getRoadmapPhaseDetail_notFoundPhase() throws Exception { .willThrow(new RoadMapException(RoadmapErrorCode.PHASE_NOT_FOUND)); // when & then - mockMvc.perform(get("/api/v1/phases/{phaseId}/roadmap", phaseId)) + mockMvc.perform(get("/api/v1/roadmap/phases/{phaseId}", phaseId)) .andDo(print()) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value(RoadmapErrorCode.PHASE_NOT_FOUND.getMessage())); @@ -138,7 +138,7 @@ void getHomePhaseDetail_success() throws Exception { .willReturn(response); // when & then - mockMvc.perform(get("/api/v1/phases/{phaseId}/home", phaseId)) + mockMvc.perform(get("/api/v1/roadmap/phases/{phaseId}/home", phaseId)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("홈 Phase 상세정보가 조회되었습니다.")) @@ -158,7 +158,7 @@ void getHomePhaseDetail_notFoundPhase() throws Exception { .willThrow(new RoadMapException(RoadmapErrorCode.PHASE_NOT_FOUND)); // when & then - mockMvc.perform(get("/api/v1/phases/{phaseId}/home", phaseId)) + mockMvc.perform(get("/api/v1/roadmap/phases/{phaseId}/home", phaseId)) .andDo(print()) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value(RoadmapErrorCode.PHASE_NOT_FOUND.getMessage())); diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/fixture/PhaseFixture.java b/src/test/java/org/sopt/kareer/domain/roadmap/fixture/PhaseFixture.java index b9fc4a10..24eb415b 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/fixture/PhaseFixture.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/fixture/PhaseFixture.java @@ -1,7 +1,7 @@ package org.sopt.kareer.domain.roadmap.fixture; -import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.roadmap.entity.Phase; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import java.time.LocalDate; @@ -13,7 +13,7 @@ public class PhaseFixture { public static final LocalDate START_DATE = LocalDate.of(2026, 6, 1); public static final LocalDate END_DATE = LocalDate.of(2026, 8, 31); - public static Phase getPhase(Member member, int sequence, PhaseStatus phaseStatus) { - return Phase.create(member, sequence, GOAL, DESCRIPTION, phaseStatus, START_DATE, END_DATE); + public static Phase getPhase(Roadmap roadmap, int sequence, PhaseStatus phaseStatus) { + return Phase.create(roadmap, sequence, GOAL, DESCRIPTION, phaseStatus, START_DATE, END_DATE); } } diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepositoryTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepositoryTest.java index efbb6920..24760d6e 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepositoryTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepositoryTest.java @@ -8,6 +8,7 @@ import org.sopt.kareer.domain.roadmap.entity.Phase; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; import org.sopt.kareer.domain.roadmap.entity.PhaseActionGuideline; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import org.sopt.kareer.domain.roadmap.fixture.PhaseActionFixture; @@ -30,6 +31,9 @@ public class PhaseActionGuidelineRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseRepository phaseRepository; @@ -44,7 +48,8 @@ public class PhaseActionGuidelineRepositoryTest { void findContentByPhaseActionId() { // given Member member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - Phase phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + Roadmap roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + Phase phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); PhaseAction action1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction action2 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepositoryTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepositoryTest.java index 05e4deea..803860a4 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepositoryTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepositoryTest.java @@ -8,6 +8,7 @@ import org.sopt.kareer.domain.roadmap.entity.Phase; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; import org.sopt.kareer.domain.roadmap.entity.PhaseActionMistake; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import org.sopt.kareer.domain.roadmap.fixture.PhaseActionFixture; @@ -30,6 +31,9 @@ public class PhaseActionMistakeRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseRepository phaseRepository; @@ -44,7 +48,8 @@ public class PhaseActionMistakeRepositoryTest { void findContentByPhaseActionId() { // given Member member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - Phase phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + Roadmap roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + Phase phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); PhaseAction action1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction action2 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepositoryTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepositoryTest.java index 5f0a5e0e..912e1cf7 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepositoryTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepositoryTest.java @@ -8,8 +8,10 @@ import org.sopt.kareer.domain.member.repository.MemberRepository; import org.sopt.kareer.domain.roadmap.entity.Phase; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.sopt.kareer.domain.roadmap.fixture.PhaseActionFixture; import org.sopt.kareer.domain.roadmap.fixture.PhaseFixture; import org.sopt.kareer.global.config.QuerydslConfig; @@ -34,23 +36,28 @@ public class PhaseActionRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseActionRepository phaseActionRepository; private Phase phase1; private Member member1; + private Roadmap roadmap1; @BeforeEach void setUp() { member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); } @Test @DisplayName("phaseId가 일치하는 phaseAction들 중 아직 완료되지 않은 phaseAction들을 반환한다. ") void findByPhaseIdAndCompletedIsFalse() { // given - Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(member1, 2, PhaseStatus.NEXT)); + Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 2, PhaseStatus.NEXT)); PhaseAction action1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction action2 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); @@ -77,12 +84,12 @@ void findByIdAndMemberId() { PhaseAction action1 = phaseActionRepository.save(PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA)); // when - Optional result = phaseActionRepository.findByIdAndMemberId(action1.getId(), member1.getId()); + Optional result = phaseActionRepository.findByIdAndMemberIdAndRoadmapStatus(action1.getId(), member1.getId(), RoadmapActiveStatus.ACTIVE); // then assertThat(result).isPresent(); assertThat(result.get().getId()).isEqualTo(action1.getId()); - assertThat(result.get().getPhase().getMember().getId()) + assertThat(result.get().getPhase().getRoadmap().getMember().getId()) .isEqualTo(member1.getId()); } } diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryTest.java index 58d13949..8cb6cf44 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryTest.java @@ -8,8 +8,10 @@ import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; import org.sopt.kareer.domain.roadmap.entity.Phase; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; +import org.sopt.kareer.domain.roadmap.entity.enums.RoadmapActiveStatus; import org.sopt.kareer.domain.roadmap.fixture.PhaseActionFixture; import org.sopt.kareer.domain.roadmap.fixture.PhaseFixture; import org.sopt.kareer.global.config.QuerydslConfig; @@ -34,16 +36,21 @@ public class PhaseRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseActionRepository phaseActionRepository; private Phase phase1; private Member member1; + private Roadmap roadmap1; @BeforeEach void setUp() { member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); } @Nested @@ -54,8 +61,8 @@ class FindPhases { @DisplayName("phase 리스트가 sequence 오름차순 정렬되어 반환된다.") void findPhases_ordered() { // given - Phase phase3 = PhaseFixture.getPhase(member1, 3, PhaseStatus.FUTURE); - Phase phase2 = PhaseFixture.getPhase(member1, 2, PhaseStatus.NEXT); + Phase phase3 = PhaseFixture.getPhase(roadmap1, 3, PhaseStatus.FUTURE); + Phase phase2 = PhaseFixture.getPhase(roadmap1, 2, PhaseStatus.NEXT); phaseRepository.saveAll(List.of(phase2, phase3)); // when @@ -74,7 +81,8 @@ void findPhases_ordered() { void findPhases_onlyOwnData() { // given Member member2 = memberRepository.save(MemberFixture.getMember("test-provider-id-2")); - Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(member2, 1, PhaseStatus.CURRENT)); + Roadmap roadmap2 = roadmapRepository.save(Roadmap.create(member2)); + Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(roadmap2, 1, PhaseStatus.CURRENT)); // when List response = phaseRepository.findPhases(member1.getId()); @@ -124,7 +132,8 @@ void getRoadmapPhaseDetail_grouped() { void getRoadmapPhaseDetail_onlyOwnData() { // given Member member2 = memberRepository.save(MemberFixture.getMember("test-provider-id-2")); - Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(member2, 1, PhaseStatus.CURRENT)); + Roadmap roadmap2 = roadmapRepository.save(Roadmap.create(member2)); + Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(roadmap2, 1, PhaseStatus.CURRENT)); PhaseAction phaseActionVisa1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction phaseActionVisaOfOtherMember = PhaseActionFixture.getPhaseAction(phase2, PhaseActionType.CAREER); @@ -146,10 +155,8 @@ class ExistsByIdAndMemberId { @Test @DisplayName("phaseId와 memberId가 일치하는 Phase가 존재하면 true를 반환한다.") void existsByIdAndMemberId_true() { - // given - // when - boolean result = phaseRepository.existsByIdAndMember_Id(phase1.getId(), member1.getId()); + boolean result = phaseRepository.existsByIdAndRoadmap_Member_IdAndRoadmap_Status(phase1.getId(), member1.getId(), RoadmapActiveStatus.ACTIVE); // then assertThat(result).isTrue(); @@ -160,8 +167,9 @@ void existsByIdAndMemberId_true() { void existsByIdAndMemberId_false() { // given Member member2 = memberRepository.save(MemberFixture.getMember("test-provider-id-2")); + // when - boolean result = phaseRepository.existsByIdAndMember_Id(phase1.getId(), member2.getId()); + boolean result = phaseRepository.existsByIdAndRoadmap_Member_IdAndRoadmap_Status(phase1.getId(), member2.getId(), RoadmapActiveStatus.ACTIVE); // then assertThat(result).isFalse(); diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseActionServiceTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseActionServiceTest.java index bb1cb3e6..8ed90b17 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseActionServiceTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseActionServiceTest.java @@ -9,6 +9,7 @@ import org.sopt.kareer.domain.roadmap.entity.PhaseAction; import org.sopt.kareer.domain.roadmap.entity.PhaseActionGuideline; import org.sopt.kareer.domain.roadmap.entity.PhaseActionMistake; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import org.sopt.kareer.domain.roadmap.exception.RoadMapException; @@ -19,6 +20,7 @@ import org.sopt.kareer.domain.roadmap.repository.PhaseActionMistakeRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseActionRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseRepository; +import org.sopt.kareer.domain.roadmap.repository.RoadmapRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; @@ -35,6 +37,9 @@ public class PhaseActionServiceTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseRepository phaseRepository; @@ -52,11 +57,13 @@ public class PhaseActionServiceTest { private Phase phase1; private Member member1; + private Roadmap roadmap1; @BeforeEach void setUp() { member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); } @AfterEach @@ -65,6 +72,7 @@ void tearDown() { phaseActionMistakeRepository.deleteAll(); phaseActionRepository.deleteAllInBatch(); phaseRepository.deleteAllInBatch(); + roadmapRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); } diff --git a/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseServiceTest.java b/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseServiceTest.java index 8bd46e25..0602b121 100644 --- a/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseServiceTest.java +++ b/src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseServiceTest.java @@ -10,6 +10,7 @@ import org.sopt.kareer.domain.roadmap.dto.response.RoadmapPhaseDetailResponse; import org.sopt.kareer.domain.roadmap.entity.Phase; import org.sopt.kareer.domain.roadmap.entity.PhaseAction; +import org.sopt.kareer.domain.roadmap.entity.Roadmap; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseActionType; import org.sopt.kareer.domain.roadmap.entity.enums.PhaseStatus; import org.sopt.kareer.domain.roadmap.exception.RoadMapException; @@ -18,6 +19,7 @@ import org.sopt.kareer.domain.roadmap.fixture.PhaseFixture; import org.sopt.kareer.domain.roadmap.repository.PhaseActionRepository; import org.sopt.kareer.domain.roadmap.repository.PhaseRepository; +import org.sopt.kareer.domain.roadmap.repository.RoadmapRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; @@ -35,6 +37,9 @@ public class PhaseServiceTest { @Autowired private MemberRepository memberRepository; + @Autowired + private RoadmapRepository roadmapRepository; + @Autowired private PhaseRepository phaseRepository; @@ -46,17 +51,20 @@ public class PhaseServiceTest { private Phase phase1; private Member member1; + private Roadmap roadmap1; @BeforeEach void setUp() { member1 = memberRepository.save(MemberFixture.getMember("test-provider-id-1")); - phase1 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + roadmap1 = roadmapRepository.save(Roadmap.create(member1)); + phase1 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); } @AfterEach void tearDown() { phaseActionRepository.deleteAllInBatch(); phaseRepository.deleteAllInBatch(); + roadmapRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); } @@ -68,8 +76,8 @@ class GetPhases { @DisplayName("Phase 리스트를 조회한다.") void getPhases_success() { // given - Phase phase2 = PhaseFixture.getPhase(member1, 2, PhaseStatus.NEXT); - Phase phase3 = PhaseFixture.getPhase(member1, 3, PhaseStatus.FUTURE); + Phase phase2 = PhaseFixture.getPhase(roadmap1, 2, PhaseStatus.NEXT); + Phase phase3 = PhaseFixture.getPhase(roadmap1, 3, PhaseStatus.FUTURE); phaseRepository.saveAll(List.of(phase1, phase2, phase3)); // when @@ -91,9 +99,10 @@ void getPhases_success() { void getPhases_onlyOwnData() { // given Member member2 = memberRepository.save(MemberFixture.getMember("test-provider-id-2")); - Phase phase2 = PhaseFixture.getPhase(member1, 2, PhaseStatus.NEXT); - Phase phase3 = PhaseFixture.getPhase(member1, 3, PhaseStatus.FUTURE); - Phase phase4 = PhaseFixture.getPhase(member2, 2, PhaseStatus.CURRENT); + Roadmap roadmap2 = roadmapRepository.save(Roadmap.create(member2)); + Phase phase2 = PhaseFixture.getPhase(roadmap1, 2, PhaseStatus.NEXT); + Phase phase3 = PhaseFixture.getPhase(roadmap1, 3, PhaseStatus.FUTURE); + Phase phase4 = PhaseFixture.getPhase(roadmap2, 2, PhaseStatus.CURRENT); phaseRepository.saveAll(List.of(phase1, phase2, phase3, phase4)); // when @@ -163,9 +172,9 @@ void getRoadmapPhaseDetail_emptyGrouped() { @DisplayName("로드맵 Phase 상세정보를 조회시 그룹 내 아이템은 deadline 오름차순, 동일 시 title 오름차순으로 정렬된다.") void getRoadmapPhaseDetail_ordered() { // given - PhaseAction phaseActionVisaLateTitle1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA, "test-title-1", LocalDate.of(2026, 3,2)); - PhaseAction phaseActionVisaLateTitle2 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA,"test-title-2", LocalDate.of(2026, 3,2)); - PhaseAction phaseActionVisaEarlyTitle1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA, "test-title-1", LocalDate.of(2026, 3,1)); + PhaseAction phaseActionVisaLateTitle1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA, "test-title-1", LocalDate.of(2026, 3, 2)); + PhaseAction phaseActionVisaLateTitle2 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA, "test-title-2", LocalDate.of(2026, 3, 2)); + PhaseAction phaseActionVisaEarlyTitle1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA, "test-title-1", LocalDate.of(2026, 3, 1)); phaseActionRepository.saveAll(List.of(phaseActionVisaLateTitle1, phaseActionVisaLateTitle2, phaseActionVisaEarlyTitle1)); @@ -186,7 +195,7 @@ void getRoadmapPhaseDetail_ordered() { @DisplayName("특정 로드맵 Phase 상세정보 조회 시, 다른 Phase의 Action은 포함되지 않는다.") void getRoadmapPhaseDetail_onlyParticularPhaseData() { // given - Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); PhaseAction phaseActionVisa1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction phaseActionVisa2 = PhaseActionFixture.getPhaseAction(phase2, PhaseActionType.VISA); phaseActionRepository.saveAll(List.of(phaseActionVisa1, phaseActionVisa2)); @@ -261,7 +270,7 @@ void getHomePhaseDetail_completed() { @DisplayName("특정 홈 Phase 상세정보 조회 시, 다른 Phase의 Action은 포함되지 않는다.") void getHomePhaseDetail_onlyParticularPhaseData() { // given - Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(member1, 1, PhaseStatus.CURRENT)); + Phase phase2 = phaseRepository.save(PhaseFixture.getPhase(roadmap1, 1, PhaseStatus.CURRENT)); PhaseAction phaseActionVisa1 = PhaseActionFixture.getPhaseAction(phase1, PhaseActionType.VISA); PhaseAction phaseActionVisa2 = PhaseActionFixture.getPhaseAction(phase2, PhaseActionType.VISA); phaseActionRepository.saveAll(List.of(phaseActionVisa1, phaseActionVisa2)); diff --git a/src/test/java/org/sopt/kareer/support/ControllerTestSupport.java b/src/test/java/org/sopt/kareer/support/ControllerTestSupport.java index f3ab36f4..35b91f45 100644 --- a/src/test/java/org/sopt/kareer/support/ControllerTestSupport.java +++ b/src/test/java/org/sopt/kareer/support/ControllerTestSupport.java @@ -5,6 +5,8 @@ import org.sopt.kareer.domain.jobposting.service.JobPostingCrawler; import org.sopt.kareer.domain.jobposting.service.JobPostingService; import org.sopt.kareer.domain.member.controller.MemberController; +import org.sopt.kareer.domain.member.controller.MemberControllerV2; +import org.sopt.kareer.domain.member.service.LocalizedOnboardQueryService; import org.sopt.kareer.domain.member.service.MemberService; import org.sopt.kareer.domain.roadmap.controller.PhaseActionController; import org.sopt.kareer.domain.roadmap.controller.PhaseController; @@ -31,7 +33,7 @@ import java.util.List; -@WebMvcTest(controllers = {JobPostingController.class, MemberController.class, PhaseController.class, PhaseActionController.class}, +@WebMvcTest(controllers = {JobPostingController.class, MemberController.class, MemberControllerV2.class, PhaseController.class, PhaseActionController.class}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE) }) @AutoConfigureMockMvc(addFilters = false) @@ -56,6 +58,9 @@ public abstract class ControllerTestSupport { @MockBean protected MemberService memberService; + @MockBean + protected LocalizedOnboardQueryService localizedOnboardQueryService; + @MockBean protected RoadMapService roadMapService; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 12b1b2d6..e923ab41 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -90,6 +90,8 @@ auth: name: test secure: false same-site: test + token-blacklist: + redis-prefix: test-blacklist swagger: auth: username: ddd @@ -97,3 +99,15 @@ swagger: discord: webhook: url: test-url + +cohere: + api-key: test-cohere-key + base-url: https://api.cohere.com + rerank: + model: rerank-multilingual-v3.0 + top-n: 10 + +google: + translate: + url: https://translation.googleapis.com + api-key: test-google-translate-key