Skip to content

Commit baf1caf

Browse files
committed
Merge branch 'dev' into feat/expense-LLM
2 parents b68e661 + ce912c4 commit baf1caf

23 files changed

Lines changed: 4132 additions & 137 deletions

src/main/java/store/lastdance/controller/aijudgment/AiJudgmentController.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public ResponseEntity<ApiResponse<AiJudgmentResponseDTO>> judgeConflict(
3737
@Valid @RequestBody CreateAiJudgmentRequestDTO request,
3838
@AuthenticationPrincipal CustomOAuth2User user) {
3939

40-
log.info("갈등 판단 요청 - 사용자 ID: {}, 상황: {}", user.getUserId(), request.getSituation());
40+
log.info("갈등 판단 요청 - 사용자 ID: {}, 상황: {}", user.getUserId(), request.getSituations());
4141

4242
try {
4343
AiJudgmentResponseDTO response = aiJudgmentService.judgeConflict(request, user.getUserId());
@@ -59,7 +59,7 @@ public ResponseEntity<ApiResponse<AiJudgmentResponseDTO>> judgeConflict(
5959
@PostMapping("/{judgmentId}/feedback")
6060
public ResponseEntity<ApiResponse<String>> toggleFeedback(
6161
@PathVariable UUID judgmentId,
62-
@RequestParam("type") String type, // up 또는 down
62+
@RequestParam("type") String type,
6363
@AuthenticationPrincipal CustomOAuth2User user
6464
) {
6565
try {
@@ -68,10 +68,6 @@ public ResponseEntity<ApiResponse<String>> toggleFeedback(
6868
} catch (IllegalArgumentException e) {
6969
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
7070
.body(ApiResponse.error("요청이 잘못되었습니다: " + e.getMessage()));
71-
} catch (Exception e) {
72-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
73-
.body(ApiResponse.error("피드백 처리 중 오류가 발생했습니다."));
7471
}
7572
}
76-
7773
}

src/main/java/store/lastdance/domain/aijudgment/AiJudgment.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ public void feedback(Boolean up, Boolean down, String downReason) {
6565
this.down = down;
6666
this.downReason = downReason;
6767
}
68-
}
68+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package store.lastdance.dto.aijudgment;
22

3-
import lombok.AllArgsConstructor;
43
import lombok.Builder;
54
import lombok.Getter;
65

6+
import java.util.Map;
7+
78
@Getter
8-
@AllArgsConstructor
99
@Builder
1010
public class AiJudgmentResponseDTO {
1111
private String judgmentResult;
1212
private String judgmentId;
13-
13+
private Map<String, String> situations; // 추가된 필드
1414
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package store.lastdance.dto.aijudgment;
22

33
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotEmpty;
45
import lombok.Getter;
56
import lombok.Setter;
67

8+
import java.util.Map;
9+
710
@Getter
811
@Setter
912
public class CreateAiJudgmentRequestDTO {
10-
@NotBlank
11-
private String situation;
13+
@NotEmpty
14+
private Map<String, String> situations; // Map으로 유지
1215
}

src/main/java/store/lastdance/dto/expense/CombinedExpenseResponseDTO.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public record CombinedExpenseResponseDTO(
1313
@Schema(description = "지출 ID")
1414
Long expenseId,
1515

16+
@Schema(description = "원본 지출 ID")
17+
Long originalExpenseId,
18+
1619
@Schema(description = "지출 유형", example = "PERSONAL 또는 GROUP_SHARE")
1720
String expenseType,
1821

@@ -47,6 +50,7 @@ public record CombinedExpenseResponseDTO(
4750
public static CombinedExpenseResponseDTO fromPersonal(Expense expense) {
4851
return new CombinedExpenseResponseDTO(
4952
expense.getExpenseId(),
53+
null,
5054
"PERSONAL",
5155
expense.getTitle(),
5256
expense.getAmount(),
@@ -68,6 +72,7 @@ public static CombinedExpenseResponseDTO fromGroupShare(
6872
) {
6973
return new CombinedExpenseResponseDTO(
7074
shareExpense.getExpenseId(),
75+
originalExpense.getExpenseId(),
7176
"SHARE",
7277
shareExpense.getTitle(),
7378
originalExpense.getAmount(), // 그룹 전체 금액

src/main/java/store/lastdance/repository/aijudement/AiJudgmentRepository.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Repository Interface
21
package store.lastdance.repository.aijudement;
32

43
import org.springframework.data.domain.Page;

src/main/java/store/lastdance/scheduler/NotificationScheduler.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.springframework.scheduling.annotation.Scheduled;
66
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.annotation.Transactional;
78
import store.lastdance.domain.calendar.Calendar;
89
import store.lastdance.domain.calendar.CalendarType;
910
import store.lastdance.domain.checklist.Checklist;
@@ -42,6 +43,7 @@ public class NotificationScheduler {
4243
private final SSENotificationService sseService;
4344

4445
@Scheduled(fixedRate = 60000) // 1분마다 실행
46+
@Transactional(readOnly = true)
4547
public void processScheduledNotifications() {
4648
log.info("=== 알림 스케줄러 실행 시작 ===");
4749

@@ -78,6 +80,7 @@ public void processScheduledNotifications() {
7880
}
7981

8082
@Scheduled(fixedRate = 300000) // 5분마다 실행
83+
@Transactional(readOnly = true)
8184
public void cleanupSSEConnections() {
8285
try {
8386
log.debug("=== SSE 연결 정리 스케줄러 실행 ===");
@@ -88,6 +91,7 @@ public void cleanupSSEConnections() {
8891
}
8992
}
9093

94+
@Transactional(readOnly = true)
9195
protected void checkAndSendNotifications(User user) {
9296
NotificationSetting setting = settingRepository.findByUserId(user.getUserId()).orElse(null);
9397
if (setting == null) {
@@ -119,7 +123,8 @@ protected void checkAndSendNotifications(User user) {
119123
}
120124
}
121125

122-
private void checkScheduleNotifications(User user, LocalDateTime reminderTime) {
126+
@Transactional(readOnly = true)
127+
protected void checkScheduleNotifications(User user, LocalDateTime reminderTime) {
123128
try {
124129
LocalDateTime startRange = reminderTime.minusMinutes(2);
125130
LocalDateTime endRange = reminderTime.plusMinutes(2);
@@ -206,7 +211,8 @@ private void checkScheduleNotifications(User user, LocalDateTime reminderTime) {
206211
}
207212
}
208213

209-
private void checkPaymentNotifications(User user, LocalDateTime now) {
214+
@Transactional(readOnly = true)
215+
protected void checkPaymentNotifications(User user, LocalDateTime now) {
210216
try {
211217
// 오늘 날짜의 미정산 분담금 조회
212218
LocalDate today = now.toLocalDate();
@@ -290,7 +296,8 @@ private void checkPaymentNotifications(User user, LocalDateTime now) {
290296
}
291297
}
292298

293-
private void checkChecklistNotifications(User user, LocalDateTime now) {
299+
@Transactional(readOnly = true)
300+
protected void checkChecklistNotifications(User user, LocalDateTime now) {
294301
try {
295302
LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();
296303
LocalDateTime endOfDay = startOfDay.plusDays(1);

src/main/java/store/lastdance/service/aijudgment/AiJudgmentService.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Service Interface
21
package store.lastdance.service.aijudgment;
32

43
import store.lastdance.dto.aijudgment.CreateAiJudgmentRequestDTO;
@@ -9,5 +8,4 @@
98
public interface AiJudgmentService {
109
AiJudgmentResponseDTO judgeConflict(CreateAiJudgmentRequestDTO request, UUID userId);
1110
String toggleFeedback(UUID judgmentId, UUID userId, String type);
12-
1311
}

src/main/java/store/lastdance/service/aijudgment/AiJudgmentServiceImpl.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import store.lastdance.dto.aijudgment.CreateAiJudgmentRequestDTO;
66
import store.lastdance.dto.aijudgment.AiJudgmentResponseDTO;
77
import store.lastdance.domain.aijudgment.AiJudgment;
8-
import store.lastdance.domain.aijudgment.JudgmentType;
98
import store.lastdance.repository.aijudement.AiJudgmentRepository;
109
import store.lastdance.util.gemini.GeminiApiClient;
1110
import org.springframework.transaction.annotation.Transactional;
11+
import reactor.core.publisher.Mono;
1212

13+
import java.util.Map;
1314
import java.util.UUID;
15+
import java.util.stream.Collectors;
1416

1517
@Service
1618
@RequiredArgsConstructor
@@ -20,25 +22,52 @@ public class AiJudgmentServiceImpl implements AiJudgmentService {
2022
private final AiJudgmentRepository aiJudgmentRepository;
2123

2224
@Override
25+
@Transactional
2326
public AiJudgmentResponseDTO judgeConflict(CreateAiJudgmentRequestDTO request, UUID userId) {
24-
String result = geminiApiClient.getJudgmentRatio(request.getSituation());
27+
Map<String, String> situations = request.getSituations();
28+
String situationA = situations.getOrDefault("A", "");
29+
String situationB = situations.getOrDefault("B", "");
30+
31+
if (situationA.isEmpty() || situationB.isEmpty()) {
32+
return AiJudgmentResponseDTO.builder()
33+
.judgmentResult("상황을 조금 더 구체적으로 설명해 주세요.")
34+
.judgmentId(null)
35+
.situations(situations)
36+
.build();
37+
}
38+
39+
String combinedSituations = situations.entrySet().stream()
40+
.map(entry -> entry.getKey() + ": " + entry.getValue())
41+
.collect(Collectors.joining("\n"));
42+
43+
String moderationResult = geminiApiClient.moderateContent(combinedSituations).block();
44+
45+
if ("UNSAFE".equals(moderationResult)) {
46+
return AiJudgmentResponseDTO.builder()
47+
.judgmentResult("보안 정책 위반으로 요청을 처리할 수 없습니다.")
48+
.judgmentId(null)
49+
.situations(situations)
50+
.build();
51+
}
52+
53+
String judgmentResult = geminiApiClient.getJudgmentResult(situations).block();
2554

2655
AiJudgment judgment = AiJudgment.builder()
2756
.judgmentId(UUID.randomUUID())
2857
.userId(userId)
29-
.situation(request.getSituation())
30-
.judgmentResult(result)
58+
.situation(request.getSituations().toString())
59+
.judgmentResult(judgmentResult)
3160
.build();
3261

3362
aiJudgmentRepository.save(judgment);
3463

3564
return AiJudgmentResponseDTO.builder()
36-
.judgmentResult(result)
37-
.judgmentId(judgment.getJudgmentId().toString()) // ✅ 추가
65+
.judgmentResult(judgmentResult)
66+
.judgmentId(judgment.getJudgmentId().toString())
67+
.situations(situations)
3868
.build();
3969
}
4070

41-
4271
@Override
4372
@Transactional
4473
public String toggleFeedback(UUID judgmentId, UUID userId, String type) {
@@ -54,23 +83,22 @@ public String toggleFeedback(UUID judgmentId, UUID userId, String type) {
5483

5584
if ("up".equalsIgnoreCase(type)) {
5685
if (isUp) {
57-
judgment.feedback(false, false, null); // 좋아요 취소
86+
judgment.feedback(false, false, null);
5887
return "좋아요를 취소했습니다.";
5988
} else {
60-
judgment.feedback(true, false, null); // 좋아요 누르고 싫어요 해제
89+
judgment.feedback(true, false, null);
6190
return "좋아요를 반영했습니다.";
6291
}
6392
} else if ("down".equalsIgnoreCase(type)) {
6493
if (isDown) {
65-
judgment.feedback(false, false, null); // 싫어요 취소
94+
judgment.feedback(false, false, null);
6695
return "싫어요를 취소했습니다.";
6796
} else {
68-
judgment.feedback(false, true, null); // 싫어요 누르고 좋아요 해제
97+
judgment.feedback(false, true, null);
6998
return "싫어요를 반영했습니다.";
7099
}
71100
} else {
72101
throw new IllegalArgumentException("type은 'up' 또는 'down'이어야 합니다.");
73102
}
74103
}
75-
76-
}
104+
}

src/main/java/store/lastdance/service/expense/ExpenseServiceImpl.java

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,23 +172,45 @@ private void processCustomSplit(Expense original, List<SplitDataDTO> splitData)
172172
}
173173

174174
/**
175-
* 균등 분할 처리
175+
* 균등 분할 처리 (원화 기준)
176176
*/
177177
private void processEqualSplit(Expense original, List<GroupMember> members) {
178178
BigDecimal totalAmount = original.getAmount();
179179
int memberCount = members.size();
180-
BigDecimal splitAmount = totalAmount.divide(BigDecimal.valueOf(memberCount), 2, RoundingMode.HALF_UP);
181180

182-
for (GroupMember member : members) {
181+
if (memberCount == 0) {
182+
return; // 처리할 멤버가 없으면 종료
183+
}
184+
185+
// 1. 1인당 기본 분담금 계산 (정수 단위, 버림)
186+
BigDecimal baseSplitAmount = totalAmount.divide(BigDecimal.valueOf(memberCount), 0, RoundingMode.DOWN);
187+
188+
// 2. 나누고 남은 나머지 금액 계산
189+
BigDecimal calculatedTotal = baseSplitAmount.multiply(BigDecimal.valueOf(memberCount));
190+
BigDecimal remainder = totalAmount.subtract(calculatedTotal);
191+
192+
// 3. 나머지 금액(원)을 분배할 횟수 계산
193+
int remainderToDistribute = remainder.intValue();
194+
195+
// 4. 각 멤버에게 분담금 할당
196+
for (int i = 0; i < memberCount; i++) {
197+
GroupMember member = members.get(i);
198+
BigDecimal finalSplitAmount = baseSplitAmount;
199+
200+
// 5. 나머지 금액을 순서대로 1원씩 분배
201+
if (i < remainderToDistribute) {
202+
finalSplitAmount = finalSplitAmount.add(BigDecimal.ONE);
203+
}
204+
183205
// ExpenseSplit 생성
184206
ExpenseSplit split = ExpenseSplit.builder()
185207
.expenseId(original.getExpenseId())
186208
.userId(member.getUser().getUserId())
187-
.amount(splitAmount)
209+
.amount(finalSplitAmount)
188210
.build();
189211
expenseSplitRepository.save(split);
190212

191-
createShareExpense(original, member.getUser().getUserId(), splitAmount);
213+
createShareExpense(original, member.getUser().getUserId(), finalSplitAmount);
192214
}
193215
}
194216

@@ -298,10 +320,20 @@ public ExpenseResponseDTO updateExpense(UUID userId, Long expenseId, UpdateExpen
298320
* 그룹 지출 분담 정보 업데이트
299321
*/
300322
private void updateGroupExpenseSplits(Expense original, List<SplitDataDTO> newSplitData) {
301-
log.info("updateGroupExpenseSplits: {}, {}", original.getSplitType(), newSplitData);
323+
// 1. 기존 분담 데이터 모두 삭제
324+
expenseSplitRepository.deleteByExpenseId(original.getExpenseId());
325+
expenseRepository.deleteByOriginalExpenseId(original.getExpenseId());
326+
327+
// 2. 그룹 멤버 정보 다시 조회
328+
List<GroupMember> groupMembers = groupMemberRepository.findByGroupId(original.getGroupId());
329+
if (groupMembers.isEmpty()) {
330+
throw new CustomException(ErrorCode.GROUP_MEMBER_NOT_FOUND);
331+
}
332+
333+
// 3. 새로운 분할 방식에 따라 분담금 재생성
302334
switch (original.getSplitType()) {
303-
case EQUAL -> updateEqualSplit(original);
304-
case CUSTOM, SPECIFIC -> updateCustomSplit(original, newSplitData);
335+
case EQUAL -> processEqualSplit(original, groupMembers);
336+
case CUSTOM, SPECIFIC -> processCustomSplit(original, newSplitData);
305337
}
306338
}
307339

0 commit comments

Comments
 (0)