Skip to content

[Refactor] 로드맵 관련 패키지 수정#239

Open
jeong1112 wants to merge 18 commits intodevelopfrom
refactor/#238-roadmap
Open

[Refactor] 로드맵 관련 패키지 수정#239
jeong1112 wants to merge 18 commits intodevelopfrom
refactor/#238-roadmap

Conversation

@jeong1112
Copy link
Copy Markdown
Collaborator

@jeong1112 jeong1112 commented Apr 15, 2026

Related issue 🛠

Work Description 📝

  • 기존에 존재하던 roadmap 패키지 관련 리팩토링을 진행했습니다.

Phase, PhaseAction, ActionItem <-> Roadmap 연관관계 수정

배경

roadmap 패키지에 Roadmap이라는 이름의 엔티티가 존재하지 않아 패키지명과 실제 도메인 구조가 불일치했습니다.

기존 계층 구조:
Member → Phase → PhaseAction → ActionItem

Phase가 Member에 직접 연결되어 있어 "로드맵"이라는 개념이 코드 어디에도 명시적으로 표현되지 않았고, RoadMapService / RoadMapPersistService 같은 서비스명이 실체 없는
개념을 다루는 구조였습니다.

변경 사항

Roadmap을 Aggregate Root 엔티티로 도입해 계층 구조를 명확하게 했습니다.

Member → Roadmap → Phase → PhaseAction → ActionItem

Roadmap 엔티티 컬럼

  • member_id — 소유자
  • status (ACTIVE / INACTIVE) — 소프트 딜리트용

설계 근거

Member는 하나의 active 로드맵만 가질 수 있으나, 추후 로드맵 재생성 기능이 추가될 경우 이전 로드맵을 INACTIVE로 아카이브하고 새 로드맵을 생성하는 구조가 필요합니다.
Roadmap 엔티티가 없으면 이 히스토리를 표현할 방법이 없어 도입했습니다.

엔드포인트 변경

설계 구조 변화에 따라 API 엔드포인트도 변경되었습니다. REST 설계 원칙상 하위 리소스는 상위 리소스의 경로 아래에 위치해야 합니다. Phase, PhaseAction, ActionItem은 모두 Roadmap에 속한 리소스이므로 /roadmap/ 하위로 통일했습니다.

Controller 어노테이션 분리

기존에 Controller 내부에는 Swagger 명세를 위한 어노테이션과 API 엔드포인트 매핑을 위한 어노테이션으로 인해 코드의 가독성이 많이 떨어져있는 상태였는데, 이것을 인터페이스로 분리하여 가독성을 높이고 Controller는 비즈니스 로직에 집중할 수 있도록 수정했습니다.

ScreenShots 📷

To Reviewers 📢

변경사항이 좀 많아요..ㅠ

Summary by CodeRabbit

  • New Features

    • Added Job Posting API with crawling, personalized recommendations, and bookmarking capabilities
    • Introduced Member V2 onboarding endpoint with enhanced data handling
    • Separated Roadmap generation into dedicated API endpoint
  • Refactor

    • Reorganized API endpoints to group roadmap-related features under /api/v1/roadmap/* path
    • Restructured internal roadmap architecture for improved data management

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

개요

이 PR은 로드맵 관련 도메인의 구조를 정리하면서 동시에 컨트롤러-API 인터페이스 분리 패턴을 도입합니다. 기존 Member 엔티티에서 직접 Phase를 관리하던 방식을 Roadmap 엔티티를 중간에 두고 관리하도록 변경하며, 여러 컨트롤러를 API 인터페이스 기반 구조로 리팩토링합니다.

주요 변경사항

응집도 / 파일(s) 요약
API 인터페이스 추상화
src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingApi.java, src/main/java/org/sopt/kareer/domain/member/controller/MemberApi.java, src/main/java/org/sopt/kareer/domain/member/controller/MemberApiV2.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemApi.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionApi.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseApi.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java, src/main/java/org/sopt/kareer/domain/term/controller/TermApi.java
Swagger 어노테이션 및 엔드포인트 매핑을 정의하는 새로운 인터페이스 8개 도입
컨트롤러 구현 변경
src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java, src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java, src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java, src/main/java/org/sopt/kareer/domain/term/controller/TermController.java
컨트롤러들이 해당 API 인터페이스를 구현하도록 변경; 엔드포인트 어노테이션 제거
Roadmap 엔티티 도입
src/main/java/org/sopt/kareer/domain/roadmap/entity/Roadmap.java, src/main/java/org/sopt/kareer/domain/roadmap/entity/enums/RoadmapActiveStatus.java
새로운 Roadmap 엔티티 추가로 Phase의 부모 관계를 Member에서 Roadmap으로 변경
Phase 엔티티 관계 변경
src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java
Phase의 member 필드를 roadmap 필드로 변경; 생성자 및 팩토리 메서드 업데이트
Member 엔티티 정리
src/main/java/org/sopt/kareer/domain/member/entity/Member.java, src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java
roadmapStatus 필드 및 관련 메서드 제거; RoadmapStatus enum 삭제
Member 에러 코드 정리
src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java
ROADMAP_IN_PROGRESS, ROADMAP_ALREADY_GENERATED 에러 코드 제거
Repository 메서드 경로 업데이트
src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java, src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java, src/main/java/org/sopt/kareer/domain/roadmap/repository/...*Repository.java
조인 경로가 ...Member_Id에서 ...Roadmap_Member_Id로 변경되어 새로운 엔티티 관계 구조 반영
Roadmap Repository 추가
src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java
새로운 RoadmapRepository 인터페이스 추가로 Roadmap 엔티티 조회/삭제
서비스 로직 리팩토링
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java, src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java, src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java, src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
Roadmap 엔티티 생성 로직 추가; 기존 onboarding 메서드 제거 및 V2 메서드로 통일
Member 삭제 서비스 업데이트
src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java
RoadmapRepository 주입 및 Roadmap 삭제 로직 추가
Member Repository 정리
src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java
tryMarkRoadmapInProgress 메서드 제거
컨트롤러 경로 변경
src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java, src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java
엔드포인트 경로가 /api/v1/ 에서 /api/v1/roadmap/ 으로 변경
테스트 코드 업데이트
src/test/java/org/sopt/kareer/domain/member/controller/...*Test.java, src/test/java/org/sopt/kareer/domain/member/fixture/...*Fixture.java, src/test/java/org/sopt/kareer/domain/roadmap/repository/...*Test.java, src/test/java/org/sopt/kareer/support/ControllerTestSupport.java
새로운 엔티티 관계 및 API 변경에 맞춰 테스트 및 픽스처 업데이트; Member 필드를 enum에서 code 기반으로 변경
설정 추가
src/test/resources/application-test.yml
테스트 환경에 cohere, google.translate 설정 추가

예상 코드 리뷰 난도

🎯 4 (복잡함) | ⏱️ ~60분

관련 PR 목록

제안하는 리뷰어

  • eraser502
  • hyomee2

축하 시

🐰 로드맵의 궁전을 짓다니,
Member에서 분리된 Roadmap이 우뚝 서고,
API 인터페이스는 정갈하게 정렬되었네!
Phase는 이제 Roadmap 아래 모여있고,
컨트롤러는 인터페이스를 따르며 춤을 춘다! 🎭

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 로드맵 관련 패키지 수정이라는 주요 변경 사항을 간결하고 명확하게 요약합니다.
Linked Issues check ✅ Passed PR은 #238 이슈의 로드맵 도메인 구조 재정의 요구사항을 충족합니다. Roadmap 엔티티 도입, 패키지 경계 정의, API 엔드포인트 재구성 등을 구현했습니다.
Out of Scope Changes check ✅ Passed 로드맵 관련 클래스만 수정하고 관련 테스트와 설정을 업데이트했으며, 범위 외 변경사항이 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#238-roadmap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java (1)

19-20: 인터페이스 추출 효과를 완성하기 위해 중복 애너테이션을 정리하세요.

JobPostingApi로 HTTP 계약을 분리했으나 구현체에 @RequestMapping, @AuthenticationPrincipal, @RequestParam, @RequestPart, @PathVariable가 남아 있어 중복이 발생하고 있습니다. 이 프로젝트는 Spring Boot 3.2.4를 사용하므로 Spring Framework 6.1+ 지원으로 인터페이스에 선언된 이러한 애너테이션들이 구현체 메서드에 자동으로 적용됩니다. 따라서 웹 바인딩 애너테이션은 인터페이스에만 두고 구현체에서는 제거하면 이번 PR 목적(HTTP 계약을 인터페이스로 분리하여 컨트롤러를 비즈니스 로직에 집중)에 더욱 부합합니다.

♻️ 정리 예시
-@RequestMapping("/api/v1/job-postings")
 public class JobPostingController implements JobPostingApi {

     `@Override`
-    public ResponseEntity<BaseResponse<JobPostingCrawlListResponse>> crawlJobPostings(`@RequestParam`(defaultValue = "5") int limit) {
+    public ResponseEntity<BaseResponse<JobPostingCrawlListResponse>> crawlJobPostings(int limit) {
         return ResponseEntity.status(HttpStatus.OK)
                 .body(BaseResponse.ok(jobPostingCrawler.crawlJobPostingForTest(limit), "채용 공고 크롤링에 성공하였습니다."));
     }

     `@Override`
     public ResponseEntity<BaseResponse<JobPostingListResponse>> recommendJobPostings(
-            `@AuthenticationPrincipal` Long memberId,
-            `@RequestPart`(value = "files", required = false) List<MultipartFile> files,
-            `@RequestParam`(value = "includeCompletedTodo", defaultValue = "false") boolean includeCompletedTodos) {
+            Long memberId,
+            List<MultipartFile> files,
+            boolean includeCompletedTodos) {
         return ResponseEntity.status(HttpStatus.OK)
                 .body(BaseResponse.ok(jobPostingService.recommend(memberId, files, includeCompletedTodos), "채용 공고 추천에 성공하였습니다."));
     }

     `@Override`
     public ResponseEntity<BaseResponse<Void>> createJobPostingBookmark(
-            `@AuthenticationPrincipal` Long memberId,
-            `@PathVariable` Long jobPostingId) {
+            Long memberId,
+            Long jobPostingId) {
         jobPostingService.createOrDeleteBookmark(memberId, jobPostingId);
         return ResponseEntity.status(HttpStatus.OK)
                 .body(BaseResponse.ok("채용 공고 북마크 추가 / 삭제에 성공했습니다."));
     }

     `@Override`
-    public ResponseEntity<BaseResponse<JobPostingListResponse>> getJobPostingBookmarks(`@AuthenticationPrincipal` Long memberId) {
+    public ResponseEntity<BaseResponse<JobPostingListResponse>> getJobPostingBookmarks(Long memberId) {
         return ResponseEntity.status(HttpStatus.OK)
                 .body(BaseResponse.ok(jobPostingService.getJobPostingBookmarks(memberId), "북마크 채용 공고 조회에 성공하였습니다."));
     }
 }
// JobPostingApi.java
`@RequestMapping`("/api/v1/job-postings")
public interface JobPostingApi {
    ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java`
around lines 19 - 20, The controller still has duplicate web-binding
annotations; remove the class-level `@RequestMapping` and all method-level
web-binding annotations (`@AuthenticationPrincipal`, `@RequestParam`, `@RequestPart`,
`@PathVariable`, etc.) from JobPostingController so that the HTTP contract lives
only in JobPostingApi (which should retain `@RequestMapping` and the method
annotations); leave only business logic and non-web annotations in
JobPostingController and ensure method signatures match those declared in
JobPostingApi.
src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java (1)

23-69: POST /todo 경로도 라우팅 테스트를 하나 추가해두면 좋겠습니다.

이번 변경은 base path 이동과 interface 기반 매핑 도입이 함께 들어가서, 현재의 GET /guide 검증만으로는 createPhaseActionTodo 매핑 회귀를 잡기 어렵습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java`
around lines 23 - 69, Add a new test in PhaseActionControllerTest that verifies
the POST /todo route is correctly mapped to the controller method
createPhaseActionTodo: mock phaseActionService.createPhaseActionTodo (use any()
and eq(phaseActionId) to match args), perform a POST to
"/api/v1/roadmap/phase-actions/{phaseActionId}/todo" with a representative
request body, and assert the response status (Created or appropriate) and
expected JSON message; this ensures the base path/interface-based mapping
regression is caught.
src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java (1)

16-17: 중복된 @Builder 어노테이션이 있습니다.

클래스 레벨(Line 16)과 생성자 레벨(Line 49)에 모두 @Builder가 선언되어 있습니다. 생성자에 @Builder를 사용하는 경우 클래스 레벨의 @Builder는 제거하는 것이 좋습니다.

♻️ 클래스 레벨 `@Builder` 제거 제안
 `@Entity`
 `@Table`(name = "phases")
 `@Getter`
-@Builder
 `@AllArgsConstructor`
 `@NoArgsConstructor`(access = AccessLevel.PROTECTED)
 public class Phase extends BaseEntity {

Also applies to: 49-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java` around lines
16 - 17, Phase has duplicate `@Builder` annotations at the class level and on its
constructor; remove the class-level `@Builder` above the Phase class declaration
and keep the `@Builder` on the constructor (the one paired with
`@AllArgsConstructor`) so the constructor-level builder remains the single source
of builder generation; ensure no other annotations or imports are modified.
src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java (2)

107-114: MypageResponse.of()에 라벨 대신 코드 값을 전달하고 있습니다.

MypageResponse.of() 메서드의 파라미터 이름이 countryLabel, primaryMajorLabel 등으로 라벨 값을 기대하지만, 테스트에서는 member.getCountryCode() 등 코드 값을 전달하고 있습니다.

컨트롤러 테스트에서 서비스 응답을 모킹하므로 기능적으로는 문제없지만, 실제 서비스 동작과 일관성을 위해 라벨 문자열을 사용하는 것이 테스트 가독성에 더 좋을 수 있습니다.

MypageResponse response = MypageResponse.of(
        member, memberVisa,
        "United States",  // countryLabel
        "Computer Science",  // primaryMajorLabel
        "Seoul National University",  // universityLabel
        "해외 학사",  // degreeLabel
        "상급"  // englishLevelLabel
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java`
around lines 107 - 114, The test is passing code values to
MypageResponse.of(...) but the method expects human-readable label strings
(parameters like countryLabel, primaryMajorLabel, universityLabel, degreeLabel,
englishLevelLabel); update the test to pass label strings instead of
member.getCountryCode(), member.getPrimaryMajorCode(),
member.getUniversityCode(), member.getDegreeCode(), member.getEnglishLevelCode()
so the mocked service response matches real behavior and improves readability
(e.g., replace those get*Code() calls with descriptive labels such as "United
States", "Computer Science", "Seoul National University", "해외 학사", "상급").

66-80: 온보딩 목록 조회 테스트의 응답 검증이 약화되었습니다.

getOnboardCountries()getOnboardMajors() 테스트가 HTTP 200 상태만 확인하고 응답 본문 구조는 검증하지 않습니다. 응답 형식 변경 시 감지하지 못할 수 있으므로, 최소한 $.data 필드 존재 여부 정도는 검증하는 것이 좋습니다.

💡 최소 응답 구조 검증 추가 제안
     void getOnboardCountries() throws Exception {
         mockMvc.perform(get("/api/v1/members/onboard/countries"))
                 .andDo(print())
-                .andExpect(status().isOk());
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data").exists());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java`
around lines 66 - 80, Tests getOnboardCountries() and getOnboardMajors() only
assert HTTP 200 and miss verifying response body; update both tests to assert
the expected response structure by adding JSON assertions (e.g., ensure $.data
exists) using jsonPath assertions on the MockMvc result and optionally assert
content type is application/json so the tests fail when the API response shape
changes.
src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java (1)

14-14: findAllByMember_Id 메서드를 제거해주세요.

이 메서드는 코드베이스 어디에서도 사용되지 않고 있습니다. 저장소에서 검색한 결과 선언부에서만 나타나며 실제 호출처가 없습니다. 유사한 메서드들(findByMember_IdAndStatus, deleteAllByMember_Id)은 사용 중이지만 이 메서드는 불필요한 코드입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java`
at line 14, Remove the unused repository method declaration "List<Roadmap>
findAllByMember_Id(Long memberId);" from the RoadmapRepository interface: open
the RoadmapRepository.java file, delete the findAllByMember_Id method signature
(it’s unused across the codebase) and ensure no imports or references depend on
it; keep related methods like findByMember_IdAndStatus and deleteAllByMember_Id
untouched.
src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java (1)

29-37: 서비스 호출까지 검증해 컨트롤러 wiring 회귀를 잡아주세요.

지금은 200/메시지만 확인해서 컨트롤러가 memberService.onboardMemberV2(...)를 호출하지 않아도 테스트가 통과할 수 있습니다. 최소한 인증된 memberId=1L이 위임되는지까지 확인해두면 매핑 회귀를 더 잘 잡습니다.

예시
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.willDoNothing;
+import static org.mockito.BDDMockito.then;
 ...
         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("회원 온보딩이 완료되었습니다."));
+
+        then(memberService).should().onboardMemberV2(eq(1L), any());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java`
around lines 29 - 37, Update the test MemberControllerV2Test to verify the
controller actually delegates to memberService.onboardMemberV2(...) with the
authenticated memberId: after the mockMvc.perform(...) assertions, add a
Mockito.verify(memberService).onboardMemberV2(...) check (or verify(...,
times(1))) that asserts the first argument contains the memberId 1L from
authenticatedMember(); use argument matchers or an ArgumentCaptor to inspect the
DTO/Request passed to onboardMemberV2 and confirm memberId==1L so the wiring
from mockMvc/authentication to memberService is validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java`:
- Around line 112-115: The response message string in getMemberCompletionStatus
uses a trailing comma ("온보딩/약관동의 여부 조회에 성공하였습니다,") which is a typo exposed to
users; update the call to BaseResponse.ok in
MemberController#getMemberCompletionStatus to remove the trailing comma (e.g.,
"온보딩/약관동의 여부 조회에 성공하였습니다") so the returned message is correct, leaving the rest
of the invocation (memberService.getCompletion and ResponseEntity building)
unchanged.

In `@src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java`:
- Around line 22-25: The /test endpoint (RoadmapApi.generateRoadmapForTest) is
exposed to any authenticated user; restrict it by either adding an access
control annotation (e.g., put `@PreAuthorize` with the intended role such as
`@PreAuthorize`("hasRole('ADMIN')") on the RoadmapApi interface/implementation
method or annotate the controller with `@Profile`("test") to load only in test
profiles), or correct the SecurityConfig deny/permit rules to match the actual
path (/api/v1/roadmap/test) so that only the intended roles/contexts can call
generateRoadmapForTest; update whichever of RoadmapApi.generateRoadmapForTest,
the controller class, or SecurityConfig route list is most appropriate for your
design.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java`:
- Around line 34-38: The generateRoadmapForTest endpoint in RoadmapController
currently allows any authenticated user but must be server/admin-only; annotate
the generateRoadmapForTest method (or the controller) with a method-security
restriction such as `@PreAuthorize`("hasRole('ADMIN')") (or your project's admin
role) to restrict access, ensure method security is enabled (e.g.,
`@EnableMethodSecurity/`@EnableGlobalMethodSecurity in your security config), and
verify RoadMapService.createRoadmapTest and the RoadmapTestResponse payload
remain inaccessible to non-admins; alternatively, if you prefer
environment-level control, apply `@Profile`("server") to the controller to load it
only in server profiles.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java`:
- Around line 63-64: The existence check in PhaseService uses
phaseRepository.existsByIdAndRoadmap_Member_Id(phaseId, memberId) which allows
phases from INACTIVE roadmaps; update both occurrences (the checks around lines
where existsByIdAndRoadmap_Member_Id is called) to include the roadmap status
condition, e.g. call a repository method like
existsByIdAndRoadmap_Member_IdAndRoadmap_Status(phaseId, memberId,
RoadmapStatus.ACTIVE) (or Roadmap.Status.ACTIVE depending on your enum), and
ensure the repository method signature is added/used accordingly so only phases
belonging to ACTIVE roadmaps pass the check.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java`:
- Line 39: The current RoadMapPersistService call that does
roadmapRepository.save(Roadmap.create(member)) can create duplicate ACTIVE
roadmaps under concurrency; fix by enforcing uniqueness at the DB and adding an
atomic check-and-create path: add a database-level uniqueness constraint/index
to Roadmap for (member_id, status=ACTIVE) (or a partial unique index if
supported), and update the creation flow in RoadMapPersistService to run in a
transactional/atomic block that first checks for an existing ACTIVE roadmap
using a locking find (e.g., roadmapRepository.findByMemberAndStatus(member,
ACTIVE) with a PESSIMISTIC_WRITE/SELECT FOR UPDATE or equivalent), and only call
roadmapRepository.save(Roadmap.create(member)) if none exists (or handle the
unique constraint violation to retry/return the existing entity); reference
Roadmap.create, roadmapRepository.save, and the service method
RoadMapPersistService for where to implement the transaction/lock and DB
constraint.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java`:
- Around line 48-52: The current read-modify-write in RoadMapService using
roadmapRepository.findByMember_IdAndStatus(RoadmapActiveStatus.ACTIVE) then
deactivating and saving a new roadmap is racy under concurrent requests; fix by
enforcing the invariant at DB or lock level: add a UNIQUE partial/index
constraint for (member_id) where status=ACTIVE and/or perform the
find+deactivate+save inside a single transactional boundary with a pessimistic
lock (use roadmapRepository.findByMember_IdAndStatus with
`@Lock`(PESSIMISTIC_WRITE) or a repository method that executes SELECT ... FOR
UPDATE) so only one thread can see and mutate the ACTIVE row; ensure
actionItemRepository.deactivateAllByRoadmapId(...) and existing.deactivate()
execute in the same transaction before persisting the new roadmap to prevent
duplicate ACTIVES (also apply same change at the other occurrence noted around
line 79).

---

Nitpick comments:
In
`@src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java`:
- Around line 19-20: The controller still has duplicate web-binding annotations;
remove the class-level `@RequestMapping` and all method-level web-binding
annotations (`@AuthenticationPrincipal`, `@RequestParam`, `@RequestPart`,
`@PathVariable`, etc.) from JobPostingController so that the HTTP contract lives
only in JobPostingApi (which should retain `@RequestMapping` and the method
annotations); leave only business logic and non-web annotations in
JobPostingController and ensure method signatures match those declared in
JobPostingApi.

In `@src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java`:
- Around line 16-17: Phase has duplicate `@Builder` annotations at the class level
and on its constructor; remove the class-level `@Builder` above the Phase class
declaration and keep the `@Builder` on the constructor (the one paired with
`@AllArgsConstructor`) so the constructor-level builder remains the single source
of builder generation; ensure no other annotations or imports are modified.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java`:
- Line 14: Remove the unused repository method declaration "List<Roadmap>
findAllByMember_Id(Long memberId);" from the RoadmapRepository interface: open
the RoadmapRepository.java file, delete the findAllByMember_Id method signature
(it’s unused across the codebase) and ensure no imports or references depend on
it; keep related methods like findByMember_IdAndStatus and deleteAllByMember_Id
untouched.

In
`@src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java`:
- Around line 107-114: The test is passing code values to MypageResponse.of(...)
but the method expects human-readable label strings (parameters like
countryLabel, primaryMajorLabel, universityLabel, degreeLabel,
englishLevelLabel); update the test to pass label strings instead of
member.getCountryCode(), member.getPrimaryMajorCode(),
member.getUniversityCode(), member.getDegreeCode(), member.getEnglishLevelCode()
so the mocked service response matches real behavior and improves readability
(e.g., replace those get*Code() calls with descriptive labels such as "United
States", "Computer Science", "Seoul National University", "해외 학사", "상급").
- Around line 66-80: Tests getOnboardCountries() and getOnboardMajors() only
assert HTTP 200 and miss verifying response body; update both tests to assert
the expected response structure by adding JSON assertions (e.g., ensure $.data
exists) using jsonPath assertions on the MockMvc result and optionally assert
content type is application/json so the tests fail when the API response shape
changes.

In
`@src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java`:
- Around line 29-37: Update the test MemberControllerV2Test to verify the
controller actually delegates to memberService.onboardMemberV2(...) with the
authenticated memberId: after the mockMvc.perform(...) assertions, add a
Mockito.verify(memberService).onboardMemberV2(...) check (or verify(...,
times(1))) that asserts the first argument contains the memberId 1L from
authenticatedMember(); use argument matchers or an ArgumentCaptor to inspect the
DTO/Request passed to onboardMemberV2 and confirm memberId==1L so the wiring
from mockMvc/authentication to memberService is validated.

In
`@src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java`:
- Around line 23-69: Add a new test in PhaseActionControllerTest that verifies
the POST /todo route is correctly mapped to the controller method
createPhaseActionTodo: mock phaseActionService.createPhaseActionTodo (use any()
and eq(phaseActionId) to match args), perform a POST to
"/api/v1/roadmap/phase-actions/{phaseActionId}/todo" with a representative
request body, and assert the response status (Created or appropriate) and
expected JSON message; this ensures the base path/interface-based mapping
regression is caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 287821b2-73eb-4301-b22d-860a29eb133d

📥 Commits

Reviewing files that changed from the base of the PR and between 482e5ae and 98b6ad2.

📒 Files selected for processing (58)
  • src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingApi.java
  • src/main/java/org/sopt/kareer/domain/jobposting/controller/JobPostingController.java
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberApi.java
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberApiV2.java
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberControllerV2.java
  • src/main/java/org/sopt/kareer/domain/member/entity/Member.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java
  • src/main/java/org/sopt/kareer/domain/member/exception/MemberErrorCode.java
  • src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java
  • src/main/java/org/sopt/kareer/domain/member/service/MemberDeletionService.java
  • src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemApi.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/ActionItemController.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionApi.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionController.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseApi.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/PhaseController.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapApi.java
  • src/main/java/org/sopt/kareer/domain/roadmap/controller/RoadmapController.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/Phase.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/Roadmap.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/enums/RoadmapActiveStatus.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryCustomImpl.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/RoadmapRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java
  • src/main/java/org/sopt/kareer/domain/term/controller/TermApi.java
  • src/main/java/org/sopt/kareer/domain/term/controller/TermController.java
  • src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerTest.java
  • src/test/java/org/sopt/kareer/domain/member/controller/MemberControllerV2Test.java
  • src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java
  • src/test/java/org/sopt/kareer/domain/member/fixture/MemberFixture.java
  • src/test/java/org/sopt/kareer/domain/member/fixture/MemberOnboardRequestFixture.java
  • src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java
  • src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java
  • src/test/java/org/sopt/kareer/domain/member/service/MemberServiceTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseActionControllerTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/controller/PhaseControllerTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/fixture/PhaseFixture.java
  • src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineRepositoryTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeRepositoryTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionRepositoryTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/repository/PhaseRepositoryTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseActionServiceTest.java
  • src/test/java/org/sopt/kareer/domain/roadmap/service/PhaseServiceTest.java
  • src/test/java/org/sopt/kareer/support/ControllerTestSupport.java
  • src/test/resources/application-test.yml
💤 Files with no reviewable changes (6)
  • src/test/java/org/sopt/kareer/domain/member/repository/MemberRepositoryTest.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/RoadmapStatus.java
  • src/test/java/org/sopt/kareer/domain/member/fixture/MemberVisaFixture.java
  • src/main/java/org/sopt/kareer/domain/member/repository/MemberRepository.java
  • src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
  • src/main/java/org/sopt/kareer/domain/member/entity/Member.java

Comment thread src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java Outdated
Comment thread src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 로드맵 관련 도메인 패키지 구조 정리

1 participant