Skip to content

Commit 9806aa5

Browse files
committed
fix: 병합 해결
2 parents 3b23c99 + 5feda39 commit 9806aa5

23 files changed

Lines changed: 276 additions & 93 deletions

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ dependencies {
7474

7575
// HTML 파싱 (TIL 본문 경험치 산정 시 태그 제외 후 순수 텍스트 길이 계산)
7676
implementation 'org.jsoup:jsoup:1.18.1'
77+
78+
// Rate Limiting (AI 엔드포인트 사용자별 요청 제한)
79+
implementation 'com.bucket4j:bucket4j-core:8.10.1'
7780
}
7881

7982
// ─── 테스트용 MySQL 컨테이너 생명주기 태스크 ─────────────────────────────────

src/main/java/com/Rootin/domain/ai/service/AiResultService.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.Rootin.domain.user.entity.User;
1313
import com.Rootin.domain.user.repository.UserRepository;
1414
import com.Rootin.global.exception.CustomException;
15+
import com.Rootin.global.exception.ErrorCode;
1516
import lombok.RequiredArgsConstructor;
1617
import org.springframework.stereotype.Service;
1718
import org.springframework.transaction.annotation.Transactional;
@@ -37,17 +38,17 @@ public class AiResultService {
3738
@Transactional
3839
public AiResultResponse save(AiResultSaveRequest request, Long userId) {
3940
Pot pot = potRepository.findById(request.potId())
40-
.orElseThrow(() -> CustomException.notFound("화분을 찾을 수 없습니다."));
41+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
4142

4243
if (!pot.getUserId().equals(userId)) {
43-
throw CustomException.forbidden("본인의 화분에만 AI 결과를 저장할 수 있습니다.");
44+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
4445
}
4546

4647
List<Til> tils = tilRepository.findByUserIdAndPotIdAndStatus(
4748
userId, request.potId(), PostStatus.PUBLISHED);
4849

4950
if (tils.isEmpty()) {
50-
throw CustomException.notFound("화분에 저장된 TIL이 없습니다.");
51+
throw CustomException.of(ErrorCode.TIL_NOT_FOUND);
5152
}
5253

5354
// JPA FK 설정을 위해 프록시 참조 사용 (실제 SELECT 없이 ID만 사용)
@@ -81,10 +82,10 @@ public List<AiResultResponse> getResults(Long userId, Long potId) {
8182
}
8283

8384
Pot pot = potRepository.findById(potId)
84-
.orElseThrow(() -> CustomException.notFound("화분을 찾을 수 없습니다."));
85+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
8586

8687
if (!pot.getUserId().equals(userId)) {
87-
throw CustomException.forbidden("본인의 화분 결과만 조회할 수 있습니다.");
88+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
8889
}
8990

9091
return aiResultRepository.findAllByUserAndPotId(userRef, potId).stream()
@@ -95,10 +96,10 @@ public List<AiResultResponse> getResults(Long userId, Long potId) {
9596
@Transactional
9697
public void delete(Long resultId, Long userId) {
9798
AiResult aiResult = aiResultRepository.findById(resultId)
98-
.orElseThrow(() -> CustomException.notFound("AI 결과를 찾을 수 없습니다."));
99+
.orElseThrow(() -> CustomException.of(ErrorCode.AI_RESULT_NOT_FOUND));
99100

100101
if (!aiResult.getUser().getId().equals(userId)) {
101-
throw CustomException.forbidden("본인의 AI 결과만 삭제할 수 있습니다.");
102+
throw CustomException.of(ErrorCode.AI_RESULT_FORBIDDEN);
102103
}
103104

104105
aiResultRepository.delete(aiResult);

src/main/java/com/Rootin/domain/ai/service/AiService.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.Rootin.domain.user.entity.User;
1919
import com.Rootin.domain.user.repository.UserRepository;
2020
import com.Rootin.global.exception.CustomException;
21+
import com.Rootin.global.exception.ErrorCode;
2122
import com.fasterxml.jackson.databind.ObjectMapper;
2223
import lombok.RequiredArgsConstructor;
2324
import org.springframework.http.HttpStatus;
@@ -62,14 +63,14 @@ public AiSummaryResponse summarize(AiSummaryRequest request, Long userId) {
6263
tils = resolveTilsByIds(request.tilIds(), userId, "요약");
6364
} else {
6465
Pot pot = potRepository.findById(request.potId())
65-
.orElseThrow(() -> CustomException.notFound("화분을 찾을 수 없습니다."));
66+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
6667
if (!pot.getUserId().equals(user.getId())) {
67-
throw CustomException.forbidden("본인의 화분만 요약할 수 있습니다.");
68+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
6869
}
6970
tils = tilRepository.findByUserIdAndPotIdAndStatus(
7071
user.getId(), request.potId(), PostStatus.PUBLISHED);
7172
if (tils.isEmpty()) {
72-
throw CustomException.notFound("요약할 TIL이 없습니다.");
73+
throw CustomException.of(ErrorCode.TIL_NOT_FOUND);
7374
}
7475
}
7576

@@ -115,14 +116,14 @@ public AiQuizResponse generateQuiz(AiQuizRequest request, Long userId) {
115116
tils = resolveTilsByIds(request.tilIds(), userId, "퀴즈 생성");
116117
} else {
117118
Pot pot = potRepository.findById(request.potId())
118-
.orElseThrow(() -> CustomException.notFound("화분을 찾을 수 없습니다."));
119+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
119120
if (!pot.getUserId().equals(user.getId())) {
120-
throw CustomException.forbidden("본인의 화분으로만 복습 문제를 생성할 수 있습니다.");
121+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
121122
}
122123
tils = tilRepository.findByUserIdAndPotIdAndStatus(
123124
user.getId(), request.potId(), PostStatus.PUBLISHED);
124125
if (tils.isEmpty()) {
125-
throw CustomException.notFound("문제를 생성할 TIL이 없습니다.");
126+
throw CustomException.of(ErrorCode.TIL_NOT_FOUND);
126127
}
127128
}
128129

src/main/java/com/Rootin/domain/auth/service/AuthService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.Rootin.domain.user.entity.User;
99
import com.Rootin.domain.user.repository.UserRepository;
1010
import com.Rootin.global.exception.CustomException;
11+
import com.Rootin.global.exception.ErrorCode;
1112
import com.Rootin.global.jwt.JwtTokenProvider;
1213
import lombok.RequiredArgsConstructor;
1314
import org.springframework.beans.factory.annotation.Value;
@@ -82,13 +83,13 @@ public TokenResponse signup(SignupRequest request) {
8283
if (!existing.isEnabled()) {
8384
deleteUserAndTokens(existing);
8485
} else {
85-
throw CustomException.badRequest("이미 사용 중인 이메일입니다.");
86+
throw CustomException.of(ErrorCode.DUPLICATE_EMAIL);
8687
}
8788
});
8889

8990
// 2. 닉네임 중복 검사
9091
if (userRepository.existsByNickname(request.getNickname())) {
91-
throw CustomException.badRequest("이미 사용 중인 닉네임입니다.");
92+
throw CustomException.of(ErrorCode.DUPLICATE_NICKNAME);
9293
}
9394

9495
// 3~4. 비밀번호 암호화 후 User 생성 및 저장
@@ -204,7 +205,7 @@ public TokenResponse googleLogin(GoogleLoginRequest request) {
204205
// 3. 신규 사용자 → 자동 회원가입
205206
if (user == null) {
206207
if (userRepository.existsByEmail(email)) {
207-
throw CustomException.badRequest("이미 다른 방식으로 가입된 이메일입니다.");
208+
throw CustomException.of(ErrorCode.EMAIL_PROVIDER_MISMATCH);
208209
}
209210

210211
String nickname = generateUniqueNickname(googleName, sub);

src/main/java/com/Rootin/domain/garden/service/ExperienceService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.Rootin.domain.user.entity.User;
1212
import com.Rootin.domain.user.repository.UserRepository;
1313
import com.Rootin.global.exception.CustomException;
14+
import com.Rootin.global.exception.ErrorCode;
1415
import lombok.RequiredArgsConstructor;
1516
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.stereotype.Service;
@@ -64,7 +65,7 @@ public void applyWatering(Long userId, Pot pot, int contentLength, Long tilId) {
6465
// 1. 동일 TIL 포스트에 대한 물주기 중복 적립 방지 검사.
6566
// 애플리케이션 레벨에서 먼저 막고, WateringLog.post_id unique 제약으로 DB 레벨에서도 한 번 더 막습니다.
6667
if (wateringLogRepository.existsByPostId(tilId)) {
67-
throw CustomException.badRequest("이미 물주기가 완료된 TIL입니다.");
68+
throw CustomException.of(ErrorCode.ALREADY_WATERED_TODAY);
6869
}
6970

7071
// 2. 대상 TIL 포스트 존재 여부 및 유저 소유권, 화분 매핑 일치 검증.
@@ -118,7 +119,7 @@ public void applyWatering(Long userId, Pot pot, int contentLength, Long tilId) {
118119

119120
// [새 정책] 식물 개별 경험치 가산.
120121
PlantItem plantItem = plantItemRepository.findByPotIdAndIsHarvestedFalse(pot.getId())
121-
.orElseThrow(() -> CustomException.notFound("화분에 심어진 식물이 존재하지 않습니다. ID: " + pot.getId()));
122+
.orElseThrow(() -> CustomException.of(ErrorCode.NO_ACTIVE_PLANT));
122123
int beforePlantExp = plantItem.getGrowthExp();
123124
plantItem.increaseGrowthExp(gainedExp);
124125
log.info("식물 경험치 변동: {} Exp -> {} Exp (획득 경험치: {})", beforePlantExp, plantItem.getGrowthExp(), gainedExp);

src/main/java/com/Rootin/domain/garden/service/GardenDashboardService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.Rootin.domain.til.entity.PostStatus;
1515
import com.Rootin.domain.til.repository.TilRepository;
1616
import com.Rootin.global.exception.CustomException;
17+
import com.Rootin.global.exception.ErrorCode;
1718
import lombok.RequiredArgsConstructor;
1819
import org.springframework.stereotype.Service;
1920
import org.springframework.transaction.annotation.Transactional;
@@ -67,22 +68,22 @@ public class GardenDashboardService {
6768
public GardenInfoResponse getGardenDashboard(Long potId, Long userId) {
6869
// 1. 화분 정보를 조회합니다. (락 미사용)
6970
Pot pot = potRepository.findById(potId)
70-
.orElseThrow(() -> CustomException.notFound("존재하지 않는 화분입니다. ID: " + potId));
71+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
7172

7273
// 2. 요청자가 이 화분의 실제 소유주가 맞는지 검증합니다.
7374
if (!pot.getUserId().equals(userId)) {
74-
throw CustomException.forbidden("해당 화분의 대시보드에 접근할 권한이 없습니다.");
75+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
7576
}
7677

7778
// 3. 현재 화분에 심겨 있고 수확되지 않은 식물(PlantItem)을 조회합니다.
7879
// PlantItem은 "사용자의 화분에 어떤 식물이 심겨 있는지"를 나타내는 연결 테이블 역할을 합니다.
7980
PlantItem plantItem = plantItemRepository.findByPotIdAndIsHarvestedFalse(potId)
80-
.orElseThrow(() -> CustomException.notFound("화분에 심어진 식물이 존재하지 않습니다. ID: " + potId));
81+
.orElseThrow(() -> CustomException.of(ErrorCode.NO_ACTIVE_PLANT));
8182

8283
// 4. 식물의 기본 정보(Fallback 대비 마스터 데이터)를 획득합니다.
8384
// Plant는 이미지 URL, 등급, 성장 단계 같은 "식물 마스터 데이터"를 담습니다.
8485
Plant basePlant = plantRepository.findById(plantItem.getPlantId())
85-
.orElseThrow(() -> CustomException.notFound("식물 마스터 정보가 존재하지 않습니다. ID: " + plantItem.getPlantId()));
86+
.orElseThrow(() -> CustomException.of(ErrorCode.PLANT_NOT_FOUND));
8687

8788
// 5. 현재 식물의 누적 경험치를 토대로 이 식물의 런타임 성장 단계를 판별합니다.
8889
GrowthStage calculatedStage = levelCalculator.determinePlantGrowthStage(plantItem.getGrowthExp());

src/main/java/com/Rootin/domain/garden/service/GardenService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.Rootin.domain.user.entity.User;
1414
import com.Rootin.domain.user.repository.UserRepository;
1515
import com.Rootin.global.exception.CustomException;
16+
import com.Rootin.global.exception.ErrorCode;
1617
import lombok.RequiredArgsConstructor;
1718
import org.springframework.stereotype.Service;
1819
import org.springframework.transaction.annotation.Transactional;
@@ -201,7 +202,7 @@ public void updateGardenLayout(Long userId, GardenLayoutUpdateRequest request) {
201202
List<Pot> pots = potRepository.findAllById(distinctPotIds);
202203

203204
if (pots.size() != distinctPotIds.size()) {
204-
throw CustomException.notFound("일부 화분을 찾을 수 없거나 존재하지 않는 ID가 포함되어 있습니다.");
205+
throw CustomException.of(ErrorCode.POT_NOT_FOUND);
205206
}
206207

207208
// Collectors.toMap() 연산 수행 시 중복 키로 인한 IllegalStateException(500 에러) 발생을 방지하기 위해

src/main/java/com/Rootin/domain/garden/service/HarvestService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.Rootin.domain.plant.entity.enums.GrowthStage;
1111
import com.Rootin.domain.plant.repository.PlantRepository;
1212
import com.Rootin.global.exception.CustomException;
13+
import com.Rootin.global.exception.ErrorCode;
1314
import lombok.RequiredArgsConstructor;
1415
import org.springframework.stereotype.Service;
1516
import org.springframework.transaction.annotation.Transactional;
@@ -28,15 +29,15 @@ public class HarvestService {
2829
public HarvestResponse harvest(Long userId, Long potId) {
2930
// 1. 동시 수확 요청 시 중복 씨앗 생성을 방지하기 위해 비관적 쓰기 락(Pessimistic Write Lock)을 사용해 화분을 조회합니다.
3031
Pot pot = potRepository.findByIdWithLock(potId)
31-
.orElseThrow(() -> CustomException.notFound("존재하지 않는 화분입니다."));
32+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
3233

3334
if (!pot.getUserId().equals(userId)) {
34-
throw CustomException.forbidden("해당 화분에 접근할 권한이 없습니다.");
35+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
3536
}
3637

3738
// 2. 현재 화분에 자라고 있는 활성 식물(isHarvested = false)을 조회합니다.
3839
PlantItem current = plantItemRepository.findByPotIdAndIsHarvestedFalse(potId)
39-
.orElseThrow(() -> CustomException.notFound("수확할 식물이 없습니다."));
40+
.orElseThrow(() -> CustomException.of(ErrorCode.NO_ACTIVE_PLANT));
4041

4142
// 3. 수확 시점의 성장 단계(0=씨앗 ~ 4=만개)를 계산합니다.
4243
int stageIndex = levelCalculator.determinePlantGrowthStage(current.getGrowthExp()).ordinal();
@@ -47,7 +48,7 @@ public HarvestResponse harvest(Long userId, Long potId) {
4748
plantItemRepository.saveAndFlush(current);
4849

4950
Plant harvestedPlant = plantRepository.findById(current.getPlantId())
50-
.orElseThrow(() -> CustomException.notFound("식물 마스터 데이터를 찾을 수 없습니다."));
51+
.orElseThrow(() -> CustomException.of(ErrorCode.PLANT_NOT_FOUND));
5152

5253
// 5. 수확 단계에 따라 다음 씨앗 배정
5354
// FULL_BLOOM: 전체 풀 랜덤 + 새 종이면 해금 풀에 추가

src/main/java/com/Rootin/domain/garden/service/PotPlantService.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.Rootin.domain.plant.entity.enums.GrowthStage;
1515
import com.Rootin.domain.plant.repository.PlantRepository;
1616
import com.Rootin.global.exception.CustomException;
17+
import com.Rootin.global.exception.ErrorCode;
1718
import lombok.RequiredArgsConstructor;
1819
import org.springframework.stereotype.Service;
1920
import org.springframework.transaction.annotation.Transactional;
@@ -69,7 +70,7 @@ public PotPlantOptionsResponse getPlantOptions(Long userId, Long potId) {
6970
@Transactional
7071
public PotPlantResponse plant(Long userId, Long potId, PotPlantRequest request) {
7172
Pot pot = potRepository.findByIdWithLock(potId)
72-
.orElseThrow(() -> CustomException.notFound("존재하지 않는 화분입니다. ID: " + potId));
73+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
7374
validateOwner(pot, userId);
7475

7576
List<PlantItem> activeItems = plantItemRepository.findActivePlantItemsByPotId(pot.getId());
@@ -97,14 +98,14 @@ public PotPlantResponse plant(Long userId, Long potId, PotPlantRequest request)
9798

9899
private Pot getOwnedPot(Long userId, Long potId) {
99100
Pot pot = potRepository.findById(potId)
100-
.orElseThrow(() -> CustomException.notFound("존재하지 않는 화분입니다. ID: " + potId));
101+
.orElseThrow(() -> CustomException.of(ErrorCode.POT_NOT_FOUND));
101102
validateOwner(pot, userId);
102103
return pot;
103104
}
104105

105106
private void validateOwner(Pot pot, Long userId) {
106107
if (!pot.getUserId().equals(userId)) {
107-
throw CustomException.forbidden("해당 화분에 접근할 권한이 없습니다.");
108+
throw CustomException.of(ErrorCode.POT_FORBIDDEN);
108109
}
109110
}
110111

@@ -135,7 +136,7 @@ private Plant selectHarvestedPlantSeed(Long userId, Long sourcePlantItemId) {
135136
}
136137

137138
Plant sourcePlant = plantRepository.findById(sourceItem.getPlantId())
138-
.orElseThrow(() -> CustomException.notFound("식물 마스터 데이터를 찾을 수 없습니다."));
139+
.orElseThrow(() -> CustomException.of(ErrorCode.PLANT_NOT_FOUND));
139140

140141
if (sourcePlant.getGrowthStage() != GrowthStage.SEED) {
141142
throw CustomException.badRequest("수확 식물 아이템은 씨앗 단계 식물 마스터 데이터와 연결되어야 합니다.");
@@ -152,7 +153,7 @@ private Plant selectRandomSeedPlant() {
152153
candidates = plantRepository.findByGradeAndGrowthStage(Grade.COMMON, GrowthStage.SEED);
153154
}
154155
if (candidates.isEmpty()) {
155-
throw CustomException.notFound("배정 가능한 식물 마스터 데이터가 없습니다.");
156+
throw CustomException.of(ErrorCode.PLANT_NOT_FOUND);
156157
}
157158

158159
return candidates.get(ThreadLocalRandom.current().nextInt(candidates.size()));
@@ -201,7 +202,7 @@ private Map<Long, Plant> getPlantMap(PlantItem currentPlantItem, List<PlantItem>
201202

202203
private PotPlantResponse toPotPlantResponse(PlantItem plantItem, Plant plant) {
203204
if (plant == null) {
204-
throw CustomException.notFound("식물 마스터 데이터를 찾을 수 없습니다.");
205+
throw CustomException.of(ErrorCode.PLANT_NOT_FOUND);
205206
}
206207
return new PotPlantResponse(
207208
plantItem.getPotId(),
@@ -216,7 +217,7 @@ private PotPlantResponse toPotPlantResponse(PlantItem plantItem, Plant plant) {
216217

217218
private PlantOptionResponse toPlantOptionResponse(PlantItem plantItem, Plant plant) {
218219
if (plant == null) {
219-
throw CustomException.notFound("식물 마스터 데이터를 찾을 수 없습니다.");
220+
throw CustomException.of(ErrorCode.PLANT_NOT_FOUND);
220221
}
221222
return new PlantOptionResponse(
222223
plantItem.getId(),

0 commit comments

Comments
 (0)