Skip to content
This repository was archived by the owner on Jul 3, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a4d942b
[SW-127] feat(seasonMission Read): 특정 시즌의 특별 미션 목록 조회
BORB-CHOI Aug 28, 2025
d72306c
[SW-128] feat(TeamMission Entity): startedAt 필드 추가
BORB-CHOI Aug 28, 2025
fb8ffcd
[SW-128] feat(speacial request): 특별 미션 등록용 request dto
BORB-CHOI Aug 28, 2025
1684b7a
[SW-128] feat(teamMission response): 응답 필드 startedAt, endedAt 추가
BORB-CHOI Aug 28, 2025
e4aa4c5
[SW-128] feat(teamMission exceptions): 특별 미션 등록에 필요한 exceptions
BORB-CHOI Aug 28, 2025
3e9d4c4
[SW-128] feat(teamMission service): startedAt 필드 추가에 따라 addRandomMiss…
BORB-CHOI Aug 28, 2025
58190f6
[SW-128] feat(teamMission service): 관리자의 특별 미션 배정
BORB-CHOI Aug 28, 2025
b9367c5
[SW-128] feat(teamMission controller): 관리자의 특별 미션 배정, 특별 팀 미션을 모든 팀에 생성
BORB-CHOI Aug 28, 2025
dbc05e0
[SW-127] feat(seasonMission Read): 특정 시즌의 특별 미션 목록 조회 (#38)
BORB-CHOI Sep 1, 2025
49ecb0b
[0.6.0] SW-120 URL 변조로 다른 팀 정보 조회 가능 & 팀 미션 조회 시 중복 튜플 존재 버그 해결 (#37)
arirang030 Sep 4, 2025
2e46cde
[0.6.0] SW-133 팀 시간표 조회 컨트롤러 내 유저 아이디 UUID로 받기
arirang030 Sep 5, 2025
44baaf0
[0.6.0] SW-124 아이템 뽑기 전 포인트 잔량 확인하기 (#41)
arirang030 Sep 6, 2025
87101ee
[SW-128] feat(teamMission dto): DTO 레벨 검증 추가: endedAt > startedAt 보장
BORB-CHOI Sep 10, 2025
c74c7cb
[SW-128] feat(teamMission service): 시즌 범위 벗어난 시간 차단. special 미션 시간이 시…
BORB-CHOI Sep 10, 2025
a6dc8a9
[SW-128] fix(teamMission service): 미래 시작 미션까지 노출되는 버그: startedAt <= n…
BORB-CHOI Sep 10, 2025
8a575b7
[SW-128] refactor(teamMission service): 시즌 경계 검증이 한쪽 경계 누락 시 무시 됨 — 각…
BORB-CHOI Sep 10, 2025
52483c7
[SW-128] feat(TeamMission Entity): startedAt 필드 추가
BORB-CHOI Aug 28, 2025
e0cc8b7
[SW-128] feat(speacial request): 특별 미션 등록용 request dto
BORB-CHOI Aug 28, 2025
0a8d685
[SW-128] feat(teamMission response): 응답 필드 startedAt, endedAt 추가
BORB-CHOI Aug 28, 2025
6e8be2f
[SW-128] feat(teamMission exceptions): 특별 미션 등록에 필요한 exceptions
BORB-CHOI Aug 28, 2025
c9479fb
[SW-128] feat(teamMission service): startedAt 필드 추가에 따라 addRandomMiss…
BORB-CHOI Aug 28, 2025
abfee04
[SW-128] feat(teamMission service): 관리자의 특별 미션 배정
BORB-CHOI Aug 28, 2025
16b5c72
[SW-128] feat(teamMission controller): 관리자의 특별 미션 배정, 특별 팀 미션을 모든 팀에 생성
BORB-CHOI Aug 28, 2025
ae7a8aa
[SW-128] feat(teamMission dto): DTO 레벨 검증 추가: endedAt > startedAt 보장
BORB-CHOI Sep 10, 2025
825b1d3
[SW-128] feat(teamMission service): 시즌 범위 벗어난 시간 차단. special 미션 시간이 시…
BORB-CHOI Sep 10, 2025
f00f7fd
[SW-128] fix(teamMission service): 미래 시작 미션까지 노출되는 버그: startedAt <= n…
BORB-CHOI Sep 10, 2025
a63a3ac
[SW-128] refactor(teamMission service): 시즌 경계 검증이 한쪽 경계 누락 시 무시 됨 — 각…
BORB-CHOI Sep 10, 2025
5d28bbc
Merge remote-tracking branch 'origin/SW-128' into SW-128
BORB-CHOI Sep 12, 2025
f47ef18
[SW-128] feat(TeamMission Entity): startedAt 필드 추가
BORB-CHOI Aug 28, 2025
cfc1ad1
[SW-128] feat(speacial request): 특별 미션 등록용 request dto
BORB-CHOI Aug 28, 2025
a5453d7
[SW-128] feat(teamMission response): 응답 필드 startedAt, endedAt 추가
BORB-CHOI Aug 28, 2025
7155ed7
[SW-128] feat(teamMission exceptions): 특별 미션 등록에 필요한 exceptions
BORB-CHOI Aug 28, 2025
92363bc
[SW-128] feat(teamMission service): startedAt 필드 추가에 따라 addRandomMiss…
BORB-CHOI Aug 28, 2025
fbda45f
[SW-128] feat(teamMission dto): DTO 레벨 검증 추가: endedAt > startedAt 보장
BORB-CHOI Sep 10, 2025
7e17a29
[SW-128] fix(teamMission service): 미래 시작 미션까지 노출되는 버그: startedAt <= n…
BORB-CHOI Sep 10, 2025
3e74ce1
Merge branch 'SW-128' of https://github.com/kmu-shell-we/shell-we-ser…
BORB-CHOI Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public ApiResponse<MissionListResponse> getSeasonMissions(
return ApiResponse.ok(adminSeasonMissionService.getSeasonMissions(season));
}

@GetMapping("/special")
@Operation(summary = "특정 시즌 스페셜 미션 조회")
public ApiResponse<MissionListResponse> getSeasonSpecialMissions(
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season
) {

return ApiResponse.ok(adminSeasonMissionService.getSeasonSpecialMissions(season));
}

@PostMapping("/{mission}")
@Operation(summary = "시즌 미션 생성")
public ApiResponse<MissionResponse> createSeasonMission(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum SeasonMissionExceptions implements ApiExceptionCode {

NOT_FOUND_SEASON_MISSION("SEASON_MISSION_001", "시즌 미션을 찾을 수 없습니다."),
ALREADY_ADDED_SEASON_MISSION("SEASON_MISSION_002", "이미 추가된 시즌 미션입니다."),
INVALID_SEASON_MISSION("SEASON_MISSION_003", "현재 시즌의 미션이 아닙니다."),
;

private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.kmu_shell_we.domain.mission._season_mission.entity.SeasonMission;
import com.github.kmu_shell_we.domain.mission._season_mission.exception.SeasonMissionExceptions;
import com.github.kmu_shell_we.domain.mission._season_mission.repository.SeasonMissionRepository;
import com.github.kmu_shell_we.domain.mission.constant.MissionType;
import com.github.kmu_shell_we.domain.mission.dto.response.MissionListResponse;
import com.github.kmu_shell_we.domain.mission.dto.response.MissionResponse;
import com.github.kmu_shell_we.domain.mission.entity.Mission;
Expand All @@ -23,6 +24,14 @@ public MissionListResponse getSeasonMissions(Season season) {
return MissionListResponse.fromSeasonMission(seasonMissionRepository.findAllBySeason(season));
}

@Transactional(readOnly = true)
public MissionListResponse getSeasonSpecialMissions(Season season){

return MissionListResponse.fromSeasonMission(
seasonMissionRepository.findAllBySeasonAndMissionType(season, MissionType.SPECIAL)
);
}

@Transactional
public MissionResponse createSeasonMission(Season season, Mission mission) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
Expand Down Expand Up @@ -36,5 +37,6 @@ public class Mission extends BaseSchema {

@OneToMany(mappedBy = "mission", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@Builder.Default
List<SeasonMission> seasonMissions = List.of();
@Setter(AccessLevel.NONE)
List<SeasonMission> seasonMissions = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public enum MissionExceptions implements ApiExceptionCode {

NOT_FOUND_MISSION("MISSION_001", "미션을 찾을 수 없습니다."),
INVALID_SPECIAL_MISSION_TYPE("MISSION_002", "SPECIAL 타입이 아닙니다."),
;

private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import com.github.kmu_shell_we.domain.season._team._item.service.ItemService;
import com.github.kmu_shell_we.domain.season._team.entity.Team;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.domain.user.entity.User;
import com.github.kmu_shell_we.global.response.ApiResponse;
import com.github.kmu_shell_we.global.security.guard.MemberGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@MemberGuard
Expand All @@ -26,18 +28,20 @@ public class ItemController {
@GetMapping
@Operation(summary = "아이템 조회")
public ApiResponse<ItemListResponse> getItems(
@AuthenticationPrincipal User user,
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team
) {
return ApiResponse.ok(itemService.getItems(season, team));
return ApiResponse.ok(itemService.getItems(user, season, team));
}

@PostMapping("/draw")
@Operation(summary = "아이템 뽑기")
public ApiResponse<? extends ItemResponse> drawItem(
@AuthenticationPrincipal User user,
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team
) {
return ApiResponse.ok(itemService.drawItem(season, team));
return ApiResponse.ok(itemService.drawItem(user, season, team));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public enum ItemExceptions implements ApiExceptionCode {

FAILED_TO_DRAW("ITEM_001", "아이템을 뽑는데 실패했습니다."),
NOT_ENOUGH_POINT("ITEM_002", "포인트가 부족합니다."),
;

private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
import com.github.kmu_shell_we.domain.season._team._item.dto.response.detail.MissionChangeResponse;
import com.github.kmu_shell_we.domain.season._team._item.dto.response.detail.ScoreDeductionResponse;
import com.github.kmu_shell_we.domain.season._team._item.entity.Item;
import com.github.kmu_shell_we.domain.season._team._item.exception.ItemExceptions;
import com.github.kmu_shell_we.domain.season._team._item.repository.ItemRepository;
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.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;
import com.github.kmu_shell_we.domain.season._team._user_team.repository.UserTeamRepository;
import com.github.kmu_shell_we.domain.season._team.entity.Team;
import com.github.kmu_shell_we.domain.season._team.exception.TeamExceptions;
import com.github.kmu_shell_we.domain.season._team.repository.TeamRepository;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
Expand All @@ -31,20 +34,30 @@
@RequiredArgsConstructor
public class ItemService {

private final ItemRepository itemRepository;
private final TeamRepository teamRepository;
private final UserTeamRepository userTeamRepository;
private final ItemRepository itemRepository;
private final TeamMissionRepository teamMissionRepository;
private final SeasonMissionRepository seasonMissionRepository;

@Transactional(readOnly = true)
@PreAuthorize("#season.isCurrentSeason() and #season == #team.season")
public ItemListResponse getItems(Season season, Team team) {
@PreAuthorize("@itemService.canAccessItem(#user, #season, #team)")
public ItemListResponse getItems(User user, Season season, Team team) {

return ItemListResponse.from(itemRepository.findAllByTeam(team));
}

@Transactional
public ItemResponse drawItem(Season season, Team team) {
@PreAuthorize("@itemService.canAccessItem(#user, #season, #team)")
public ItemResponse drawItem(User user, Season season, Team team) {

// 0. 포인트 조회 및 차감
if (team.getPoint() < 100) {

throw ItemExceptions.NOT_ENOUGH_POINT.toException();
}

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

Comment on lines +54 to 61

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.

// 1. 아이템 랜덤 뽑기
ItemType itemType = drawRandomItem();
Expand Down Expand Up @@ -101,4 +114,11 @@ private ItemType drawRandomItem() {

return ItemType.SCORE_DEDUCTION;
}

public boolean canAccessItem(User user, Season season, Team team) {

return userTeamRepository.findByUserAndTeam(user, team).isPresent()
&& season.isCurrentSeason()
&& season.equals(team.getSeason());
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.github.kmu_shell_we.domain.season._team._mission_team.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Schema(description = "특별 팀 미션 생성 요청")
public class CreateSpecialTeamMissionRequest {

@NotNull
@Schema(description = "시작 일시")
LocalDateTime startedAt;

@NotNull
@Schema(description = "종료 일시")
LocalDateTime endedAt;

@AssertTrue(message = "종료 일시는 시작 일시보다 이후여야 합니다.")
public boolean isValidTimeRange() {
return startedAt != null && endedAt != null && endedAt.isAfter(startedAt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.github.kmu_shell_we.global.security.guard.MemberGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
Expand All @@ -30,7 +31,7 @@ public class TeamScheduleController {
public ApiResponse<TeamScheduleResponse> getTeamSchedule(
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team,
@RequestParam List<User> users
@Parameter(description = "유저 ID 목록", array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))) @RequestParam List<User> users
) {

return ApiResponse.ok(scheduleService.getTeamSchedule(season, team, users));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.controller;
package com.github.kmu_shell_we.domain.season._team._team_mission._submission.controller;


import com.github.kmu_shell_we.domain.mission.entity.Mission;
import com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.dto.request.CreateSubmissionRequest;
import com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.dto.response.SubmissionListResponse;
import com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.dto.response.SubmissionResponse;
import com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.service.SubmissionService;
import com.github.kmu_shell_we.domain.season._team._team_mission._submission.dto.request.CreateSubmissionRequest;
import com.github.kmu_shell_we.domain.season._team._team_mission._submission.dto.response.SubmissionListResponse;
import com.github.kmu_shell_we.domain.season._team._team_mission._submission.dto.response.SubmissionResponse;
import com.github.kmu_shell_we.domain.season._team._team_mission._submission.service.SubmissionService;
import com.github.kmu_shell_we.domain.season._team._team_mission.entity.TeamMission;
import com.github.kmu_shell_we.domain.season._team.entity.Team;
import com.github.kmu_shell_we.domain.season.entity.Season;
import com.github.kmu_shell_we.domain.user.entity.User;
import com.github.kmu_shell_we.global.response.ApiResponse;
import com.github.kmu_shell_we.global.security.guard.MemberGuard;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -16,6 +17,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@MemberGuard
Expand All @@ -30,33 +32,36 @@ public class SubmissionController {
@GetMapping("/submissions")
@Operation(summary = "팀 미션 제출 목록 조회")
public ApiResponse<SubmissionListResponse> getSubmissions(
@AuthenticationPrincipal User user,
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team
) {

return ApiResponse.ok(submissionService.getSubmissions(season, team));
return ApiResponse.ok(submissionService.getSubmissions(user, season, team));
}

@GetMapping("/missions/{mission}/submissions")
@GetMapping("/team_mission/{teamMission}/submissions")
@Operation(summary = "팀 미션 제출 조회")
public ApiResponse<SubmissionResponse> getSubmission(
@AuthenticationPrincipal User user,
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team,
@Parameter(description = "미션 ID", schema = @Schema(type = "string", format = "uuid"))@PathVariable Mission mission
@Parameter(description = "미션 ID", schema = @Schema(type = "string", format = "uuid"))@PathVariable TeamMission teamMission
) {

return ApiResponse.ok(submissionService.getSubmission(season, team, mission));
return ApiResponse.ok(submissionService.getSubmission(user, season, team, teamMission));
}

@PostMapping("/missions/{mission}/submissions")
@PostMapping("/team_mission/{teamMission}/submissions")
@Operation(summary = "팀 미션 제출")
public ApiResponse<SubmissionResponse> submitMission(
@AuthenticationPrincipal User user,
@Parameter(description = "시즌 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Season season,
@Parameter(description = "팀 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Team team,
@Parameter(description = "미션 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable Mission mission,
@Parameter(description = "미션 ID", schema = @Schema(type = "string", format = "uuid")) @PathVariable TeamMission teamMission,
@RequestBody @Valid CreateSubmissionRequest request
) {

return ApiResponse.ok(submissionService.submitSubmission(season, team, mission, request));
return ApiResponse.ok(submissionService.submitSubmission(user, season, team, teamMission, request));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.kmu_shell_we.domain.season._team._mission_team._submission_mission_team.dto.request;
package com.github.kmu_shell_we.domain.season._team._team_mission._submission.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand Down
Loading