Skip to content
This repository was archived by the owner on Jul 3, 2026. It is now read-only.

[0.6.0] SW-128 예약 시간에 특별 미션을 모든 팀에게 배정할 수 있다.#39

Open
BORB-CHOI wants to merge 36 commits into
developfrom
SW-128
Open

[0.6.0] SW-128 예약 시간에 특별 미션을 모든 팀에게 배정할 수 있다.#39
BORB-CHOI wants to merge 36 commits into
developfrom
SW-128

Conversation

@BORB-CHOI

@BORB-CHOI BORB-CHOI commented Aug 28, 2025

Copy link
Copy Markdown
Contributor

📋 변경사항 요약

관리자가 특별 미션을 배정하는 즉시 해당 학기 모든 팀에 팀 미션이 생성되고, startedAt 값에 따라 활성화/비활성화 가능합니다.

🎯 변경 이유

🔧 주요 변경사항

  • 새로운 기능 추가
  • 기존 기능 개선
  • 버그 수정
  • 리팩토링
  • 문서 업데이트
  • 테스트 추가/수정
  • 설정 변경

🔍 검토 포인트

✅ 체크리스트

  • 코드가 프로젝트의 코딩 스타일을 따릅니다
  • 자체 검토를 완료했습니다
  • 문서를 업데이트했습니다 (해당하는 경우)
  • 변경사항이 기존 테스트를 통과합니다
  • 새로운 테스트를 추가했습니다 (해당하는 경우)

📝 추가 노트

Summary by CodeRabbit

  • New Features

    • 관리자: 시즌별 스페셜 미션 조회 API 추가, 시즌 내 전체 팀에 스페셜 미션 일괄 생성(시작/종료 시각 포함).
    • 아이템: 뽑기 시 포인트 차감(포인트 부족 에러 추가).
    • 제출: 중복 제출 방지, 인증 사용자 기반 접근 제어.
  • Refactor

    • 제출/팀 미션 API 경로 정리: team_mission 기준으로 변경, 엔드포인트에 인증 사용자 필요.
    • 진행 중 미션 판별이 시작/종료 시각 윈도우 기준으로 개선.
  • Documentation

    • 팀 일정 조회 사용자 목록 파라미터에 Swagger 문서 보강.

@BORB-CHOI BORB-CHOI requested a review from son-daehyeon August 28, 2025 14:47
@BORB-CHOI BORB-CHOI self-assigned this Aug 28, 2025
@coderabbitai

coderabbitai Bot commented Aug 28, 2025

Copy link
Copy Markdown

Walkthrough

시즌별 스페셜 미션 조회/생성 기능과 팀 미션 제출 흐름을 재편했습니다. 인증 사용자 기반 접근 제어를 도입했고 아이템 뽑기 시 포인트 차감/부족 예외를 추가했습니다. TeamMission에 시간창(startedAt/endedAt) 기준을 적용했으며 여러 엔티티의 컬렉션 초기화를 가변 리스트로 변경했습니다. 패키지 경로가 team_mission 중심으로 정리되었습니다.

Changes

Cohort / File(s) Summary
Admin: 시즌 스페셜 미션 조회
src/.../_season_mission/controller/AdminSeasonMissionController.java, src/.../_season_mission/service/AdminSeasonMissionService.java
GET /admin/seasons/{season}/missions/special 추가. 서비스에 getSeasonSpecialMissions(Season) 메서드 추가하여 MissionType.SPECIAL 필터로 조회. Swagger 주석 포함.
예외 코드 확장
src/.../_season_mission/exception/SeasonMissionExceptions.java, src/.../mission/exception/MissionExceptions.java, src/.../_team/_team_mission/exceptions/TeamMissionExceptions.java, src/.../_team/_item/exception/ItemExceptions.java
다음 상수 추가: INVALID_SEASON_MISSION, INVALID_SPECIAL_MISSION_TYPE, INVALID_MISSION_TIME, NOT_ENOUGH_POINT.
Admin: 스페셜 팀 미션 생성
src/.../_team/_team_mission/controller/AdminTeamMissionController.java, src/.../_team/_team_mission/service/TeamMissionService.java, src/.../_team/_team_mission/dto/response/TeamMissionListResponse.java, src/.../_team/_team_mission/dto/response/TeamMissionResponse.java, src/.../_team/_team_mission/entity/TeamMission.java, src/.../_team/_team_mission/repository/TeamMissionRepository.java, src/.../_team/_team_mission/dto/request/CreateSpecialTeamMissionRequest.java
Admin POST 추가: 시즌의 모든 팀에 특정 SPECIAL 미션을 기간(startedAt~endedAt)으로 생성. TeamMission에 startedAt 필드 도입. 리포지토리 조회 조건을 시간창 포함(startedAt <= now && endedAt >= now)으로 변경. DTO에 기간 필드 및 매핑 확장. 유효성/타입/시즌 상태 검증 추가.
팀 미션 제출 리팩터링 및 인증 연계
src/.../_team/_team_mission/_submission/controller/SubmissionController.java, src/.../_team/_team_mission/_submission/service/SubmissionService.java, src/.../_team/_team_mission/_submission/repository/SubmissionRepository.java, src/.../_team/_team_mission/_submission/entity/Submission.java, src/.../_team/_team_mission/_submission/dto/request/CreateSubmissionRequest.java, src/.../_team/_team_mission/_submission/dto/response/SubmissionListResponse.java, src/.../_team/_team_mission/_submission/dto/response/SubmissionResponse.java
패키지 경로를 team_mission 기준으로 이동. 모든 엔드포인트에 인증 사용자 주입 및 접근 검증 추가. TeamMission 기반 단건/목록 조회, 제출 시 중복 제출 방지. JPA 리포지토리 경로/시그니처 정비(Optional 반환).
구 리소스 제거/대체
src/.../_mission_team/_submission_mission_team/repository/SubmissionRepository.java, src/.../_mission_team/_submission_mission_team/service/SubmissionService.java
구 submission 리포지토리/서비스 삭제. 신규 team_mission 네임스페이스로 대체.
아이템: 인증 및 포인트 로직
src/.../_team/_item/controller/ItemController.java, src/.../_team/_item/service/ItemService.java
컨트롤러/서비스 시그니처에 User 추가. canAccessItem(User, Season, Team)로 접근 검증. 뽑기 시 포인트 100 미만이면 NOT_ENOUGH_POINT, 아니면 100 차감 후 진행. UserTeamRepository 의존 추가.
팀 스케줄 문서화 보강
src/.../_team/_schedule/controller/TeamScheduleController.java
users 파라미터에 Swagger ArraySchema(UUID 문자열 배열) 주석 추가.
TeamMission 컨트롤러 경로 정리
src/.../_mission_team/controller/TeamMissionController.javasrc/.../_team_mission/controller/TeamMissionController.java
패키지/임포트 이동. 로직 변경 없음.
엔티티 컬렉션 가변화 및 세터 제한
src/.../mission/entity/Mission.java, src/.../_team/entity/Team.java, src/.../season/entity/Season.java, src/.../user/entity/User.java
List.of() → new ArrayList<>()로 초기화 변경. 관련 컬렉션 필드에 @Setter(AccessLevel.NONE) 적용. Season의 missions 필드명을 seasonMissions로 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Admin as Admin
  participant AdminAPI as AdminTeamMissionController
  participant Svc as TeamMissionService
  participant Repo as TeamMissionRepository
  participant Teams as Season.teams

  Admin->>AdminAPI: POST /admin/seasons/{season}/teams/missions/{mission}
  AdminAPI->>Svc: createSpecialMission(season, mission, startedAt, endedAt)
  Svc->>Svc: validate time window / season current / mission type SPECIAL
  Svc->>Teams: iterate teams in season
  loop for each team
    Svc->>Repo: save(TeamMission(team, mission, startedAt, endedAt))
  end
  Svc-->>AdminAPI: List<TeamMission>
  AdminAPI-->>Admin: ApiResponse(TeamMissionListResponse)
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant API as ItemController
  participant S as ItemService
  participant UT as UserTeamRepository
  participant T as Team

  U->>API: POST /seasons/{season}/teams/{team}/items/draw
  API->>S: drawItem(user, season, team)
  S->>UT: isMember(user, team) and season match
  alt not authorized
    S-->>API: error (forbidden)
  else authorized
    S->>T: check points
    alt points < 100
      S-->>API: ItemExceptions.NOT_ENOUGH_POINT
    else
      S->>T: deduct 100
      S-->>API: ItemResponse(...)
    end
  end
  API-->>U: ApiResponse(...)
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant API as SubmissionController
  participant S as SubmissionService
  participant UT as UserTeamRepository
  participant TM as TeamMissionRepository
  participant SR as SubmissionRepository

  U->>API: POST /team_mission/{teamMission}/submissions
  API->>S: submitSubmission(user, season, team, teamMission, req)
  S->>UT: canAccessSubmission(user, season, team)
  alt not allowed
    S-->>API: error (forbidden)
  else
    S->>TM: verify teamMission belongs to team
    S->>SR: findByTeamMission(teamMission)
    alt exists
      S-->>API: TeamMissionExceptions.ALREADY_SUBMITTED
    else
      S->>SR: save(Submission(teamMission, req.image))
      S-->>API: SubmissionResponse
    end
  end
  API-->>U: ApiResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • son-daehyeon

Pre-merge checks (2 passed, 1 warning)

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목은 핵심 변경사항인 "예약 시간에 특별 미션을 모든 팀에게 배정" 기능을 직접적으로 요약하고 있어 변경의 목적과 범위를 명확히 전달합니다. 제목에 포함된 예시(관리자가 특별 미션을 배정하면 모든 팀에 TeamMission이 생성되며 startedAt으로 활성화/비활성화됨)는 raw_summary와 pr_objectives에서 설명된 주요 변경과 일치합니다.
Description Check ✅ Passed PR 설명은 요약과 주요 변경사항 체크리스트(새 기능 추가 등)를 포함해 핵심 동작을 전달하고 있으나 '## 🎯 변경 이유'와 '## 🔍 검토 포인트' 섹션이 비어 있고 문서·테스트 관련 항목이 미완료로 표시되어 있어 리뷰에 필요한 세부 정보가 일부 부족합니다. 템플릿의 기본 구조는 따르고 있어 대부분의 정보는 제공되었으므로 전반적으로 수용 가능하지만 추가 보완이 필요합니다.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SW-128

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

46-48: 미래 시작 미션까지 노출되는 버그: startedAt <= now 조건 추가 필요

현재 endedAt 기준만 필터링되어, 미래 시작 미션도 목록에 포함됩니다. 활성/비활성 요구사항에 맞게 startedAt 조건을 추가해 주세요.

-        return TeamMissionListResponse.from(teamMissionRepository
-                .findAllByTeamAndEndedAtGreaterThanEqual(team, LocalDateTime.now()));
+        LocalDateTime now = LocalDateTime.now();
+        return TeamMissionListResponse.from(teamMissionRepository
+                .findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(team, now, now));

레포지토리 메서드 추가(별도 파일):

List<TeamMission> findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(
        Team team, LocalDateTime startedAt, LocalDateTime endedAt);
🧹 Nitpick comments (7)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/exceptions/TeamMissionExceptions.java (1)

12-12: 동일 시각 허용 여부(>= vs >) 및 타임존(UTC/KST) 기준 명시
서비스 레이어(src/main/java/.../TeamMissionService.java L77–78)에서 endedAt.isAfter(startedAt)로 strict >검증 후INVALID_MISSION_TIME` 예외가 던져지고 있습니다. 동일 시각 허용 여부와 저장/비교 시 사용하는 타임존을 명확히 기재해 주세요.

src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (1)

27-33: 메서드 타입 파라미터화 및 페이징/정렬 도입 검토

  • AdminSeasonMissionServicegetSeasonSpecialMissions를 아래처럼 getSeasonMissionsByType(Season, MissionType)로 일반화해 재사용성을 높이세요.
  • 향후 데이터 규모 확대를 대비해 Pageable 인자를 추가하고, 리포지토리에 findAllBySeasonAndMissionType(Season, MissionType, Pageable) 시그니처를 병행 도입하세요. DB 복합 인덱스(season_id, mission_type)도 검토 바랍니다.
-    @Transactional(readOnly = true)
-    public MissionListResponse getSeasonSpecialMissions(Season season){
-
-        return MissionListResponse.fromSeasonMission(
-                seasonMissionRepository.findAllBySeasonAndMissionType(season, MissionType.SPECIAL)
-        );
-    }
+    @Transactional(readOnly = true)
+    public MissionListResponse getSeasonMissionsByType(Season season, MissionType type) {
+        return MissionListResponse.fromSeasonMission(
+                seasonMissionRepository.findAllBySeasonAndMissionType(season, type)
+        );
+    }
+
+    @Transactional(readOnly = true)
+    public MissionListResponse getSeasonSpecialMissions(Season season) {
+        return getSeasonMissionsByType(season, MissionType.SPECIAL);
+    }
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/request/CreateSpecialTeamMissionRequest.java (1)

13-19: 필드는 private으로, 스키마에 date-time 포맷/예시 명시

캡슐화와 문서 명확성을 위해 제안합니다.

-    @NotNull
-    @Schema(description = "시작 일시")
-    LocalDateTime startedAt;
+    @NotNull
+    @Schema(description = "시작 일시", type = "string", format = "date-time", example = "2025-09-01T09:00:00")
+    private LocalDateTime startedAt;
@@
-    @NotNull
-    @Schema(description = "종료 일시")
-    LocalDateTime endedAt;
+    @NotNull
+    @Schema(description = "종료 일시", type = "string", format = "date-time", example = "2025-09-01T18:00:00")
+    private LocalDateTime endedAt;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/entity/TeamMission.java (1)

30-33: 필드 접근 제어자 통일

엔티티 필드도 private으로 통일해 주세요.

-    @Column(nullable = false)
-    @NotNull
-    LocalDateTime startedAt;
+    @Column(nullable = false)
+    @NotNull
+    private LocalDateTime startedAt;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/response/TeamMissionResponse.java (1)

23-27: OpenAPI 명세 개선: date-time 포맷/예시 추가

클라이언트 스키마 가독성 향상.

-    @Schema(description = "시작 일시")
+    @Schema(description = "시작 일시", type = "string", format = "date-time", example = "2025-09-01T09:00:00")
     LocalDateTime startedAt;

-    @Schema(description = "종료 일시")
+    @Schema(description = "종료 일시", type = "string", format = "date-time", example = "2025-09-01T18:00:00")
     LocalDateTime endedAt;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

123-136: 랜덤 미션 startedAt이 시즌 시작으로 고정됨 — 의도 확인 필요

스케줄링 시점(now)에 활성화하려면 now 사용이 자연스럽습니다. 현재 로직은 시즌 시작부터 항상 활성 상태가 됩니다.

-        LocalDateTime startedAt = season.getStartedAt();
+        LocalDateTime startedAt = now; // 스케줄은 자정 실행이므로 일/주 시작 시각과 동일

의도적으로 시즌 시작을 사용한 것이라면 주석으로 근거를 남겨 주세요.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/controller/AdminTeamMissionController.java (1)

30-46: 중복 생성 응답 처리/문서화

중복 skip 로직을 적용한다면, 생성/스킵 개수를 반환하거나 207(Multi-Status)/200+요약 메시지 등으로 결과를 명확히 전달하는 방식을 고려해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7dea58e and b9367c5.

📒 Files selected for processing (10)
  • src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/controller/AdminSeasonMissionController.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/exception/SeasonMissionExceptions.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/mission/exception/MissionExceptions.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/controller/AdminTeamMissionController.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/request/CreateSpecialTeamMissionRequest.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/response/TeamMissionResponse.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/entity/TeamMission.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/exceptions/TeamMissionExceptions.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (4 hunks)
🔇 Additional comments (5)
src/main/java/com/github/kmu_shell_we/domain/mission/exception/MissionExceptions.java (1)

12-12: 메시지 표현 수정

-    INVALID_SPECIAL_MISSION_TYPE("MISSION_002", "SPECIAL 타입이 아닙니다."),
+    INVALID_SPECIAL_MISSION_TYPE("MISSION_002", "미션 타입이 SPECIAL이 아닙니다."),

해당 예외는 TeamMissionService의 타입 검증 가드에서만 사용되어 일관됩니다.

src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/exception/SeasonMissionExceptions.java (1)

13-13: INVALID_SEASON_MISSION 예외 사용 여부 확인 필요
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/exception/SeasonMissionExceptions.java에 정의만 있고 호출되는 곳이 없습니다. 실제 서비스/컨트롤러에서 사용되는지 확인 후, 미사용 시 제거하거나 예외를 던지는 로직을 추가해 주세요.

src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (1)

6-6: 필요한 enum 임포트 추가 확인됨

MissionType.SPECIAL 필터링을 위한 임포트가 정확합니다.

src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/controller/AdminSeasonMissionController.java (1)

35-42: DomainClassConverter 설정 검증 및 페이징 도입 검토

  • @PathVariable Season season이 DomainClassConverter 전제이므로, 프로젝트 전반에서 UUID→Season 엔티티 변환이 정상 작동하는지 확인 필요합니다.
  • 응답량 증가에 대비해 페이징(및 정렬) 쿼리 파라미터 도입을 고려해 주세요.
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/controller/AdminTeamMissionController.java (1)

33-35: Spring Data Web 자동 설정으로 DomainClassConverter 등록됨
Spring Boot에서 spring-boot-starter-webspring-boot-starter-data-jpa 의존성을 함께 사용하면 SpringDataWebAutoConfiguration을 통해 DomainClassConverter가 자동 등록되어
@PathVariable Season season, @PathVariable Mission mission 바인딩이 정상 동작합니다. 추가 설정 불필요합니다. (docs.spring.io, stackoverflow.com)

Likely an incorrect or invalid review comment.

BORB-CHOI and others added 4 commits September 1, 2025 16:09
* [SW-120] refactor(submission): 자신의 팀의 미션만 제출 및 팀 미션 제출 시 Mission ID가 아닌 TeamMission ID 받기

* [SW-120] refactor(item): 자신의 팀의 아이템만 뽑기 및 조회 가능

* [SW-120] refactor(submission): team이 teamMission을 갖고 있는지 검사 및 각 엔티티에서 List.of를 new ArrayList<>()로 변경

* [SW-120] refactor(submission): 미션 중복 제출 금지
* [SW-124] refactor(item): 아이템 뽑기 시 포인트 조회 및 차감

* [SW-124] chore(item): 예외 코드 수정
@BORB-CHOI BORB-CHOI requested review from arirang030 and removed request for son-daehyeon September 8, 2025 07:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

46-47: 미시작(미래) 미션이 함께 노출됩니다 — startedAt 조건 추가 필요

현재 쿼리는 endedAt >= now만 필터링하여 미래에 시작될 미션도 포함될 수 있습니다. startedAt <= now 조건을 함께 적용해주세요.

-        return TeamMissionListResponse.from(teamMissionRepository
-                .findAllByTeamAndEndedAtGreaterThanEqual(team, LocalDateTime.now()));
+        LocalDateTime now = LocalDateTime.now();
+        return TeamMissionListResponse.from(teamMissionRepository
+                .findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(team, now, now));

레포지토리에 대응 메서드가 없다면 추가가 필요합니다:
List<TeamMission> findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(Team team, LocalDateTime start, LocalDateTime end);

♻️ Duplicate comments (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

101-111: 중복 생성(멱등성) 보장 필요 — 동일 팀/미션/기간 중복 방지

엔드포인트가 반복 호출되면 동일 구간의 TeamMission이 누적 생성될 수 있습니다. 존재 여부(기간 겹침) 체크로 skip하거나 DB 유니크 제약을 권장합니다.

         List<TeamMission> teamMissions = new ArrayList<>();
         for (Team team : teams) {
+            boolean exists = teamMissionRepository
+                    .existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(
+                            team, mission, startedAt, endedAt);
+            if (exists) continue;
             teamMissions.add(TeamMission.builder()
                     .mission(mission)
                     .team(team)
                     .startedAt(startedAt)
                     .endedAt(endedAt)
                     .build());
         }

레포지토리 메서드 예시:
boolean existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(Team team, Mission mission, LocalDateTime start, LocalDateTime end);

DB 권장:

  • 유니크 인덱스 (team_id, mission_id, started_at, ended_at)
  • 조회용 인덱스 (team_id, started_at, ended_at)
🧹 Nitpick comments (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (4)

131-131: 랜덤(일/주간) 미션 startedAt은 now가 자연스럽습니다

활성/비활성 판정이 startedAt 기준이라면 시즌 시작 시각 대신 생성 시각(now)을 쓰는 편이 직관적입니다.

-        LocalDateTime startedAt = season.getStartedAt();
+        LocalDateTime startedAt = now;

Also applies to: 143-143


69-75: 서비스 계층 접근 제어 고려

컨트롤러에서만 권한을 제한했다면, 타 경로 호출 가능성을 차단하기 위해 서비스 메서드에도 역할 기반 @PreAuthorize 추가를 고려해주세요(예: 관리자 전용).


50-51: 크론 타임존 명시 제안

서버 지역/서머타임 영향 방지를 위해 @scheduled에 zone 속성 지정(예: Asia/Seoul 또는 UTC)을 권장합니다.

Also applies to: 58-60


69-112: 엣지 케이스 테스트 추가 제안

아래 케이스에 대한 단위/통합 테스트가 있으면 안정성이 올라갑니다.

  • endedAt <= startedAt
  • 시즌 경계 밖 시간
  • mission null 또는 SPECIAL 아님
  • 시즌에 팀 없음
  • 중복 호출 시 멱등성 유지

필요하시면 테스트 스켈레톤을 생성해 드릴게요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9367c5 and c74c7cb.

📒 Files selected for processing (2)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/request/CreateSpecialTeamMissionRequest.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/dto/request/CreateSpecialTeamMissionRequest.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (3)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (3)
  • Transactional (35-49)
  • Service (15-59)
  • Transactional (27-33)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)
  • Entity (13-36)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (1)
  • Transactional (50-66)
🔇 Additional comments (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (2)

88-96: 현재 시즌 제한 + SPECIAL 타입 검증 LGTM

현재 시즌이 아닐 때 차단, 미션 null 체크, SPECIAL 타입 강제 모두 적절합니다.


94-96: 특별 미션이 시즌에 등록된 미션인지 확인 필요 여부

비즈니스 규칙상 SPECIAL 미션도 해당 시즌(SeasonMission)에 등록되어 있어야 하는지 확인 부탁드립니다. 필요하다면 아래 검증을 추가해주세요.

+        seasonMissionRepository.findBySeasonAndMission(season, mission)
+                .orElseThrow(SeasonMissionExceptions.NOT_FOUND_SEASON_MISSION::toException);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/repository/TeamMissionRepository.java (1)

26-26: 시간창 검색 성능 대비 인덱스 권장

team + startedAt + endedAt 조합 검색이 빈번합니다. DB에 복합 인덱스 추가를 권장합니다.

예: (team_id, started_at, ended_at) 또는 사용 패턴에 맞춰 (team_id, started_at) + (team_id, ended_at).

또한, 스페셜 미션 멱등성 보완용 존재 여부 체크 메서드도 추가해 두세요.

+    boolean existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(
+            Team team, Mission mission, LocalDateTime start, LocalDateTime end);
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (2)

81-87: 시즌 경계 검증이 한쪽 경계(null)에서 무력화됩니다 — 각 경계를 독립 검증으로 변경

-        if (seasonStart != null && seasonEnd != null) {
-            if (startedAt.isBefore(seasonStart) || endedAt.isAfter(seasonEnd)) {
-                throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
-            }
-        }
+        if (seasonStart != null && startedAt.isBefore(seasonStart))
+            throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
+        if (seasonEnd != null && endedAt.isAfter(seasonEnd))
+            throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();

102-112: 멱등성: 동일 팀/미션/기간 중복 생성 방지

동시/반복 호출 시 중복 레코드가 생성될 수 있습니다. 존재 여부 검사 후 건너뛰거나(서비스), DB 유니크 인덱스로 보장하세요.

         List<TeamMission> teamMissions = new ArrayList<>();
         for (Team team : teams) {
-            teamMissions.add(TeamMission.builder()
+            boolean exists = teamMissionRepository
+                .existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(
+                    team, mission, startedAt, endedAt);
+            if (exists) continue;
+            teamMissions.add(TeamMission.builder()
                     .mission(mission)
                     .team(team)
                     .startedAt(startedAt)
                     .endedAt(endedAt)
                     .build());
         }

DB 측 유니크 인덱스 예: (team_id, mission_id, started_at, ended_at).

🧹 Nitpick comments (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/repository/TeamMissionRepository.java (1)

26-26: 활성 구간 조회 시그니처 단순화(+가독성 개선)

동일 시점을 두 번 전달하는 now, now1 네이밍이 혼란스럽습니다. JPQL BETWEEN을 써서 단일 파라미터로 의도를 드러내는 편이 명확합니다.

-    List<TeamMission> findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(Team team, LocalDateTime now, LocalDateTime now1);
+    @Query("SELECT tm FROM TeamMission tm WHERE tm.team = :team AND :asOf BETWEEN tm.startedAt AND tm.endedAt")
+    List<TeamMission> findActiveMissions(Team team, LocalDateTime asOf);

서비스 호출부도 다음과 같이 정리하세요(서비스 파일 참조).

src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (3)

46-49: 활성 미션 조회 메서드 간결화 + Clock 주입 제안

리포지토리 메서드를 findActiveMissions(team, now)로 단순화하고, 테스트 용이성과 TZ 일관성을 위해 Clock 주입을 고려하세요.

-        LocalDateTime now = LocalDateTime.now();
-        return TeamMissionListResponse.from(teamMissionRepository
-            .findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(team, now, now));
+        LocalDateTime now = LocalDateTime.now();
+        return TeamMissionListResponse.from(teamMissionRepository.findActiveMissions(team, now));

Clock 주입 예시(추가 변경):

// 필드
private final java.time.Clock clock;
// 사용
LocalDateTime now = LocalDateTime.now(clock);

132-146: 스케줄 미션 startedAt은 시즌 시작이 아니라 생성 시점(now)이 자연스럽습니다

startedAt = season.getStartedAt()는 의미가 넓어집니다. 실제 활성화 시점에 맞춰 now 사용을 권장합니다.

-        LocalDateTime startedAt = season.getStartedAt();
+        LocalDateTime startedAt = now;

70-113: 스페셜 미션이 시즌 미션에 속하는지 검증(정책 정합성 확인 필요)

스페셜 미션이 해당 시즌의 등록 미션(SeasonMission)인지 확인 로직이 없으면 정책 일탈이 가능합니다. 필요 시 seasonMissionRepository.findBySeasonAndMission(...)로 보강하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c74c7cb and a6dc8a9.

📒 Files selected for processing (2)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/repository/TeamMissionRepository.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/repository/TeamMissionRepository.java (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java (4)
  • Service (29-108)
  • now (52-52)
  • now (61-64)
  • Transactional (38-44)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)
  • Entity (13-36)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (3)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (3)
  • Transactional (35-49)
  • Transactional (27-33)
  • Service (15-59)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)
  • Entity (13-36)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java (1)
  • TeamMissionRepository (13-21)
🔇 Additional comments (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

70-113: 마이그레이션 파일에 team_mission.started_at·ended_at 컬럼 DDL 미발견
프로젝트 내 SQL/XML/YAML 마이그레이션에서 team_mission 테이블에 started_at, ended_at 컬럼 생성 또는 수정 스크립트가 없습니다. DB 스키마 반영 여부(또는 JPA auto-ddl 설정 사용 여부)를 확인하고, 필요 시 마이그레이션 파일을 추가해주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (1)

101-109: 중복 생성 방지(멱등성): 팀/미션/기간 겹침은 skip

엔드포인트 반복 호출 시 동일(또는 겹치는) 기간 팀미션이 누적 생성됩니다. 겹침 존재 시 생성을 건너뛰세요. DB 유니크 제약도 병행 권장.

-        for (Team team : teams) {
-            teamMissions.add(TeamMission.builder()
+        for (Team team : teams) {
+            boolean exists = teamMissionRepository
+                    .existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(
+                            team, mission, startedAt, endedAt);
+            if (exists) continue;
+            teamMissions.add(TeamMission.builder()
                     .mission(mission)
                     .team(team)
                     .startedAt(startedAt)
                     .endedAt(endedAt)
                     .build());
         }

레포지토리 메서드:

boolean existsByTeamAndMissionAndEndedAtAfterAndStartedAtBefore(
        Team team, Mission mission, LocalDateTime start, LocalDateTime end);

DB 인덱스(권장): unique(team_id, mission_id, started_at, ended_at) + (team_id, started_at, ended_at) 일반 인덱스.

🧹 Nitpick comments (5)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (5)

46-49: 활성 미션 조회 성능 인덱스 권장

팀별 활성 구간 조회가 빈번할 것이므로 (team_id, started_at, ended_at) 복합 인덱스를 추가하세요. started_at ≤ now AND ended_at ≥ now 패턴에 최적입니다.


46-46: 테스트 용이성: Clock 주입

LocalDateTime.now() 직접 호출 대신 Clock을 주입하면 시간 고정 테스트가 쉬워집니다.

-        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime now = LocalDateTime.now(clock);

(필드)

+    private final Clock clock;

(설정에서 Clock 빈 등록 필요)


94-96: SPECIAL 미션은 시즌에 등록되어 있는지도 검증 권장

시즌에 등록되지 않은 SPECIAL 미션이 배포될 수 있습니다. 일관성을 위해 SeasonMission 존재 여부를 체크하세요.

         if (mission.getType() != MissionType.SPECIAL)
             throw MissionExceptions.INVALID_SPECIAL_MISSION_TYPE.toException();
 
+        // 시즌에 해당 미션이 등록되어 있는지 확인
+        seasonMissionRepository.findBySeasonAndMission(season, mission)
+                .orElseThrow(SeasonExceptions.NOT_FOUND_SEASON_MISSION::toException);

131-145: 랜덤(일/주) 미션 startedAt 의미 보정

startedAt을 시즌 시작으로 고정하면 의미가 흐려집니다. 생성 시점을 startedAt으로 두는 편이 자연스럽고 응답/감사 로그 해석이 명확합니다.

-        LocalDateTime startedAt = season.getStartedAt();
+        LocalDateTime startedAt = now;
...
-                        .startedAt(startedAt)
+                        .startedAt(startedAt)

(주간 스케줄은 이미 자정에 실행되므로 now가 주차 시작을 충분히 대표합니다. 필요 시 월요일 00:00로 정규화하는 별도 계산도 고려하세요.)


152-156: 권한 체크 메서드 명확성(Nitpick)

teamCheck는 적절합니다. 다만 Admin 경로에서 별도 권한(ROLE_ADMIN) 우선 통과가 필요하면 조건을 분리해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6dc8a9 and 5d28bbc.

📒 Files selected for processing (1)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)
  • Entity (13-36)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java (1)
  • TeamMissionRepository (13-21)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (2)
  • Transactional (35-49)
  • Service (15-59)
src/main/java/com/github/kmu_shell_we/domain/mission/entity/Mission.java (1)
  • Entity (13-42)
🔇 Additional comments (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/service/TeamMissionService.java (2)

6-14: 필요 import 추가 LGTM

Mission/MissionExceptions, TeamMissionExceptions, TeamExceptions 추가가 본 파일 변경과 일관됩니다.


78-86: 시간 검증 로직 적절(LGTM)

startedAt/endedAt 유효성 및 시즌 경계 독립 검증이 반영되었습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/exceptions/TeamMissionExceptions.java (1)

1-1: 패키지 경로 불일치로 컴파일 실패 가능

파일 경로는 _team/_team_mission/...인데 package 선언은 _mission_team입니다. 아래처럼 정정 필요.

-package com.github.kmu_shell_we.domain.season._team._mission_team.exceptions;
+package com.github.kmu_shell_we.domain.season._team._team_mission.exceptions;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java (2)

1-1: 패키지 경로 불일치로 컴파일 실패 가능성 큼

파일 경로는 _team_mission/service인데 package는 _mission_team.service입니다. 일관되게 _team_mission로 변경하세요.

-package com.github.kmu_shell_we.domain.season._team._mission_team.service;
+package com.github.kmu_shell_we.domain.season._team._team_mission.service;

131-145: 일/주간 미션 startedAt 계산 부정확 및 시즌 범위 초과 가능성

  • startedAt을 시즌 시작 시각으로 고정하면 “생성 시점 이전부터 활성”처럼 보이는 사이드이펙트가 납니다.
  • endedAt이 시즌 종료 이후로 넘어갈 수 있습니다.
-LocalDateTime startedAt = season.getStartedAt();
-LocalDateTime endedAt = endCalculator.apply(now);
+LocalDateTime startedAt = now;
+LocalDateTime endedAt = endCalculator.apply(now);
+// 시즌 종료를 넘지 않도록 클램프
+LocalDateTime seasonEnd = season.getEndedAt();
+if (seasonEnd != null && endedAt.isAfter(seasonEnd)) {
+    endedAt = seasonEnd;
+}
+// 시간 역전 방지
+if (!endedAt.isAfter(startedAt)) {
+    throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
+}
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java (1)

1-1: 패키지 경로 불일치 수정 필요

파일 경로는 _team_mission/repository인데 package가 _mission_team.repository입니다. 컴파일 에러 원인입니다.

-package com.github.kmu_shell_we.domain.season._team._mission_team.repository;
+package com.github.kmu_shell_we.domain.season._team._team_mission.repository;
🧹 Nitpick comments (26)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/controller/TeamScheduleController.java (1)

34-34: users 쿼리파라미터 — 문서·바인딩 정합성 보강 권고

대상: src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/controller/TeamScheduleController.java
build.gradle에 spring-boot-starter-data-jpa 및 spring-boot-starter-web이 있어 Spring Data Web 자동설정(= @EnableSpringDataWebSupport)으로 DomainClassConverter가 기본 활성화될 가능성이 높음. (docs.spring.vmware.com)
리포지토리에서 @EnableSpringDataWebSupport 선언 및 SpringDataWebAutoConfiguration 제외 설정은 발견되지 않음(제공된 스크립트 결과).

  • 최소(권장): OpenAPI 문서와 실행을 일치시키기 위해 파라미터 name·required를 명시하세요. 적용 예:
-            @Parameter(description = "유저 ID 목록", array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))) @RequestParam List<User> users
+            @Parameter(name = "users", description = "유저 ID 목록", required = true,
+                    array = @ArraySchema(schema = @Schema(type = "string", format = "uuid")))
+            @RequestParam(name = "users") List<User> users
  • 검증 포인트: 프로젝트에서 @EnableWebMvc 또는 WebMvcConfigurationSupport로 MVC auto-config를 대체하는지 확인하세요 — 그럴 경우 DomainClassConverter가 등록되지 않을 수 있습니다. (stackoverflow.com)
  • 선택(권장): 성능·명확성 향상을 위해 컨트롤러는 List를 받고 서비스에서 일괄 조회(batch)로 List를 구성하는 방식 고려.
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/request/CreateSubmissionRequest.java (1)

11-14: 필드 검증 강화 제안: 공백/URL 형태 검증

현재 @NotNull만 있어 공백 문자열이 통과합니다. 최소 @NotBlank 추가를 권장합니다. (Hibernate Validator 사용 시 @URL도 고려)

-    @NotNull
+    @NotBlank
     @Schema(description = "팀 미션 인증 이미지 URL")
     String image;

외부 변경이 우려되면 필드명은 그대로 두고 검증만 보강하세요. (필요 시 추가 import)

import jakarta.validation.constraints.NotBlank;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/exception/ItemExceptions.java (1)

12-12: 포인트 부족 예외 추가 LGTM

아이템 뽑기 시 도메인 표현이 명확해졌습니다. 코드 포맷(언더스코어 vs 하이픈)은 레포 내 다른 예외 코드와 일관성만 유지하면 좋겠습니다.

src/main/java/com/github/kmu_shell_we/domain/mission/entity/Mission.java (1)

40-41: 컬렉션 가변화 LGTM — 연관관계 편의 메서드 추가 권장

@Setter(AccessLevel.NONE) + new ArrayList<>() 선택 좋습니다. 양방향 일관성을 위한 편의 메서드를 추가해 두면 안전합니다.

public void addSeasonMission(SeasonMission sm) {
    seasonMissions.add(sm);
    sm.setMission(this);
}
public void removeSeasonMission(SeasonMission sm) {
    seasonMissions.remove(sm);
    sm.setMission(null);
}
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java (1)

16-16: N+1 예방을 위한 fetch 전략 선택

목록 조회 시 Submission→연관 엔티티 접근이 잦다면 @EntityGraph 메서드 변형을 추가해 N+1을 방지하세요.

// 예시
@EntityGraph(attributePaths = {"teamMission"})
List<Submission> findAllByTeamMissionIn(List<TeamMission> teamMissions);
src/main/java/com/github/kmu_shell_we/domain/user/entity/User.java (1)

39-45: 컬렉션 가변화 LGTM — 양방향 편의 메서드로 일관성 보강

연관관계 편의 메서드를 두어 양쪽 세팅을 보장하는 것을 권장합니다.

public void addUserTeam(UserTeam ut) {
    userTeams.add(ut);
    ut.setUser(this);
}
public void addSchedule(Schedule s) {
    schedules.add(s);
    s.setUser(this);
}
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/entity/Submission.java (2)

18-21: FK 컬럼 네이밍/제약 재확인 + 일대일 제약 명시 권장

현재 컬럼명이 mission_team_id로 보이는데, 도메인 정리 후 team_mission 네이밍과 불일치합니다. 마이그레이션과 스키마를 확인해 주세요. 또한 일대일 보장을 위해 unique = true 명시를 권장합니다. 필요 시 optional = false도 함께 표현해 주세요.

아래와 같이 변경을 고려해 보세요(스키마에 맞춰 컬럼명은 최종 확인 필요).

-    @OneToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "mission_team_id", nullable = false)
+    @OneToOne(fetch = FetchType.LAZY, optional = false)
+    @JoinColumn(name = "team_mission_id", nullable = false, unique = true)
     TeamMission teamMission;

추가로, Hibernate 버전에 따라 @OneToOne(fetch = LAZY)가 실제 Lazy로 동작하지 않을 수 있습니다. 필요 시 @LazyToOne(LazyToOneOption.NO_PROXY) 검토 부탁드립니다.


22-24: 필드 의미 명확화 및 길이 제한

image가 URL이라면 imageUrl로 리네이밍 및 길이 제한을 권장합니다(긴 URL 대비).

-    @Column(nullable = false)
-    @NotNull
-    String image;
+    @Column(nullable = false, length = 2048)
+    @NotNull
+    String image; // imageUrl 권장
src/main/java/com/github/kmu_shell_we/domain/season/entity/Season.java (4)

43-45: 컬렉션 불변 교체 방지 설정 👍 (+ 양방향 고정 헬퍼 메서드 제안)

@Setter(AccessLevel.NONE)과 가변 리스트 초기화는 JPA 관점에서 안전합니다. 양방향 연관 고정 편의를 위해 헬퍼 메서드 추가를 권장합니다.

public void addTeam(Team team) {
    team.setSeason(this);
    this.teams.add(team);
}

48-50: 필드명 seasonMissions 정리 👍 (+ 헬퍼 메서드 제안)

네이밍 일관성이 좋아졌습니다. 연관 고정을 위한 편의 메서드도 함께 두면 사용성이 좋아집니다.

public void addSeasonMission(SeasonMission sm) {
    sm.setSeason(this);
    this.seasonMissions.add(sm);
}

51-56: 경계 포함 여부 명확화

현재 구현은 시작/종료 시각과 정확히 같은 순간을 “현재 시즌 아님”으로 처리합니다. 일반적으로 시작 포함, 종료 미포함([startedAt, endedAt))이 직관적입니다. 요구사항에 맞게 확인 후 아래와 같이 조정 권장.

-        return now.isAfter(startedAt) && now.isBefore(endedAt);
+        // 시작 포함, 종료 미포함: [startedAt, endedAt)
+        return !now.isBefore(startedAt) && now.isBefore(endedAt);

33-40: 시간 기반 조회 대비 인덱스/타임존 정책 점검

startedAt/endedAt로 “현재 시즌” 조회가 잦다면 DB 인덱스를 고려해 주세요. 또한 서버/DB 타임존 정책(예: UTC 고정) 일관성 검증을 권장합니다.

예시:

  • JPA: @Table(indexes = @Index(name = "idx_season_period", columnList = "startedAt,endedAt"))
  • 운영: 애플리케이션/DB 타임존 UTC 통일 후 표시 단계에서 지역 시간 변환
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionResponse.java (1)

17-18: 필드 명칭 미세 개선 제안

응답 스키마에서 URL임이 명확하도록 imageimageUrl 제안. API 문서와도 용어를 맞추면 좋습니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/entity/Team.java (3)

39-41: JPA 안전한 컬렉션 초기화 👍 (+ 연관 고정 헬퍼 권장)

List.of() 제거와 @Setter(AccessLevel.NONE) 적용 좋습니다. 양방향 고정을 위한 편의 메서드 추가를 권장합니다.

public void addUserTeam(UserTeam ut) {
    ut.setTeam(this);
    this.userTeams.add(ut);
}

44-46: 동일 패턴 일관 적용 👍

items에도 동일 전략 적용 OK. 편의 메서드도 함께 두면 일관성↑

public void addItem(Item item) {
    item.setTeam(this);
    this.items.add(item);
}

33-35: 조회 성능 관점 인덱스 고려(다대일 FK)

팀을 시즌별로 자주 조회한다면 season_id 인덱스를 확인/추가해 주세요.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java (1)

51-69: 스케줄러 타임존 명시 필요

배포 환경 JVM 기본 타임존에 의존하면 일 배치/주 배치 시각이 달라질 수 있습니다. 서비스 기준 TZ(예: Asia/Seoul)로 고정하세요.

-@Scheduled(cron = "0 0 0 * * ?")
+@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul")
 public void assignDailyMission() { ... }

-@Scheduled(cron = "0 0 0 ? * MON")
+@Scheduled(cron = "0 0 0 ? * MON", zone = "Asia/Seoul")
 public void assignWeeklyMission() { ... }
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)

13-20: 조회 최적화를 위한 인덱스 권장

활성 미션 조회가 (team, startedAt<=now, endedAt>=now)에 의존하므로 복합 인덱스를 권장합니다.

 @Entity
+@Table(indexes = {
+        @Index(name = "idx_team_started_ended", columnList = "team_id, startedAt, endedAt")
+})
 public class TeamMission extends BaseSchema {
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionResponse.java (1)

23-27: 시간 필드의 타임존 명시 또는 타입 개선 필요

LocalDateTime는 타임존이 없어 클라이언트별 해석 차이가 날 수 있습니다. API에서 혼동 방지를 위해 OffsetDateTime/Instant 사용을 권장하거나 최소한 스키마에 TZ를 명시해 주세요. 예시처럼 포맷/예제를 추가하면 문서 가독성이 좋아집니다.

-    @Schema(description = "시작 일시")
+    @Schema(description = "시작 일시 (KST, yyyy-MM-dd'T'HH:mm:ss)", example = "2025-09-01T09:00:00")
     LocalDateTime startedAt;

-    @Schema(description = "종료 일시")
+    @Schema(description = "종료 일시 (KST, yyyy-MM-dd'T'HH:mm:ss)", example = "2025-09-01T23:59:59")
     LocalDateTime endedAt;
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java (3)

81-83: 시간 소스 주입으로 테스트/일관성 개선

LocalDateTime.now() 대신 Clock을 주입해 사용하면 테스트 용이성과 일관성이 높아집니다.

-                        .findCurrentMission(team, LocalDateTime.now())
+                        .findCurrentMission(team, LocalDateTime.now(clock))

추가(필드/임포트):

+import java.time.Clock;
 ...
     private final TeamRepository teamRepository;
     private final UserTeamRepository userTeamRepository;
     private final ItemRepository itemRepository;
     private final TeamMissionRepository teamMissionRepository;
     private final SeasonMissionRepository seasonMissionRepository;
+    private final Clock clock;

85-94: MISSION_CHANGE 시 동일 미션으로 변경되는 경우 방지 권장

현재 로직은 기존 미션과 동일 미션을 뽑아도 그대로 세팅됩니다. 사용자 체감상 “변경”이 무효가 될 수 있어 제외 조건을 추가해 주세요(리포지토리에서 제외 조회 또는 재추첨).

-                Mission after = seasonMissionRepository.findRandomDailyMissionBySeason(season)
+                Mission after = seasonMissionRepository.findRandomDailyMissionBySeasonExcluding(season, current.getMission().getId())
                         .orElseThrow(SeasonMissionExceptions.NOT_FOUND_SEASON_MISSION::toException)
                         .getMission();

55-56: 매직 넘버 상수화

100(뽑기 비용), 50(감점치) 하드코딩을 상수로 올려두면 유지보수 용이합니다.

+    private static final int DRAW_COST = 100;
+    private static final int SCORE_DEDUCTION_POINT = 50;
 ...
-        if (team.getPoint() < 100) {
+        if (team.getPoint() < DRAW_COST) {
             throw ItemExceptions.NOT_ENOUGH_POINT.toException();
         }
-        team.setPoint(team.getPoint() - 100);
+        team.setPoint(team.getPoint() - DRAW_COST);
 ...
-                otherTeam.setExperience(Math.max(0, otherTeam.getExperience() - 50));
+                otherTeam.setExperience(Math.max(0, otherTeam.getExperience() - SCORE_DEDUCTION_POINT));
 ...
-                yield ScoreDeductionResponse.from(otherTeam, 50);
+                yield ScoreDeductionResponse.from(otherTeam, SCORE_DEDUCTION_POINT);

Also applies to: 100-103, 107-116

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (2)

33-39: 정렬 추가 제안

목록 응답은 최신 제출 우선 등 정렬 기준을 명시하면 UX가 좋아집니다(예: createdAt desc).


45-48: 404로 매핑 고려

단건 조회에서 미존재 시 null 대신 404가 더 명확할 수 있습니다. 컨트롤러/ApiResponse 정책에 맞춰 검토 부탁드립니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/controller/SubmissionController.java (2)

55-66: POST는 201 Created 권장

리소스 생성 응답 코드를 201로 명시하면 API 의미가 분명합니다.

+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
 ...
     @PostMapping("/team_mission/{teamMission}/submissions")
     @Operation(summary = "팀 미션 제출")
+    @ResponseStatus(HttpStatus.CREATED)
     public ApiResponse<SubmissionResponse> submitMission(

58-66: 메서드명 의미 일치화 제안

submitMissionsubmitSubmission이 컨트롤러 역할과 더 잘 맞습니다. 공개 API가 아니라면 리네이밍 고려 바랍니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d28bbc and 3e74ce1.

📒 Files selected for processing (25)
  • src/main/java/com/github/kmu_shell_we/domain/mission/entity/Mission.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/controller/ItemController.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/exception/ItemExceptions.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java (3 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/_submission_mission_team/repository/SubmissionRepository.java (0 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/_submission_mission_team/service/SubmissionService.java (0 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/controller/TeamScheduleController.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/controller/SubmissionController.java (3 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/request/CreateSubmissionRequest.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionListResponse.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionResponse.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/entity/Submission.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/TeamMissionController.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionListResponse.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionResponse.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/exceptions/TeamMissionExceptions.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java (1 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java (5 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/entity/Team.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/season/entity/Season.java (2 hunks)
  • src/main/java/com/github/kmu_shell_we/domain/user/entity/User.java (2 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/_submission_mission_team/repository/SubmissionRepository.java
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/_submission_mission_team/service/SubmissionService.java
🧰 Additional context used
🧬 Code graph analysis (13)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (1)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java (1)
  • Service (33-124)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/repository/UserTeamRepository.java (1)
  • UserTeamRepository (12-17)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/repository/ItemRepository.java (1)
  • ItemRepository (10-13)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/exceptions/TeamMissionExceptions.java (5)
src/main/java/com/github/kmu_shell_we/domain/mission/exception/MissionExceptions.java (1)
  • Getter (7-16)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/exception/SeasonMissionExceptions.java (1)
  • Getter (7-17)
src/main/java/com/github/kmu_shell_we/domain/season/_team/exception/TeamExceptions.java (1)
  • Getter (7-16)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/exception/UserTeamExceptions.java (1)
  • Getter (7-17)
src/main/java/com/github/kmu_shell_we/domain/user/_schedule/exception/ScheduleExceptions.java (1)
  • Getter (7-16)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/controller/ItemController.java (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/controller/SubmissionController.java (1)
  • MemberGuard (23-67)
src/main/java/com/github/kmu_shell_we/domain/season/_team/controller/TeamController.java (1)
  • MemberGuard (23-59)
src/main/java/com/github/kmu_shell_we/global/security/authentication/UserAuthentication.java (1)
  • RequiredArgsConstructor (14-52)
src/main/java/com/github/kmu_shell_we/domain/user/controller/UserController.java (1)
  • MemberGuard (15-28)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/exception/ItemExceptions.java (3)
src/main/java/com/github/kmu_shell_we/domain/season/_team/exception/TeamExceptions.java (1)
  • Getter (7-16)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/exception/UserTeamExceptions.java (1)
  • Getter (7-17)
src/main/java/com/github/kmu_shell_we/domain/auth/exception/AuthExceptions.java (1)
  • Getter (7-18)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java (4)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/AdminItemService.java (2)
  • Service (12-24)
  • Transactional (18-23)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/service/AdminUserTeamService.java (1)
  • Service (14-41)
src/main/java/com/github/kmu_shell_we/domain/season/_team/service/TeamService.java (1)
  • Service (17-47)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/service/TeamScheduleService.java (1)
  • Service (20-42)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java (3)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/controller/AdminSeasonMissionController.java (3)
  • AdminGuard (17-65)
  • PostMapping (44-52)
  • GetMapping (35-42)
src/main/java/com/github/kmu_shell_we/domain/season/_team/controller/AdminTeamController.java (1)
  • AdminGuard (18-66)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (1)
  • Service (15-59)
src/main/java/com/github/kmu_shell_we/domain/season/entity/Season.java (3)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/entity/SeasonMission.java (1)
  • Entity (9-26)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/service/AdminSeasonMissionService.java (1)
  • Service (15-59)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/repository/SeasonMissionRepository.java (1)
  • Nonnull (23-26)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/controller/TeamScheduleController.java (2)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/service/TeamScheduleService.java (3)
  • Service (20-42)
  • verifyUserTeam (36-41)
  • Transactional (29-34)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/dto/response/TeamScheduleResponse.java (1)
  • Data (9-15)
src/main/java/com/github/kmu_shell_we/domain/user/entity/User.java (7)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/entity/UserTeam.java (1)
  • Entity (10-29)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/dto/response/UserSchedulePair.java (1)
  • Data (16-37)
src/main/java/com/github/kmu_shell_we/domain/user/_schedule/entity/Schedule.java (1)
  • Entity (15-60)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/service/AdminUserTeamService.java (1)
  • Service (14-41)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/repository/UserTeamRepository.java (1)
  • UserTeamRepository (12-17)
src/main/java/com/github/kmu_shell_we/domain/user/_schedule/repository/ScheduleRepository.java (1)
  • findAllByUser (12-12)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/service/TeamScheduleService.java (1)
  • Service (20-42)
src/main/java/com/github/kmu_shell_we/domain/mission/entity/Mission.java (2)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/entity/SeasonMission.java (1)
  • Entity (9-26)
src/main/java/com/github/kmu_shell_we/domain/mission/_season_mission/repository/SeasonMissionRepository.java (1)
  • Nonnull (23-26)
src/main/java/com/github/kmu_shell_we/domain/season/_team/entity/Team.java (3)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/entity/UserTeam.java (1)
  • Entity (10-29)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_user_team/repository/UserTeamRepository.java (1)
  • UserTeamRepository (12-17)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/repository/ItemRepository.java (1)
  • ItemRepository (10-13)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionResponse.java (3)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionResponse.java (1)
  • Data (9-27)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionListResponse.java (1)
  • Data (10-22)
src/main/java/com/github/kmu_shell_we/domain/mission/dto/response/MissionResponse.java (1)
  • Data (12-41)
🔇 Additional comments (16)
src/main/java/com/github/kmu_shell_we/domain/season/_team/_schedule/controller/TeamScheduleController.java (1)

12-12: Swagger ArraySchema import 추가 적합

문서 스키마 명확화에 필요한 import가 정확히 추가되었습니다. 별도 이슈 없습니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/exceptions/TeamMissionExceptions.java (1)

12-12: 새 예외 코드 추가 LGTM — 시간 검증 전역 적용 여부 확인

INVALID_MISSION_TIME 도입은 적절합니다. startedAt < endedAt, 시간대(UTC/Asia/Seoul) 처리, null 허용 정책 등에 대해 서비스/밸리데이터가 이 코드를 일관되게 던지는지 확인 부탁드립니다. 테스트도 추가해 주세요.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/request/CreateSubmissionRequest.java (1)

1-1: 패키지 이동 LGTM — 잔존 참조 재검증 필요

입력하신 rg 명령이 출력 없이 종료되어 잔존 참조 유무를 확정할 수 없습니다. 컨트롤러/서비스/테스트에서 새 FQN으로 모두 갱신되었는지 직접 확인하세요.

# 모든 참조(심볼) 검색
rg -n --hidden -S '\bCreateSubmissionRequest\b'

# 새 패키지의 정확한 import 검색 (파일의 package 선언 기준)
rg -nP 'import\s+com\.github\.kmu_shell_we\.domain\.season\._team\._team_mission\._submission\.dto\.request\.CreateSubmissionRequest' -S

# 폭넓은 FQN 검색(오타나 이전 패키지 포착용)
rg -nP 'com\.github\.kmu_shell_we\.domain\.season\.[\w._]*CreateSubmissionRequest' -S
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionListResponse.java (1)

1-1: 패키지/임포트 정리 LGTM — toList() 사용 확인 완료 (JDK 21 타겟)
build.gradle의 java.toolchain이 JavaLanguageVersion.of(21)로 설정되어 있어 Stream#toList() 사용이 안전합니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionListResponse.java (1)

1-1: 패키지/임포트 정리 LGTM

변경 사항 문제없습니다.

Also applies to: 3-3

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/entity/Submission.java (1)

1-3: 패키지/임포트 리네이밍 적절

네이밍 정리가 일관성을 높입니다. 추가 동작 변경 없어서 영향도 낮습니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/TeamMissionController.java (1)

1-5: 패키지/임포트 리팩터링 적절

네임스페이스 정돈으로 가독성/탐색성이 좋아졌습니다. 컨트롤러 시그니처 변화 없음 확인했습니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/dto/response/SubmissionResponse.java (1)

1-4: 패키지 정리 LGTM

의존 경로 일관화 OK. 동작 변경 없음.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java (2)

46-49: 활성 구간 조회 로직 전환 LGTM

단일 now로 startedAt/endedAt 윈도우 필터링하는 변경 적절합니다.


152-156: 팀 접근 체크 간결하고 명확함

현재 구현(내 팀 소속 여부 확인) 적절합니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/controller/ItemController.java (1)

31-36: 인증 사용자 컨텍스트 전달 변경 LGTM

@AuthenticationPrincipal 연결과 서비스 인자 업데이트 일관적입니다. 컨트롤러 단에서 추가 검증은 서비스 canAccess 로 위임된 것으로 보아 현재 수준 적절합니다.

Also applies to: 41-46

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/entity/TeamMission.java (1)

30-33: startedAt 필드 추가 타당

활성 구간 윈도우 쿼리를 위해 @NotNull startedAt 추가 적절합니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/dto/response/TeamMissionResponse.java (1)

31-36: 매핑 LGTM

엔티티 → DTO 변환에서 startedAt/endedAt 전달이 정확합니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java (1)

118-123: 접근 제어 LGTM

팀 소속·시즌 동시 검증으로 권한 경계가 명확합니다.

src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (1)

53-64: 활성 팀미션 검증 추가 필요

File: src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java (lines 53-64)

        submissionRepository.findByTeamMission(teamMission).ifPresent(submission -> {
            throw TeamMissionExceptions.ALREADY_SUBMITTED.toException();
        });

        Submission submission = submissionRepository.save(
                Submission.builder()
                        .teamMission(teamMission)
                        .image(request.getImage())
                        .build()
        );

제안된 패치:

+        // 활성 팀 미션인지 확인 (현재 시간 기준)
+        var now = java.time.LocalDateTime.now();
+        var current = teamMissionRepository.findCurrentMission(team, now)
+                .orElseThrow(TeamMissionExceptions.NOT_FOUND_TEAM_MISSION::toException);
+        if (!current.getId().equals(teamMission.getId())) {
+            throw TeamMissionExceptions.NOT_FOUND_TEAM_MISSION.toException();
+        }

확인 요청:

  • teamMissionRepository.findCurrentMission의 활성 조건이 startedAt/endedAt 기준으로 올바르게 정의되어 있는지(특히 endedAt 포함 여부) 확인.
  • 제출 정책이 "활성 상태인 팀 미션에만 제출 허용"과 일치하는지(비활성 미션에 대한 제출 차단) 확인.
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/controller/SubmissionController.java (1)

43-53: 컨트롤러 연계 LGTM

User 주입 및 TeamMission 기반 라우팅 전환이 서비스 변경과 일관됩니다.

Comment on lines +54 to 61
// 0. 포인트 조회 및 차감
if (team.getPoint() < 100) {

throw ItemExceptions.NOT_ENOUGH_POINT.toException();
}

team.setPoint(team.getPoint() - 100);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

동시성 경쟁으로 포인트 음수/과차감 가능성 높음

현재 team.getPoint() 검사 후 차감까지 원자성이 보장되지 않아 동시에 호출되면 둘 다 통과해 과차감이 발생할 수 있습니다. DB 레벨에서 원자적으로 갱신하거나 행 잠금을 적용해 주세요.

두 가지 중 한 가지로 보완을 권장합니다.

  • 원자적 업데이트 쿼리
-        if (team.getPoint() < 100) {
-            throw ItemExceptions.NOT_ENOUGH_POINT.toException();
-        }
-        team.setPoint(team.getPoint() - 100);
+        int updated = teamRepository.decrementPointIfEnough(team.getId(), 100); // UPDATE team SET point = point - 100 WHERE id = :id AND point >= 100
+        if (updated == 0) {
+            throw ItemExceptions.NOT_ENOUGH_POINT.toException();
+        }
  • 또는 PESSIMISTIC_WRITE 잠금(대안)
    • TeamRepository@Lock(PESSIMISTIC_WRITE) 메서드 추가 후 서비스에서 재조회하여 차감.
    • 혹은 Team 엔티티에 @Version 추가(낙관적 잠금) + 재시도 로직.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_item/service/ItemService.java
around lines 54-61 the current check-then-update on team.getPoint() is not
atomic and allows race conditions; fix it by implementing an atomic DB-side
update or a row lock: Option A (recommended): add a TeamRepository method with
@Modifying and a JPQL/SQL update like "update Team t set t.point = t.point - 100
where t.id = :id and t.point >= 100" that returns the number of rows updated; in
the service call that method and throw NOT_ENOUGH_POINT if updated count is 0.
Option B (alternative): add a repository finder annotated with
@Lock(PESSIMISTIC_WRITE) to re-load the Team inside a transactional method, then
perform the point check and subtract and save; or add @Version to Team and
implement an optimistic retry loop catching OptimisticLockException — implement
one of these approaches and remove the non-atomic getPoint() then setPoint(...)
sequence.


public interface SubmissionRepository extends JpaRepository<Submission, UUID> {

Optional<Submission> findByTeamMission(TeamMission teamMission);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

단건 조회는 1:1 제약 전제 — DB 유니크 제약 추가 권장

findByTeamMission이 Optional 단건을 반환하므로 Submission:TeamMission이 1:1임을 암시합니다. 스키마에 유니크 제약을 추가해 데이터 정합성을 보장하세요.

예) Submission 엔티티

@Table(
  uniqueConstraints = @UniqueConstraint(columnNames = "team_mission_id")
)

또는 레포에 존재 여부 체크 추가:

 public interface SubmissionRepository extends JpaRepository<Submission, UUID> {
     Optional<Submission> findByTeamMission(TeamMission teamMission);
+    boolean existsByTeamMission(TeamMission teamMission);
 }

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo root: $(pwd) ==="
echo

echo "=== Locate SubmissionRepository.java ==="
rg -n --hidden --glob 'src/**' 'SubmissionRepository\.java' -S || true
echo

REPO_PATH="src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java"
if [ -f "$REPO_PATH" ]; then
  echo "=== Showing $REPO_PATH ==="
  sed -n '1,240p' "$REPO_PATH"
else
  echo "SubmissionRepository not found at the expected path. Searching for interfaces named SubmissionRepository..."
  rg -n --hidden --glob 'src/**' 'interface\s+SubmissionRepository\b' -S || true
  for f in $(rg -n --hidden --glob 'src/**' 'interface\s+SubmissionRepository\b' -S | cut -d: -f1 | sort -u); do
    echo "---- $f ----"
    sed -n '1,240p' "$f"
  done
fi

echo
echo "=== Find Submission entity files ==="
rg -n --hidden --glob 'src/**' '\bclass\s+Submission\b' -S || true
for f in $(rg -n --hidden --glob 'src/**' '\bclass\s+Submission\b' -S | cut -d: -f1 | sort -u); do
  echo "---- $f ----"
  sed -n '1,320p' "$f"
done

echo
echo "=== Search for @Table uniqueConstraints / team_mission_id / @Column(name = \"team_mission_id\") ==="
rg -n --hidden --glob 'src/**' 'uniqueConstraints|team_mission_id|@Table|@Column\s*\(\s*name\s*=\s*["'\'' ]*team_mission_id' -S || true

echo
echo "=== Search for existsByTeamMission in repo ==="
rg -n --hidden --glob 'src/**' 'existsByTeamMission' -S || true

Length of output: 2569


DB 유니크 제약 추가 필요 — Submission:TeamMission(1:1) 관계 보장

findByTeamMission이 Optional을 반환하고 Submission 엔티티는 @OnetoOne@JoinColumn(name = "mission_team_id", nullable = false)으로 선언되어 있지만 DB 수준의 unique 제약이 없어 같은 TeamMission을 여러 Submission이 참조할 수 있습니다. DB 유니크 제약을 추가하거나 JPA에 unique=true를 명시하세요.

  • 엔티티 수정 (src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/entity/Submission.java)

    • 옵션 A: @joincolumn에 unique 추가
      @joincolumn(name = "mission_team_id", nullable = false, unique = true)
    • 옵션 B: 클래스 레벨로 유니크 제약 추가
      @table(uniqueConstraints = @UniqueConstraint(columnNames = {"mission_team_id"}))
  • 레포지토리 보완 (src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java)

    • 존재 여부 검사 메서드 추가:
      boolean existsByTeamMission(TeamMission teamMission);
🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/repository/SubmissionRepository.java
around line 14 and the Submission entity file, add a DB-level unique constraint
for the mission_team_id to enforce the 1:1 relationship and add an existence
check in the repository: update the Submission entity either by marking the
@JoinColumn(name = "mission_team_id", nullable = false, unique = true) or add a
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"mission_team_id"}))
at class level, then in SubmissionRepository add a boolean
existsByTeamMission(TeamMission teamMission) method; this ensures the database
prevents multiple Submissions for the same TeamMission and lets callers cheaply
check existence.

Comment on lines +41 to +43
@Transactional(readOnly = true)
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
public SubmissionResponse getSubmission(User user, Season season, Team team, TeamMission teamMission) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SpEL에서 엔티티 동등성 비교는 ID로 비교 권장

프록시/영속성 컨텍스트 차이로 객체 동등성 비교가 실패할 수 있습니다. ID 기반 비교로 바꿔주세요.

-    @PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
+    @PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team.id == #team.id")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional(readOnly = true)
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
public SubmissionResponse getSubmission(User user, Season season, Team team, TeamMission teamMission) {
@Transactional(readOnly = true)
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team.id == #team.id")
public SubmissionResponse getSubmission(User user, Season season, Team team, TeamMission teamMission) {
🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java
around lines 41-43, the SpEL expression compares entity instances which can fail
with proxies or different persistence contexts; change the expression to compare
IDs (e.g., replace "#teamMission.team == #team" with "#teamMission.team.id ==
#team.id") so authorization uses ID-based equality instead of object identity.

Comment on lines +50 to +52
@Transactional
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
public SubmissionResponse submitSubmission(User user, Season season, Team team, TeamMission teamMission, CreateSubmissionRequest request) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동일: ID 비교로 정정

-    @PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
+    @PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team.id == #team.id")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team == #team")
public SubmissionResponse submitSubmission(User user, Season season, Team team, TeamMission teamMission, CreateSubmissionRequest request) {
@Transactional
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and #teamMission.team.id == #team.id")
public SubmissionResponse submitSubmission(User user, Season season, Team team, TeamMission teamMission, CreateSubmissionRequest request) {
🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java
around lines 50 to 52, the @PreAuthorize expression compares entity objects
(#teamMission.team == #team) which should be changed to compare their
identifiers; update the expression to compare IDs (for example:
@PreAuthorize("@submissionService.canAccessSubmission(#user, #season, #team) and
#teamMission.team.id == #team.id") or the correct ID property names used in your
models) so the authorization check uses ID equality instead of object reference
equality.

Comment on lines +54 to +57
submissionRepository.findByTeamMission(teamMission).ifPresent(submission -> {
throw TeamMissionExceptions.ALREADY_SUBMITTED.toException();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복 제출 경쟁 상태: DB 유니크 제약과 예외 변환 필요

사전 조회 → 저장 사이 경쟁으로 중복 레코드가 생성될 수 있습니다. submission.team_mission에 유니크 인덱스를 추가하고, 저장 시 무결성 위반을 잡아 ALREADY_SUBMITTED로 변환하세요.

-        submissionRepository.findByTeamMission(teamMission).ifPresent(submission -> {
-            throw TeamMissionExceptions.ALREADY_SUBMITTED.toException();
-        });
-
-        Submission submission = submissionRepository.save(
-                Submission.builder()
-                        .teamMission(teamMission)
-                        .image(request.getImage())
-                        .build()
-        );
-
-        return SubmissionResponse.from(submission);
+        submissionRepository.findByTeamMission(teamMission).ifPresent(s -> {
+            throw TeamMissionExceptions.ALREADY_SUBMITTED.toException();
+        });
+        try {
+            Submission submission = submissionRepository.save(
+                    Submission.builder()
+                            .teamMission(teamMission)
+                            .image(request.getImage())
+                            .build()
+            );
+            return SubmissionResponse.from(submission);
+        } catch (org.springframework.dao.DataIntegrityViolationException e) {
+            // 유니크 제약 위반 → 중복 제출
+            throw TeamMissionExceptions.ALREADY_SUBMITTED.toException();
+        }

원하시면 DDL(유니크 인덱스) 패치 스니펫도 드리겠습니다.

Also applies to: 58-66

🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/_submission/service/SubmissionService.java
around lines 54-57 (and also apply same change to 58-66), the current pre-check
findByTeamMission(...) can race with concurrent saves and allow duplicate
submissions; add a DB-level unique index on submission.team_mission (create a
migration / DDL to add UNIQUE constraint on the team_mission column) and keep or
remove the pre-check but crucially wrap repository.save(...) in a try/catch that
catches the persistence integrity exception (e.g.,
org.springframework.dao.DataIntegrityViolationException or
javax.persistence.ConstraintViolationException depending on stack) and translate
that exception into TeamMissionExceptions.ALREADY_SUBMITTED.toException();
ensure you only translate constraint-violation cases and rethrow other
exceptions.

@@ -0,0 +1,47 @@
package com.github.kmu_shell_we.domain.season._team._mission_team.controller;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

패키지 경로 불일치로 컴파일 불가

디렉터리는 _team_mission/controller인데 package는 _mission_team.controller입니다. _team_mission로 정정하세요.

-package com.github.kmu_shell_we.domain.season._team._mission_team.controller;
+package com.github.kmu_shell_we.domain.season._team._team_mission.controller;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package com.github.kmu_shell_we.domain.season._team._mission_team.controller;
package com.github.kmu_shell_we.domain.season._team._team_mission.controller;
🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_mission_team/controller/AdminTeamMissionController.java
around line 1, the package declaration does not match the directory (directory
is _team_mission/controller but package is _mission_team.controller); update the
package declaration to use
com.github.kmu_shell_we.domain.season._team._mission.controller so it matches
the directory structure and restores successful compilation.

Comment on lines +3 to +9
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.season._team._mission_team.dto.request.CreateSpecialTeamMissionRequest;
import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
import com.github.kmu_shell_we.domain.season._team._mission_team.service.TeamMissionService;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.global.response.ApiResponse;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

이전 패키지 import 사용 중

엔티티/서비스/DTO import를 _team_mission로 교체하세요.

-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.request.CreateSpecialTeamMissionRequest;
-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
-import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
-import com.github.kmu_shell_we.domain.season._team._mission_team.service.TeamMissionService;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.request.CreateSpecialTeamMissionRequest;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
+import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
+import com.github.kmu_shell_we.domain.season._team._team_mission.service.TeamMissionService;

Also applies to: 19-26, 30-46


🏁 Script executed:

#!/bin/bash
# 컨트롤러에서 _mission_team import 남아있는지 재확인
rg -n 'import .+_mission_team\.' src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller

Length of output: 858


Import 경로 수정 필요 — _mission_team_team_mission

AdminTeamMissionController 파일에 아직 _mission_team 패키지 import(라인 4–7)가 남아 있습니다. 아래 대체로 변경하세요.

-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.request.CreateSpecialTeamMissionRequest;
-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
-import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
-import com.github.kmu_shell_we.domain.season._team._mission_team.service.TeamMissionService;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.request.CreateSpecialTeamMissionRequest;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
+import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
+import com.github.kmu_shell_we.domain.season._team._team_mission.service.TeamMissionService;

파일: src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java (라인 4–7)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.season._team._mission_team.dto.request.CreateSpecialTeamMissionRequest;
import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
import com.github.kmu_shell_we.domain.season._team._mission_team.service.TeamMissionService;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.global.response.ApiResponse;
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.season._team._team_mission.dto.request.CreateSpecialTeamMissionRequest;
import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
import com.github.kmu_shell_we.domain.season._team._team_mission.service.TeamMissionService;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.global.response.ApiResponse;
🤖 Prompt for AI Agents
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java
(lines 4-7): 현재 import 경로에 `_mission_team` 패키지를 참조하고 있으므로,
CreateSpecialTeamMissionRequest, TeamMissionListResponse, TeamMission,
TeamMissionService 네 개의 import 경로를 `_mission_team` 대신 `_team_mission`으로 수정하여 올바른
패키지로 가져오도록 변경하세요.

Comment on lines +26 to 27
List<TeamMission> findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(Team team, LocalDateTime now, LocalDateTime now1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

중복 생성 방지용 exists 쿼리 추가 제안

스페셜 미션 생성 시 겹치는 기간 중복 생성을 막기 위한 파생 쿼리를 추가하세요.

 List<TeamMission> findAllByTeamAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(Team team, LocalDateTime now, LocalDateTime now1);
+
+boolean existsByTeamAndMissionAndEndedAtGreaterThanEqualAndStartedAtLessThanEqual(
+        Team team,
+        Mission mission,
+        LocalDateTime startedAt,
+        LocalDateTime endedAt
+);

활성 구간 조회가 잦다면 (team_id, startedAt, endedAt) 복합 인덱스를 고려하세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java
around lines 26-27, add a derived exists query that detects overlapping missions
to prevent duplicate creation by checking existing.startedAt <= newEnded AND
existing.endedAt >= newStarted (e.g. an existsBy... method returning boolean and
taking Team, newEnd, newStart in that order), or provide an equivalent @Query if
derived naming is awkward; also add a note to create a composite DB index on
(team_id, started_at, ended_at) if active-range lookups are frequent to improve
performance.

Comment on lines +6 to +7
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

엔티티/DTO/레포지토리 import 경로가 이전 패키지로 남아 있음

엔티티와 DTO가 _team_mission로 이전되었는데 import는 여전히 _mission_team를 가리킵니다. 컴파일 에러 납니다. 레포지토리 패키지도 통일 필요.

-import com.github.kmu_shell_we.domain.mission.entity.Mission;
-import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
-import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
-import com.github.kmu_shell_we.domain.season._team._mission_team.exceptions.TeamMissionExceptions;
-import com.github.kmu_shell_we.domain.season._team._mission_team.repository.TeamMissionRepository;
+import com.github.kmu_shell_we.domain.mission.entity.Mission;
+import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
+import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
+import com.github.kmu_shell_we.domain.season._team._team_mission.exceptions.TeamMissionExceptions;
+import com.github.kmu_shell_we.domain.season._team._team_mission.repository.TeamMissionRepository;

다음 스크립트로 패키지 불일치 지점을 빠르게 스캔하세요.

Also applies to: 10-11, 13-14


🏁 Script executed:

#!/bin/bash
# 1) 실제 디렉터리 아래에 'mission_team' 패키지 선언이 남아있는지
rg -n '^package .+_mission_team\.' src/main/java/**/_team_mission/**

# 2) import에서 _mission_team 참조가 남아있는지
rg -n 'import .+_mission_team\.' src/main/java

# 3) TeamMission 엔티티의 실제 패키지
rg -n '^package .+TeamMission' src/main/java | sed -n '1p'

Length of output: 1941


잘못된 패키지 import: _mission_team → _team_mission (컴파일 오류 발생)

엔티티/DTO/레포지토리 import가 이전 패키지명(_mission_team)으로 남아 있어 컴파일 오류가 납니다. 아래 파일들의 import를 com.github.kmu_shell_we.domain.season._team._team_mission.* 으로 변경하세요.

  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java:4
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java:8-11
  • src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java:4-7
-import com.github.kmu_shell_we.domain.mission.entity.Mission;
-import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
-import com.github.kmu_shell_we.domain.season._team._mission_team.dto.response.TeamMissionListResponse;
-import com.github.kmu_shell_we.domain.season._team._mission_team.entity.TeamMission;
-import com.github.kmu_shell_we.domain.season._team._mission_team.exceptions.TeamMissionExceptions;
-import com.github.kmu_shell_we.domain.season._team._mission_team.repository.TeamMissionRepository;
+import com.github.kmu_shell_we.domain.mission.entity.Mission;
+import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
+import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
+import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
+import com.github.kmu_shell_we.domain.season._team._team_mission.exceptions.TeamMissionExceptions;
+import com.github.kmu_shell_we.domain.season._team._team_mission.repository.TeamMissionRepository;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.mission.exception.MissionExceptions;
import com.github.kmu_shell_we.domain.season._team._team_mission.dto.response.TeamMissionListResponse;
import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
import com.github.kmu_shell_we.domain.season._team._team_mission.exceptions.TeamMissionExceptions;
import com.github.kmu_shell_we.domain.season._team._team_mission.repository.TeamMissionRepository;
🤖 Prompt for AI Agents
In
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/service/TeamMissionService.java
around lines 6-7, the imports reference the old _mission_team package; update
the entity/DTO/repository imports to use
com.github.kmu_shell_we.domain.season._team._team_mission.* instead. Also apply
the same change in the two other files noted:
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/repository/TeamMissionRepository.java
(line 4) and
src/main/java/com/github/kmu_shell_we/domain/season/_team/_team_mission/controller/AdminTeamMissionController.java
(lines 4-7) so all imports point to
com.github.kmu_shell_we.domain.season._team._team_mission.{appropriate classes}
to fix the compilation errors.

Comment on lines +70 to +112
@Transactional
public List<TeamMission> createSpecialMission(
Season season,
Mission mission,
LocalDateTime startedAt,
LocalDateTime endedAt
) {

if (startedAt == null || endedAt == null || !endedAt.isAfter(startedAt))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();

LocalDateTime seasonStart = season.getStartedAt();
LocalDateTime seasonEnd = season.getEndedAt();
if (seasonStart != null && startedAt.isBefore(seasonStart))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
if (seasonEnd != null && endedAt.isAfter(seasonEnd))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();

if (!season.isCurrentSeason())
throw SeasonExceptions.NOT_FOUND_CURRENT_SEASON.toException();

if (mission == null)
throw MissionExceptions.NOT_FOUND_MISSION.toException();

if (mission.getType() != MissionType.SPECIAL)
throw MissionExceptions.INVALID_SPECIAL_MISSION_TYPE.toException();

List<Team> teams = season.getTeams();
if (teams.isEmpty())
throw TeamExceptions.NOT_FOUND_TEAM.toException();

List<TeamMission> teamMissions = new ArrayList<>();
for (Team team : teams) {
teamMissions.add(TeamMission.builder()
.mission(mission)
.team(team)
.startedAt(startedAt)
.endedAt(endedAt)
.build());
}

return teamMissionRepository.saveAll(teamMissions);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

스페셜 미션 중복 생성 방지(Idempotency) 추가 권장

동일 팀/미션에 시간이 겹치는 TeamMission이 이미 있으면 중복 생성됩니다. 운영 중 실수/재시도에서 중복 데이터가 생깁니다. 겹침 여부 exists 쿼리를 추가하고, 발견 시 skip 또는 예외 처리하세요.

     List<TeamMission> teamMissions = new ArrayList<>();
     for (Team team : teams) {
+        // 동일 구간(or 겹치는 구간) 중복 생성 방지
+        if (teamMissionRepository
+                .existsByTeamAndMissionAndEndedAtGreaterThanEqualAndStartedAtLessThanEqual(
+                        team, mission, startedAt, endedAt)) {
+            continue;
+        }
         teamMissions.add(TeamMission.builder()
                 .mission(mission)
                 .team(team)
                 .startedAt(startedAt)
                 .endedAt(endedAt)
                 .build());
     }

레포지토리에 아래 시그니처도 추가해 주세요(아래 파일 코멘트에 diff 포함).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional
public List<TeamMission> createSpecialMission(
Season season,
Mission mission,
LocalDateTime startedAt,
LocalDateTime endedAt
) {
if (startedAt == null || endedAt == null || !endedAt.isAfter(startedAt))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
LocalDateTime seasonStart = season.getStartedAt();
LocalDateTime seasonEnd = season.getEndedAt();
if (seasonStart != null && startedAt.isBefore(seasonStart))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
if (seasonEnd != null && endedAt.isAfter(seasonEnd))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
if (!season.isCurrentSeason())
throw SeasonExceptions.NOT_FOUND_CURRENT_SEASON.toException();
if (mission == null)
throw MissionExceptions.NOT_FOUND_MISSION.toException();
if (mission.getType() != MissionType.SPECIAL)
throw MissionExceptions.INVALID_SPECIAL_MISSION_TYPE.toException();
List<Team> teams = season.getTeams();
if (teams.isEmpty())
throw TeamExceptions.NOT_FOUND_TEAM.toException();
List<TeamMission> teamMissions = new ArrayList<>();
for (Team team : teams) {
teamMissions.add(TeamMission.builder()
.mission(mission)
.team(team)
.startedAt(startedAt)
.endedAt(endedAt)
.build());
}
return teamMissionRepository.saveAll(teamMissions);
}
@Transactional
public List<TeamMission> createSpecialMission(
Season season,
Mission mission,
LocalDateTime startedAt,
LocalDateTime endedAt
) {
if (startedAt == null || endedAt == null || !endedAt.isAfter(startedAt))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
LocalDateTime seasonStart = season.getStartedAt();
LocalDateTime seasonEnd = season.getEndedAt();
if (seasonStart != null && startedAt.isBefore(seasonStart))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
if (seasonEnd != null && endedAt.isAfter(seasonEnd))
throw TeamMissionExceptions.INVALID_MISSION_TIME.toException();
if (!season.isCurrentSeason())
throw SeasonExceptions.NOT_FOUND_CURRENT_SEASON.toException();
if (mission == null)
throw MissionExceptions.NOT_FOUND_MISSION.toException();
if (mission.getType() != MissionType.SPECIAL)
throw MissionExceptions.INVALID_SPECIAL_MISSION_TYPE.toException();
List<Team> teams = season.getTeams();
if (teams.isEmpty())
throw TeamExceptions.NOT_FOUND_TEAM.toException();
List<TeamMission> teamMissions = new ArrayList<>();
for (Team team : teams) {
// 동일 구간(or 겹치는 구간) 중복 생성 방지
if (teamMissionRepository
.existsByTeamAndMissionAndEndedAtGreaterThanEqualAndStartedAtLessThanEqual(
team, mission, startedAt, endedAt)) {
continue;
}
teamMissions.add(TeamMission.builder()
.mission(mission)
.team(team)
.startedAt(startedAt)
.endedAt(endedAt)
.build());
}
return teamMissionRepository.saveAll(teamMissions);
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants