feat : initial commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.4.0'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
}
|
||||
|
||||
group = 'com.healthsync'
|
||||
version = '1.0.0'
|
||||
|
||||
java {
|
||||
sourceCompatibility = '21'
|
||||
targetCompatibility = '21'
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
// ✅ WebClient는 Mock에서 사용하지 않으므로 제거 가능하지만 향후 확장을 위해 유지
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
|
||||
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.healthsync.goal;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* Goal Service의 메인 애플리케이션 클래스입니다.
|
||||
* 사용자의 건강 목표 설정 및 미션 관리 기능을 제공합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.healthsync.goal", "com.healthsync.common"})
|
||||
@ConfigurationPropertiesScan
|
||||
public class GoalServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GoalServiceApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
package com.healthsync.goal.application_services;
|
||||
|
||||
import com.healthsync.goal.domain.services.GoalDomainService;
|
||||
import com.healthsync.goal.domain.repositories.GoalRepository;
|
||||
import com.healthsync.goal.dto.*;
|
||||
import com.healthsync.goal.infrastructure.entities.MissionCompletionHistoryEntity;
|
||||
import com.healthsync.goal.infrastructure.entities.UserMissionGoalEntity;
|
||||
import com.healthsync.goal.infrastructure.ports.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 목표 관리 유스케이스입니다.
|
||||
* Clean Architecture의 Application Service 계층에 해당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class GoalUseCase {
|
||||
|
||||
private final GoalDomainService goalDomainService;
|
||||
private final GoalRepository goalRepository;
|
||||
private final UserServicePort userServicePort;
|
||||
private final com.healthsync.goal.domain.ports.IntelligenceServicePort intelligenceServicePort;
|
||||
private final CachePort cachePort;
|
||||
private final EventPublisherPort eventPublisherPort;
|
||||
|
||||
|
||||
/**
|
||||
* 미션을 선택하고 목표를 설정합니다.
|
||||
*
|
||||
* @param request 미션 선택 요청
|
||||
* @return 목표 설정 결과
|
||||
*/
|
||||
public GoalSetupResponse selectMissions(MissionSelectionRequest request) {
|
||||
log.info("미션 선택 처리 시작: memberSerialNumber={}", request.getMemberSerialNumber());
|
||||
|
||||
// 사용자 정보 검증 (간단히 처리)
|
||||
// userServicePort.validateUserExists(request.getMemberSerialNumber());
|
||||
|
||||
// 미션 선택 검증
|
||||
goalDomainService.validateMissionSelection(request);
|
||||
|
||||
// 기존 활성 미션 비활성화
|
||||
goalRepository.deactivateCurrentMissions(request.getMemberSerialNumber());
|
||||
|
||||
// 새 미션 설정 저장
|
||||
String goalId = goalRepository.saveGoalSettings(request);
|
||||
|
||||
// 선택된 미션 정보 구성 - SelectedMissionDetail에서 정보 추출
|
||||
List<SelectedMission> selectedMissions = request.getSelectedMissionIds().stream()
|
||||
.map(missionDetail -> SelectedMission.builder()
|
||||
.missionId(generateMissionId(missionDetail)) // 미션 ID 생성
|
||||
.title(missionDetail.getTitle()) // 제목은 SelectedMissionDetail에서
|
||||
.description(generateMissionDescription(missionDetail)) // 설명 생성
|
||||
.startDate(LocalDate.now().toString())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
// 이벤트 발행 - 미션 ID 목록 추출
|
||||
List<String> missionIdList = request.getSelectedMissionIds().stream()
|
||||
.map(this::generateMissionId)
|
||||
.toList();
|
||||
eventPublisherPort.publishGoalSetEvent(request.getMemberSerialNumber(), missionIdList);
|
||||
|
||||
// 캐시 무효화
|
||||
cachePort.invalidateUserMissionCache(request.getMemberSerialNumber());
|
||||
|
||||
GoalSetupResponse response = GoalSetupResponse.builder()
|
||||
.goalId(goalId)
|
||||
.selectedMissions(selectedMissions)
|
||||
.message("선택하신 미션으로 건강 목표가 설정되었습니다.")
|
||||
.setupCompletedAt(java.time.LocalDateTime.now().toString())
|
||||
.build();
|
||||
|
||||
log.info("미션 선택 처리 완료: memberSerialNumber={}, goalId={}", request.getMemberSerialNumber(), goalId);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정된 활성 미션을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 활성 미션 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public ActiveMissionsResponse getActiveMissions(String memberSerialNumber) {
|
||||
log.info("활성 미션 조회: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
// 캐시 확인
|
||||
ActiveMissionsResponse cachedResponse = cachePort.getActiveMissions(memberSerialNumber);
|
||||
if (cachedResponse != null) {
|
||||
log.info("캐시에서 활성 미션 조회: memberSerialNumber={}", memberSerialNumber);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// 활성 미션 조회
|
||||
List<DailyMission> dailyMissions = goalRepository.findActiveMissionsByUserId(memberSerialNumber);
|
||||
|
||||
// 완료 통계 계산
|
||||
int totalMissions = dailyMissions.size();
|
||||
int todayCompletedCount = (int) dailyMissions.stream()
|
||||
.mapToInt(mission -> mission.isCompletedToday() ? 1 : 0)
|
||||
.sum();
|
||||
|
||||
double completionRate = totalMissions > 0 ?
|
||||
((double) todayCompletedCount / totalMissions) * 100.0 : 0.0;
|
||||
|
||||
// 연속 달성 일수 계산
|
||||
int currentStreak = dailyMissions.stream()
|
||||
.mapToInt(DailyMission::getStreakDays)
|
||||
.max()
|
||||
.orElse(0);
|
||||
|
||||
ActiveMissionsResponse response = ActiveMissionsResponse.builder()
|
||||
.dailyMissions(dailyMissions)
|
||||
.totalMissions(totalMissions)
|
||||
.todayCompletedCount(todayCompletedCount)
|
||||
.completionRate(completionRate)
|
||||
.currentStreak(currentStreak)
|
||||
.bestStreak(calculateBestStreak(memberSerialNumber))
|
||||
.motivationalMessage(generateMotivationalMessage(completionRate))
|
||||
.build();
|
||||
|
||||
// 캐시 저장
|
||||
cachePort.cacheActiveMissions(memberSerialNumber, response);
|
||||
|
||||
log.info("활성 미션 조회 완료: memberSerialNumber={}, totalMissions={}", memberSerialNumber, totalMissions);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 완료를 처리합니다.
|
||||
*
|
||||
* 🎯 핵심 기능:
|
||||
* 1. 미션 완료 기록 (mission_completion_history 테이블)
|
||||
* 2. 목표 달성 여부 확인 (daily_completed_count >= daily_target_count)
|
||||
* 3. 목표 달성 시 HealthSync_Intelligence Python API 호출
|
||||
*
|
||||
* 🐍 Python API 연동:
|
||||
* - 조건: daily_completed_count == daily_target_count
|
||||
* - 호출: POST /api/intelligence/missions/celebrate
|
||||
* - 요청: { userId: long, missionId: long }
|
||||
* - 응답: { congratsMessage: str }
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @param request 미션 완료 요청
|
||||
* @return 미션 완료 결과 (Python 축하 메시지 포함)
|
||||
*/
|
||||
public MissionCompleteResponse completeMission(String missionId, MissionCompleteRequest request) {
|
||||
log.info("미션 완료 처리: memberSerialNumber={}, missionId={}", request.getMemberSerialNumber(), missionId);
|
||||
|
||||
// 미션 완료 기록
|
||||
goalRepository.recordMissionCompletion(missionId, request);
|
||||
|
||||
// 🎯 목표 달성 여부 확인 및 축하 API 호출
|
||||
CelebrationResponse celebrationResponse = checkAndCelebrateMissionAchievement(missionId, request.getMemberSerialNumber());
|
||||
|
||||
// 연속 달성 일수 계산
|
||||
int newStreakDays = calculateNewStreakDays(request.getMemberSerialNumber(), missionId);
|
||||
|
||||
// 총 완료 횟수 조회
|
||||
int totalCompletedCount = goalRepository.getTotalCompletedCount(request.getMemberSerialNumber(), missionId);
|
||||
|
||||
// 성취 메시지 생성 (축하 API 응답 우선 사용)
|
||||
String achievementMessage = celebrationResponse != null && celebrationResponse.getCongratsMessage() != null
|
||||
? celebrationResponse.getCongratsMessage()
|
||||
: generateAchievementMessage(newStreakDays, totalCompletedCount);
|
||||
|
||||
// 캐시 무효화
|
||||
cachePort.invalidateUserMissionCache(request.getMemberSerialNumber());
|
||||
|
||||
// 이벤트 발행
|
||||
eventPublisherPort.publishMissionCompleteEvent(request.getMemberSerialNumber(), missionId, newStreakDays);
|
||||
|
||||
MissionCompleteResponse response = MissionCompleteResponse.builder()
|
||||
.message("미션이 완료되었습니다!")
|
||||
.status("SUCCESS")
|
||||
.achievementMessage(achievementMessage)
|
||||
.newStreakDays(newStreakDays)
|
||||
.totalCompletedCount(totalCompletedCount)
|
||||
.earnedPoints(calculateEarnedPoints(newStreakDays))
|
||||
.build();
|
||||
|
||||
log.info("미션 완료 처리 완료: memberSerialNumber={}, missionId={}, streakDays={}",
|
||||
request.getMemberSerialNumber(), missionId, newStreakDays);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 목표 달성 여부를 확인하고 달성시 축하 API를 호출합니다.
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 축하 응답 (달성하지 않은 경우 null)
|
||||
*/
|
||||
private CelebrationResponse checkAndCelebrateMissionAchievement(String missionId, String memberSerialNumber) {
|
||||
log.info("🎯 [MISSION_ACHIEVEMENT] 목표 달성 여부 확인: memberSerialNumber={}, missionId={}",
|
||||
memberSerialNumber, missionId);
|
||||
|
||||
try {
|
||||
// 오늘의 미션 완료 이력 조회
|
||||
boolean isTargetAchieved = goalRepository.isTodayTargetAchieved(memberSerialNumber, missionId);
|
||||
|
||||
if (isTargetAchieved) {
|
||||
log.info("🎉 [MISSION_ACHIEVEMENT] 목표 달성 확인! Python 축하 API 호출: memberSerialNumber={}, missionId={}",
|
||||
memberSerialNumber, missionId);
|
||||
|
||||
try {
|
||||
// 🔧 Python API 스펙에 맞춘 축하 요청 생성 (userId: long, missionId: long)
|
||||
CelebrationRequest celebrationRequest = CelebrationRequest.builder()
|
||||
.userId(Long.parseLong(memberSerialNumber)) // 🔧 String → Long 변환
|
||||
.missionId(Long.parseLong(missionId)) // 🔧 String → Long 변환 (큰 ID 지원)
|
||||
.build();
|
||||
|
||||
// HealthSync_Intelligence Python Service의 축하 API 호출
|
||||
return intelligenceServicePort.celebrateMissionAchievement(celebrationRequest);
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("❌ [MISSION_ACHIEVEMENT] 숫자 변환 실패: memberSerialNumber={}, missionId={}, error={}",
|
||||
memberSerialNumber, missionId, e.getMessage());
|
||||
|
||||
// 🔧 숫자 변환 실패시 Fallback 축하 메시지 반환
|
||||
return CelebrationResponse.builder()
|
||||
.congratsMessage("🎉 목표를 달성하셨습니다! 훌륭해요! 💪✨")
|
||||
.build();
|
||||
}
|
||||
} else {
|
||||
log.info("📝 [MISSION_ACHIEVEMENT] 목표 미달성: memberSerialNumber={}, missionId={}",
|
||||
memberSerialNumber, missionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [MISSION_ACHIEVEMENT] 목표 달성 확인 중 오류: memberSerialNumber={}, missionId={}, error={}",
|
||||
memberSerialNumber, missionId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 점진적 완료 처리 메서드 추가
|
||||
private MissionCompleteResponse processIncrementalCompletion(String missionId, MissionCompleteRequest request, UserMissionGoalEntity mission) {
|
||||
// 오늘 완료 기록 조회/생성
|
||||
MissionCompletionHistoryEntity todayCompletion = goalRepository.findOrCreateTodayCompletion(
|
||||
missionId, request.getMemberSerialNumber(), mission.getDailyTargetCount());
|
||||
|
||||
// 목표 초과 방지
|
||||
int currentCount = todayCompletion.getDailyCompletedCount();
|
||||
int targetCount = todayCompletion.getDailyTargetCount();
|
||||
|
||||
if (currentCount >= targetCount) {
|
||||
throw new IllegalStateException("이미 오늘 목표를 달성했습니다.");
|
||||
}
|
||||
|
||||
// 진행도 증가
|
||||
int newCount = Math.min(currentCount + request.getIncrementCount(), targetCount);
|
||||
todayCompletion.setDailyCompletedCount(newCount);
|
||||
goalRepository.saveMissionCompletion(todayCompletion);
|
||||
|
||||
// 결과 계산
|
||||
boolean isTargetAchieved = newCount >= targetCount;
|
||||
double achievementRate = (double) newCount / targetCount * 100.0;
|
||||
|
||||
// 목표 달성 시 추가 처리
|
||||
String celebrationMessage = null;
|
||||
String achievementMessage = null;
|
||||
int streakDays = 0;
|
||||
int earnedPoints = 5; // 기본 포인트
|
||||
|
||||
if (isTargetAchieved) {
|
||||
streakDays = goalRepository.calculateStreakDays(request.getMemberSerialNumber(), missionId);
|
||||
celebrationMessage = "🎉 오늘 목표를 달성했어요!";
|
||||
achievementMessage = generateAchievementMessage(streakDays, newCount);
|
||||
earnedPoints += 10; // 목표 달성 보너스
|
||||
|
||||
// 이벤트 발행
|
||||
eventPublisherPort.publishMissionCompleteEvent(request.getMemberSerialNumber(), missionId, streakDays);
|
||||
}
|
||||
|
||||
// 캐시 무효화
|
||||
cachePort.invalidateUserMissionCache(request.getMemberSerialNumber());
|
||||
|
||||
// 응답 생성
|
||||
return MissionCompleteResponse.builder()
|
||||
.message(isTargetAchieved ? "🎉 오늘 목표를 달성했어요!" : "좋아요! 계속 진행해보세요!")
|
||||
.status("SUCCESS")
|
||||
.achievementMessage(achievementMessage)
|
||||
.newStreakDays(streakDays)
|
||||
.totalCompletedCount(goalRepository.getTotalCompletedCount(request.getMemberSerialNumber(), missionId))
|
||||
.earnedPoints(earnedPoints)
|
||||
.currentCount(newCount)
|
||||
.targetCount(targetCount)
|
||||
.isTargetAchieved(isTargetAchieved)
|
||||
.achievementRate(achievementRate)
|
||||
.completionType("INCREMENT")
|
||||
.celebrationMessage(celebrationMessage)
|
||||
.build();
|
||||
}
|
||||
|
||||
// 전체 완료 처리 메서드 추가 (기존 로직 유지)
|
||||
private MissionCompleteResponse processFullCompletion(String missionId, MissionCompleteRequest request, UserMissionGoalEntity mission) {
|
||||
// 기존 완료 로직 수행
|
||||
goalRepository.recordMissionCompletion(missionId, request);
|
||||
|
||||
// 연속 달성 일수 계산
|
||||
int newStreakDays = calculateNewStreakDays(request.getMemberSerialNumber(), missionId);
|
||||
|
||||
// 총 완료 횟수 조회
|
||||
int totalCompletedCount = goalRepository.getTotalCompletedCount(request.getMemberSerialNumber(), missionId);
|
||||
|
||||
// 성취 메시지 생성
|
||||
String achievementMessage = generateAchievementMessage(newStreakDays, totalCompletedCount);
|
||||
|
||||
// 캐시 무효화 및 이벤트 발행
|
||||
cachePort.invalidateUserMissionCache(request.getMemberSerialNumber());
|
||||
eventPublisherPort.publishMissionCompleteEvent(request.getMemberSerialNumber(), missionId, newStreakDays);
|
||||
|
||||
// 오늘 완료 상태 조회
|
||||
MissionCompletionHistoryEntity todayCompletion = goalRepository.findOrCreateTodayCompletion(
|
||||
missionId, request.getMemberSerialNumber(), mission.getDailyTargetCount());
|
||||
|
||||
return MissionCompleteResponse.builder()
|
||||
.message("미션이 완료되었습니다!")
|
||||
.status("SUCCESS")
|
||||
.achievementMessage(achievementMessage)
|
||||
.newStreakDays(newStreakDays)
|
||||
.totalCompletedCount(totalCompletedCount)
|
||||
.earnedPoints(calculateEarnedPoints(newStreakDays))
|
||||
.currentCount(todayCompletion.getDailyCompletedCount())
|
||||
.targetCount(todayCompletion.getDailyTargetCount())
|
||||
.isTargetAchieved(true)
|
||||
.achievementRate(100.0)
|
||||
.completionType("FULL")
|
||||
.celebrationMessage(achievementMessage)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 달성 이력을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param startDate 시작일
|
||||
* @param endDate 종료일
|
||||
* @param missionIds 미션 ID 목록
|
||||
* @return 미션 달성 이력
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public MissionHistoryResponse getMissionHistory(String memberSerialNumber, String startDate, String endDate, String missionIds) {
|
||||
log.info("미션 이력 조회: memberSerialNumber={}, period={} to {}", memberSerialNumber, startDate, endDate);
|
||||
|
||||
// 캐시 키 생성
|
||||
String cacheKey = String.format("mission_history:%s:%s:%s:%s", memberSerialNumber, startDate, endDate, missionIds);
|
||||
|
||||
// 캐시 확인
|
||||
MissionHistoryResponse cachedResponse = cachePort.getMissionHistory(cacheKey);
|
||||
if (cachedResponse != null) {
|
||||
log.info("캐시에서 미션 이력 조회: memberSerialNumber={}", memberSerialNumber);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// 기본값 설정
|
||||
if (startDate == null) startDate = LocalDate.now().minusMonths(1).toString();
|
||||
if (endDate == null) endDate = LocalDate.now().toString();
|
||||
|
||||
// 미션 이력 조회
|
||||
List<MissionStats> missionStats = goalRepository.findMissionHistoryByPeriod(memberSerialNumber, startDate, endDate, missionIds);
|
||||
|
||||
// 실제 데이터를 기반으로 통계 계산
|
||||
AchievementStats achievementStats = calculateStatsFromMissionData(missionStats);
|
||||
|
||||
// 차트 데이터 생성
|
||||
Map<String, Object> chartData = goalDomainService.generateChartData(missionStats);
|
||||
|
||||
// 인사이트 생성
|
||||
List<String> insights = goalDomainService.analyzeProgressPatterns(missionStats);
|
||||
|
||||
MissionHistoryResponse response = MissionHistoryResponse.builder()
|
||||
.totalAchievementRate(achievementStats.getTotalAchievementRate())
|
||||
.periodAchievementRate(achievementStats.getPeriodAchievementRate())
|
||||
.bestStreak(achievementStats.getBestStreak())
|
||||
.missionStats(missionStats)
|
||||
.chartData(chartData)
|
||||
.period(Period.builder()
|
||||
.startDate(startDate)
|
||||
.endDate(endDate)
|
||||
.build())
|
||||
.insights(insights)
|
||||
.build();
|
||||
|
||||
// 캐시 저장
|
||||
cachePort.cacheMissionHistory(cacheKey, response);
|
||||
|
||||
log.info("미션 이력 조회 완료: memberSerialNumber={}, achievementRate={}", memberSerialNumber, response.getTotalAchievementRate());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 데이터를 기반으로 달성 통계를 계산합니다.
|
||||
*
|
||||
* @param missionStats 미션 통계 목록
|
||||
* @return 달성 통계
|
||||
*/
|
||||
private AchievementStats calculateStatsFromMissionData(List<MissionStats> missionStats) {
|
||||
if (missionStats.isEmpty()) {
|
||||
log.info("미션 데이터가 없어 기본값으로 통계 반환");
|
||||
return AchievementStats.builder()
|
||||
.totalAchievementRate(0.0)
|
||||
.periodAchievementRate(0.0)
|
||||
.bestStreak(0)
|
||||
.completedDays(0)
|
||||
.totalDays(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// 평균 달성률 계산
|
||||
double avgAchievementRate = missionStats.stream()
|
||||
.mapToDouble(MissionStats::getAchievementRate)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
// 총 완료 일수와 전체 일수 합계
|
||||
int totalCompletedDays = missionStats.stream()
|
||||
.mapToInt(MissionStats::getCompletedDays)
|
||||
.sum();
|
||||
|
||||
int totalDays = missionStats.stream()
|
||||
.mapToInt(MissionStats::getTotalDays)
|
||||
.sum();
|
||||
|
||||
// 최고 연속 달성 계산 (간단한 로직)
|
||||
int bestStreak = missionStats.stream()
|
||||
.mapToInt(stat -> (int) (stat.getAchievementRate() / 10)) // 임시 계산
|
||||
.max()
|
||||
.orElse(0);
|
||||
|
||||
log.info("미션 통계 계산 완료: 평균달성률={}, 총완료일수={}, 전체일수={}", avgAchievementRate, totalCompletedDays, totalDays);
|
||||
|
||||
return AchievementStats.builder()
|
||||
.totalAchievementRate(avgAchievementRate)
|
||||
.periodAchievementRate(avgAchievementRate)
|
||||
.bestStreak(bestStreak)
|
||||
.completedDays(totalCompletedDays)
|
||||
.totalDays(totalDays)
|
||||
.build();
|
||||
}
|
||||
/**
|
||||
* 미션을 재설정합니다.
|
||||
*
|
||||
* @param request 미션 재설정 요청
|
||||
* @return 미션 재설정 결과
|
||||
*/
|
||||
public MissionResetResponse resetMissions(MissionResetRequest request) {
|
||||
log.info("미션 재설정: memberSerialNumber={}, reason={}", request.getMemberSerialNumber(), request.getReason());
|
||||
|
||||
// 현재 활성 미션 비활성화 (기존 구현 사용)
|
||||
goalRepository.deactivateCurrentMissions(request.getMemberSerialNumber());
|
||||
|
||||
// 새로운 미션 추천 요청 (기존 구현 사용)
|
||||
List<RecommendedMission> newRecommendations = intelligenceServicePort
|
||||
.getNewMissionRecommendations(request.getMemberSerialNumber(), request.getReason());
|
||||
|
||||
// 캐시 무효화
|
||||
cachePort.invalidateUserMissionCache(request.getMemberSerialNumber());
|
||||
|
||||
// 이벤트 발행 (기존 구현 사용)
|
||||
eventPublisherPort.publishMissionResetEvent(request.getMemberSerialNumber(), request.getReason());
|
||||
|
||||
MissionResetResponse response = MissionResetResponse.builder()
|
||||
.message("미션이 재설정되었습니다.")
|
||||
.newRecommendations(newRecommendations)
|
||||
.resetCompletedAt(java.time.LocalDateTime.now().toString())
|
||||
.build();
|
||||
|
||||
log.info("미션 재설정 완료: memberSerialNumber={}", request.getMemberSerialNumber());
|
||||
return response;
|
||||
}
|
||||
|
||||
// === Private Helper Methods ===
|
||||
|
||||
/**
|
||||
* SelectedMissionDetail로부터 고유한 미션 ID를 생성합니다.
|
||||
*/
|
||||
private String generateMissionId(SelectedMissionDetail missionDetail) {
|
||||
if (missionDetail == null || missionDetail.getTitle() == null) {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
// 제목을 기반으로 ID 생성 + UUID 일부 추가 (중복 방지)
|
||||
String baseId = missionDetail.getTitle()
|
||||
.replaceAll("[^가-힣a-zA-Z0-9]", "_")
|
||||
.toLowerCase()
|
||||
.replaceAll("_+", "_")
|
||||
.replaceAll("^_|_$", "");
|
||||
|
||||
String shortUuid = UUID.randomUUID().toString().substring(0, 8);
|
||||
return String.format("mission_%s_%s", baseId, shortUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectedMissionDetail로부터 미션 설명을 생성합니다.
|
||||
*/
|
||||
private String generateMissionDescription(SelectedMissionDetail missionDetail) {
|
||||
return String.format("%s (일일 %d회) - %s",
|
||||
missionDetail.getTitle(),
|
||||
missionDetail.getDaily_target_count(),
|
||||
missionDetail.getReason());
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 메서드들 - 호환성을 위해 유지
|
||||
*/
|
||||
private String getMissionTitle(String missionId) {
|
||||
// 실제 구현에서는 미션 ID로 제목을 조회
|
||||
return "미션 제목"; // placeholder
|
||||
}
|
||||
|
||||
private String getMissionDescription(String missionId) {
|
||||
// 실제 구현에서는 미션 ID로 설명을 조회
|
||||
return "미션 설명"; // placeholder
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 GoalUseCase에 있던 메서드들 (기존 구현 유지)
|
||||
*/
|
||||
private int calculateNewStreakDays(String memberSerialNumber, String missionId) {
|
||||
// 기존 구현 로직 유지
|
||||
return 1; // placeholder - 실제로는 연속 일수 계산
|
||||
}
|
||||
|
||||
private String generateAchievementMessage(int streakDays, int totalCount) {
|
||||
if (streakDays >= 7) {
|
||||
return String.format("🔥 대단해요! %d일 연속 달성!", streakDays);
|
||||
} else if (streakDays >= 3) {
|
||||
return String.format("💪 좋아요! %d일 연속 달성 중!", streakDays);
|
||||
} else {
|
||||
return "🌟 오늘도 목표 달성! 계속 화이팅!";
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateEarnedPoints(int streakDays) {
|
||||
// 기본 포인트 + 연속 달성 보너스 (기존 구현)
|
||||
int basePoints = 10;
|
||||
int streakBonus = Math.min(streakDays * 2, 50); // 최대 50점
|
||||
return basePoints + streakBonus;
|
||||
}
|
||||
|
||||
private int calculateBestStreak(String memberSerialNumber) {
|
||||
// 기존 구현 - Repository에서 조회하지 않고 간단히 처리
|
||||
return 7; // placeholder
|
||||
}
|
||||
|
||||
private String generateMotivationalMessage(double completionRate) {
|
||||
if (completionRate >= 80) {
|
||||
return "🔥 오늘도 대단해요! 이런 페이스로 계속 가세요!";
|
||||
} else if (completionRate >= 50) {
|
||||
return "💪 잘하고 있어요! 조금만 더 힘내세요!";
|
||||
} else {
|
||||
return "🌱 시작이 반이에요! 작은 걸음부터 차근차근 해봐요!";
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateEarnedPoints(String missionId, int streakDays) {
|
||||
// 오버로드된 메서드 - 위의 calculateEarnedPoints(int)를 호출
|
||||
return calculateEarnedPoints(streakDays);
|
||||
}
|
||||
|
||||
private double calculateTotalAchievementRate(List<MissionStats> missionStats) {
|
||||
if (missionStats.isEmpty()) return 0.0;
|
||||
|
||||
return missionStats.stream()
|
||||
.mapToDouble(MissionStats::getAchievementRate)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
}
|
||||
|
||||
private double calculatePeriodAchievementRate(List<MissionStats> missionStats) {
|
||||
// 기간별 달성률 계산 로직
|
||||
return calculateTotalAchievementRate(missionStats);
|
||||
}
|
||||
|
||||
private Object generateChartData(List<MissionStats> missionStats, String startDate, String endDate) {
|
||||
// 차트 데이터 생성 로직
|
||||
return new Object(); // placeholder
|
||||
}
|
||||
|
||||
private List<String> generateInsights(List<MissionStats> missionStats, double achievementRate) {
|
||||
List<String> insights = new java.util.ArrayList<>();
|
||||
|
||||
if (achievementRate >= 80) {
|
||||
insights.add("훌륭한 성과를 보이고 있습니다!");
|
||||
} else if (achievementRate >= 60) {
|
||||
insights.add("꾸준히 목표를 향해 나아가고 있어요.");
|
||||
} else {
|
||||
insights.add("조금 더 꾸준함이 필요해 보여요.");
|
||||
}
|
||||
|
||||
return insights;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.healthsync.goal.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Goal Service의 보안 설정을 관리하는 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class GoalSecurityConfig {
|
||||
|
||||
/**
|
||||
* Security Filter Chain을 구성합니다.
|
||||
*
|
||||
* @param http HttpSecurity
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception 예외
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger UI 관련 경로들 모두 허용
|
||||
.requestMatchers("/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**", "/api-docs/**").permitAll()
|
||||
.requestMatchers("/webjars/**").permitAll()
|
||||
.requestMatchers("/swagger-resources/**").permitAll()
|
||||
// Actuator 허용
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
// 🎯 API 경로들 허용 (개발용)
|
||||
.requestMatchers("/api/**").permitAll()
|
||||
// 나머지는 인증 필요
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 설정을 구성합니다.
|
||||
*
|
||||
* @return CorsConfigurationSource
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// goal-service/src/main/java/com/healthsync/goal/config/SwaggerAccessController.java
|
||||
package com.healthsync.goal.config;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Swagger UI 접근을 위한 경로 처리 컨트롤러입니다.
|
||||
* Ingress에서 /api/goals 경로로 들어오는 요청을 처리합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class SwaggerAccessController {
|
||||
|
||||
/**
|
||||
* /api/goals/swagger-ui.html 요청을 /swagger-ui.html로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/swagger-ui.html")
|
||||
public String redirectToSwaggerUi(HttpServletRequest request) {
|
||||
log.info("Swagger UI 접근 요청: {}", request.getRequestURI());
|
||||
return "redirect:/swagger-ui.html";
|
||||
}
|
||||
|
||||
/**
|
||||
* /api/goals/swagger-ui/** 요청을 /swagger-ui/**로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/swagger-ui/**")
|
||||
public String redirectToSwaggerUiResources(HttpServletRequest request) {
|
||||
String originalPath = request.getRequestURI();
|
||||
String redirectPath = originalPath.replace("/api/goals", "");
|
||||
log.info("Swagger UI 리소스 리다이렉트: {} -> {}", originalPath, redirectPath);
|
||||
return "redirect:" + redirectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* /api/goals/v3/api-docs 요청을 /v3/api-docs로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/v3/api-docs")
|
||||
public String redirectToApiDocs(HttpServletRequest request) {
|
||||
log.info("API Docs 접근 요청: {}", request.getRequestURI());
|
||||
return "redirect:/v3/api-docs";
|
||||
}
|
||||
|
||||
/**
|
||||
* /api/goals/v3/api-docs/** 요청을 /v3/api-docs/**로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/v3/api-docs/**")
|
||||
public String redirectToApiDocsResources(HttpServletRequest request) {
|
||||
String originalPath = request.getRequestURI();
|
||||
String redirectPath = originalPath.replace("/api/goals", "");
|
||||
log.info("API Docs 리소스 리다이렉트: {} -> {}", originalPath, redirectPath);
|
||||
return "redirect:" + redirectPath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* /api/goals/actuator/health 요청을 /actuator/health로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/actuator/health")
|
||||
public String redirectToActuatorHealth(HttpServletRequest request) {
|
||||
log.info("Actuator Health 접근 요청: {}", request.getRequestURI());
|
||||
return "redirect:/actuator/health";
|
||||
}
|
||||
|
||||
/**
|
||||
* /api/goals/actuator/** 요청을 /actuator/**로 리다이렉트합니다.
|
||||
*/
|
||||
@GetMapping("/api/goals/actuator/**")
|
||||
public String redirectToActuatorResources(HttpServletRequest request) {
|
||||
String originalPath = request.getRequestURI();
|
||||
String redirectPath = originalPath.replace("/api/goals", "");
|
||||
log.info("Actuator 리소스 리다이렉트: {} -> {}", originalPath, redirectPath);
|
||||
return "redirect:" + redirectPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.healthsync.goal.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* WebClient 설정 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
/**
|
||||
* WebClient Bean을 생성합니다.
|
||||
*
|
||||
* @return WebClient 인스턴스
|
||||
*/
|
||||
@Bean
|
||||
public WebClient webClient() {
|
||||
return WebClient.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.healthsync.goal.domain.repositories;
|
||||
|
||||
import com.healthsync.goal.dto.*;
|
||||
import com.healthsync.goal.infrastructure.entities.MissionCompletionHistoryEntity;
|
||||
import com.healthsync.goal.infrastructure.entities.UserMissionGoalEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 목표 데이터 저장소 인터페이스입니다.
|
||||
* Clean Architecture의 Domain 계층에서 정의합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface GoalRepository {
|
||||
|
||||
/**
|
||||
* 목표 설정을 저장합니다.
|
||||
*
|
||||
* @param request 미션 선택 요청
|
||||
* @return 목표 ID
|
||||
*/
|
||||
String saveGoalSettings(MissionSelectionRequest request);
|
||||
|
||||
/**
|
||||
* 사용자의 활성 미션을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 활성 미션 목록
|
||||
*/
|
||||
List<DailyMission> findActiveMissionsByUserId(String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 현재 미션을 비활성화합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
*/
|
||||
void deactivateCurrentMissions(String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 미션 완료를 기록합니다.
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @param request 미션 완료 요청
|
||||
*/
|
||||
void recordMissionCompletion(String missionId, MissionCompleteRequest request);
|
||||
|
||||
/**
|
||||
* 총 완료 횟수를 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param missionId 미션 ID
|
||||
* @return 총 완료 횟수
|
||||
*/
|
||||
int getTotalCompletedCount(String memberSerialNumber, String missionId);
|
||||
|
||||
/**
|
||||
* 기간별 미션 이력을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param startDate 시작일
|
||||
* @param endDate 종료일
|
||||
* @param missionIds 미션 ID 목록
|
||||
* @return 미션 통계 목록
|
||||
*/
|
||||
List<MissionStats> findMissionHistoryByPeriod(String memberSerialNumber, String startDate, String endDate, String missionIds);
|
||||
|
||||
|
||||
/**
|
||||
* 미션 ID와 사용자로 미션을 조회합니다.
|
||||
*/
|
||||
UserMissionGoalEntity findMissionByIdAndUser(String missionId, String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 오늘의 완료 기록을 조회하거나 생성합니다.
|
||||
*/
|
||||
MissionCompletionHistoryEntity findOrCreateTodayCompletion(String missionId, String memberSerialNumber, Integer dailyTargetCount);
|
||||
|
||||
/**
|
||||
* 미션 완료 기록을 저장합니다.
|
||||
*/
|
||||
void saveMissionCompletion(MissionCompletionHistoryEntity completion);
|
||||
|
||||
/**
|
||||
* 연속 달성일수를 계산합니다.
|
||||
*/
|
||||
int calculateStreakDays(String memberSerialNumber, String missionId);
|
||||
|
||||
/**
|
||||
* 오늘 해당 미션의 목표를 달성했는지 확인합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param missionId 미션 ID
|
||||
* @return 목표 달성 여부
|
||||
*/
|
||||
boolean isTodayTargetAchieved(String memberSerialNumber, String missionId);
|
||||
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.healthsync.goal.domain.services;
|
||||
|
||||
import com.healthsync.common.exception.ValidationException;
|
||||
import com.healthsync.goal.dto.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 목표 관련 비즈니스 로직을 처리하는 도메인 서비스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GoalDomainService {
|
||||
|
||||
/**
|
||||
* 미션 선택을 검증합니다.
|
||||
*
|
||||
* @param request 미션 선택 요청
|
||||
*/
|
||||
public void validateMissionSelection(MissionSelectionRequest request) {
|
||||
if (request.getSelectedMissionIds() == null || request.getSelectedMissionIds().isEmpty()) {
|
||||
throw new ValidationException("최소 1개 이상의 미션을 선택해야 합니다.");
|
||||
}
|
||||
|
||||
if (request.getSelectedMissionIds().size() > 5) {
|
||||
throw new ValidationException("최대 5개까지 미션을 선택할 수 있습니다.");
|
||||
}
|
||||
|
||||
// 중복 미션 검사
|
||||
if (request.getSelectedMissionIds().size() != request.getSelectedMissionIds().stream().distinct().count()) {
|
||||
throw new ValidationException("중복된 미션이 선택되었습니다.");
|
||||
}
|
||||
|
||||
log.info("미션 선택 검증 완료: memberSerialNumber={}, missionCount={}", request.getMemberSerialNumber(), request.getSelectedMissionIds().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 완료를 검증합니다.
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @param userId 사용자 ID
|
||||
*/
|
||||
public void validateMissionCompletion(String missionId, String userId) {
|
||||
if (missionId == null || missionId.trim().isEmpty()) {
|
||||
throw new ValidationException("미션 ID가 필요합니다.");
|
||||
}
|
||||
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
throw new ValidationException("사용자 ID가 필요합니다.");
|
||||
}
|
||||
|
||||
log.info("미션 완료 검증 완료: userId={}, missionId={}", userId, missionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 연속 달성 일수를 계산합니다.
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param missionId 미션 ID
|
||||
* @return 연속 달성 일수
|
||||
*/
|
||||
public int calculateStreakDays(String userId, String missionId) {
|
||||
// 실제 구현에서는 DB에서 연속 달성 일수 계산
|
||||
// Mock 데이터로 반환
|
||||
return (int) (Math.random() * 10) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 데이터를 생성합니다.
|
||||
*
|
||||
* @param missionStats 미션 통계 목록
|
||||
* @return 차트 데이터
|
||||
*/
|
||||
public Map<String, Object> generateChartData(List<MissionStats> missionStats) { // Object -> Map<String, Object>로 변경
|
||||
Map<String, Object> chartData = new HashMap<>();
|
||||
|
||||
// 미션별 달성률 데이터
|
||||
List<Map<String, Object>> achievementData = missionStats.stream()
|
||||
.map(stat -> {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("missionTitle", stat.getTitle());
|
||||
data.put("achievementRate", stat.getAchievementRate());
|
||||
data.put("completedDays", stat.getCompletedDays());
|
||||
return data;
|
||||
})
|
||||
.toList();
|
||||
|
||||
chartData.put("achievementByMission", achievementData);
|
||||
chartData.put("chartType", "bar");
|
||||
chartData.put("title", "미션별 달성률");
|
||||
|
||||
return chartData; // 이미 Map<String, Object>를 반환하고 있었음
|
||||
}
|
||||
|
||||
/**
|
||||
* 진행 패턴을 분석하여 인사이트를 생성합니다.
|
||||
*
|
||||
* @param missionStats 미션 통계 목록
|
||||
* @return 인사이트 목록
|
||||
*/
|
||||
public List<String> analyzeProgressPatterns(List<MissionStats> missionStats) {
|
||||
// 실제 구현에서는 통계 데이터를 분석하여 패턴 발견
|
||||
return List.of(
|
||||
"💪 운동 미션의 달성률이 생활습관 미션보다 15% 높습니다.",
|
||||
"📈 최근 1주일간 미션 달성률이 20% 향상되었습니다.",
|
||||
"🎯 '어깨 스트레칭' 미션이 가장 높은 달성률(95%)을 보입니다.",
|
||||
"⏰ 오전에 시작하는 미션들의 달성률이 더 높은 경향을 보입니다."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 달성 통계 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "달성 통계")
|
||||
public class AchievementStats {
|
||||
|
||||
@Schema(description = "전체 달성률 (%)")
|
||||
private double totalAchievementRate;
|
||||
|
||||
@Schema(description = "기간 내 달성률 (%)")
|
||||
private double periodAchievementRate;
|
||||
|
||||
@Schema(description = "최고 연속 달성 일수")
|
||||
private int bestStreak;
|
||||
|
||||
@Schema(description = "완료 일수")
|
||||
private int completedDays;
|
||||
|
||||
@Schema(description = "총 일수")
|
||||
private int totalDays;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 활성 미션 응답 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "활성 미션 응답")
|
||||
public class ActiveMissionsResponse {
|
||||
|
||||
@Schema(description = "일일 미션 목록")
|
||||
private List<DailyMission> dailyMissions;
|
||||
|
||||
@Schema(description = "총 미션 수")
|
||||
private int totalMissions;
|
||||
|
||||
@Schema(description = "오늘 완료된 미션 수")
|
||||
private int todayCompletedCount;
|
||||
|
||||
@Schema(description = "완료율 (%)")
|
||||
private double completionRate;
|
||||
|
||||
@Schema(description = "현재 연속 달성 일수")
|
||||
private int currentStreak;
|
||||
|
||||
@Schema(description = "최고 연속 달성 일수")
|
||||
private int bestStreak;
|
||||
|
||||
@Schema(description = "동기부여 메시지")
|
||||
private String motivationalMessage;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* HealthSync_Intelligence Python API의 celebration 엔드포인트 호출용 요청 모델
|
||||
* Python 스펙: userId(int), missionId(int) - Python int는 Java Long에 해당
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CelebrationRequest {
|
||||
|
||||
@JsonProperty("userId")
|
||||
private Long userId; // 🔧 Integer → Long 변경 (Python int = Java Long)
|
||||
|
||||
@JsonProperty("missionId")
|
||||
private Long missionId; // 🔧 Integer → Long 변경 (큰 ID 값 지원)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* HealthSync_Intelligence Python API의 celebration 응답 모델
|
||||
* Python 스펙: congratsMessage(str)
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CelebrationResponse {
|
||||
|
||||
@JsonProperty("congratsMessage")
|
||||
private String congratsMessage;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 완료 데이터 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "완료 데이터")
|
||||
public class CompletionData {
|
||||
|
||||
@Schema(description = "사용자 ID")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "완료 여부")
|
||||
private boolean completed;
|
||||
|
||||
@Schema(description = "완료 시간")
|
||||
private String completedAt;
|
||||
|
||||
@Schema(description = "메모")
|
||||
private String notes;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 일일 미션 DTO 클래스입니다.
|
||||
* API 설계서와 실제 사용에 맞춰 필드를 정리했습니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "일일 미션")
|
||||
public class DailyMission {
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "미션 제목")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "미션 설명")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "일일 목표 횟수")
|
||||
private int targetCount;
|
||||
|
||||
@Schema(description = "미션 상태 (ACTIVE, COMPLETED, PENDING)")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "오늘 완료 여부")
|
||||
private boolean completedToday;
|
||||
|
||||
@Schema(description = "오늘 완료한 횟수")
|
||||
private int completedCount;
|
||||
|
||||
@Schema(description = "연속 달성 일수")
|
||||
private int streakDays;
|
||||
|
||||
@Schema(description = "다음 알림 시간")
|
||||
private String nextReminderTime;
|
||||
|
||||
/**
|
||||
* 미션 완료율을 계산합니다.
|
||||
*/
|
||||
public double getCompletionRate() {
|
||||
if (targetCount == 0) return 0.0;
|
||||
return Math.min(100.0, (double) completedCount / targetCount * 100.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 목표 달성 여부를 확인합니다.
|
||||
*/
|
||||
public boolean isTargetAchieved() {
|
||||
return completedCount >= targetCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 목표 설정 응답 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "목표 설정 응답")
|
||||
public class GoalSetupResponse {
|
||||
|
||||
@Schema(description = "목표 ID")
|
||||
private String goalId;
|
||||
|
||||
@Schema(description = "선택된 미션 목록")
|
||||
private List<SelectedMission> selectedMissions;
|
||||
|
||||
@Schema(description = "응답 메시지")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "설정 완료 시간")
|
||||
private String setupCompletedAt;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 미션 정보 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 정보")
|
||||
public class Mission {
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "미션 제목")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "미션 설명")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "미션 카테고리")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "난이도")
|
||||
private String difficulty;
|
||||
|
||||
@Schema(description = "건강 효과")
|
||||
private String healthBenefit;
|
||||
|
||||
@Schema(description = "직업군 관련성")
|
||||
private String occupationRelevance;
|
||||
|
||||
@Schema(description = "예상 소요시간(분)")
|
||||
private int estimatedTimeMinutes;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 미션 완료 요청 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 완료 요청")
|
||||
public class MissionCompleteRequest {
|
||||
|
||||
@NotBlank(message = "회원 시리얼 번호는 필수입니다.")
|
||||
@Schema(description = "회원 시리얼 번호")
|
||||
private String memberSerialNumber;
|
||||
|
||||
@NotNull(message = "완료 여부는 필수입니다.")
|
||||
@Schema(description = "완료 여부")
|
||||
private boolean completed;
|
||||
|
||||
@Schema(description = "완료 시간")
|
||||
private String completedAt;
|
||||
|
||||
@Schema(description = "메모")
|
||||
private String notes;
|
||||
|
||||
// ✅ 새로 추가할 필드들
|
||||
@Schema(description = "증가할 완료 횟수 (점진적 완료용)", example = "1")
|
||||
@Min(value = 1, message = "증가 횟수는 1 이상이어야 합니다")
|
||||
@Builder.Default
|
||||
private Integer incrementCount = 1;
|
||||
|
||||
@Schema(description = "완료 유형 - INCREMENT: 점진적 완료, FULL: 전체 완료",
|
||||
example = "INCREMENT", allowableValues = {"INCREMENT", "FULL"})
|
||||
@Builder.Default
|
||||
private String completionType = "INCREMENT";
|
||||
|
||||
// ✅ 새로 추가할 메서드들
|
||||
public boolean isIncrementMode() {
|
||||
return !completed && "INCREMENT".equals(completionType);
|
||||
}
|
||||
|
||||
public boolean isFullCompleteMode() {
|
||||
return completed || "FULL".equals(completionType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 미션 완료 응답 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 완료 응답")
|
||||
public class MissionCompleteResponse {
|
||||
|
||||
@Schema(description = "응답 메시지")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "처리 상태")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "성취 메시지")
|
||||
private String achievementMessage;
|
||||
|
||||
@Schema(description = "새로운 연속 달성 일수")
|
||||
private int newStreakDays;
|
||||
|
||||
@Schema(description = "총 완료 횟수")
|
||||
private int totalCompletedCount;
|
||||
|
||||
@Schema(description = "획득 포인트")
|
||||
private int earnedPoints;
|
||||
|
||||
@Schema(description = "현재 완료 횟수 (오늘)", example = "4")
|
||||
private Integer currentCount;
|
||||
|
||||
@Schema(description = "목표 횟수 (오늘)", example = "8")
|
||||
private Integer targetCount;
|
||||
|
||||
@Schema(description = "목표 달성 여부", example = "false")
|
||||
private Boolean isTargetAchieved;
|
||||
|
||||
@Schema(description = "달성률", example = "50.0")
|
||||
private Double achievementRate;
|
||||
|
||||
@Schema(description = "완료 유형", example = "INCREMENT")
|
||||
private String completionType;
|
||||
|
||||
@Schema(description = "축하 메시지 (목표 달성 시)", example = "🎉 오늘 목표를 달성했어요!")
|
||||
private String celebrationMessage;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* AI API에서 조회한 미션 상세 정보 응답 DTO입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MissionDetailResponse {
|
||||
|
||||
/**
|
||||
* 미션 ID
|
||||
*/
|
||||
private String missionId;
|
||||
|
||||
/**
|
||||
* 미션 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 미션 설명
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 미션 카테고리
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 난이도
|
||||
*/
|
||||
private String difficulty;
|
||||
|
||||
/**
|
||||
* 건강 효과
|
||||
*/
|
||||
private String healthBenefit;
|
||||
|
||||
/**
|
||||
* 직업군 관련성
|
||||
*/
|
||||
private String occupationRelevance;
|
||||
|
||||
/**
|
||||
* 예상 소요 시간 (분)
|
||||
*/
|
||||
private Integer estimatedTimeMinutes;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI API에 미션 상세 정보 요청을 위한 DTO입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MissionDetailsRequest {
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 조회할 미션 ID 목록
|
||||
*/
|
||||
private List<String> missionIds;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 미션 이력 응답 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 이력 응답")
|
||||
public class MissionHistoryResponse {
|
||||
|
||||
@Schema(description = "전체 달성률 (%)")
|
||||
private double totalAchievementRate;
|
||||
|
||||
@Schema(description = "기간 내 달성률 (%)")
|
||||
private double periodAchievementRate;
|
||||
|
||||
@Schema(description = "최고 연속 달성 일수")
|
||||
private int bestStreak;
|
||||
|
||||
@Schema(description = "미션별 통계")
|
||||
private List<MissionStats> missionStats;
|
||||
|
||||
// @Schema(description = "차트 데이터")
|
||||
// private Object chartData;
|
||||
|
||||
@Schema(description = "차트 데이터")
|
||||
private Map<String, Object> chartData;
|
||||
|
||||
|
||||
@Schema(description = "조회 기간")
|
||||
private Period period;
|
||||
|
||||
@Schema(description = "인사이트")
|
||||
private List<String> insights;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 미션 추천 응답 DTO 클래스입니다.
|
||||
* 원래는 Intelligence Service에 있었지만 Mock 처리를 위해 추가.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 추천 응답")
|
||||
public class MissionRecommendationResponse {
|
||||
|
||||
@Schema(description = "추천 미션 목록")
|
||||
private List<Mission> missions;
|
||||
|
||||
@Schema(description = "추천 이유")
|
||||
private String recommendationReason;
|
||||
|
||||
@Schema(description = "총 추천 미션 수")
|
||||
private int totalRecommended;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 미션 재설정 요청 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 재설정 요청")
|
||||
public class MissionResetRequest {
|
||||
|
||||
@NotBlank(message = "회원 시리얼 번호는 필수입니다.")
|
||||
@Schema(description = "회원 시리얼 번호")
|
||||
private String memberSerialNumber;
|
||||
|
||||
@NotBlank(message = "재설정 이유는 필수입니다.")
|
||||
@Schema(description = "재설정 이유")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "현재 미션 ID 목록")
|
||||
private List<String> currentMissionIds;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 미션 재설정 응답 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 재설정 응답")
|
||||
public class MissionResetResponse {
|
||||
|
||||
@Schema(description = "응답 메시지")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "새로운 추천 미션 목록")
|
||||
private List<RecommendedMission> newRecommendations;
|
||||
|
||||
@Schema(description = "재설정 완료 시간")
|
||||
private String resetCompletedAt;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// goal-service/src/main/java/com/healthsync/goal/dto/MissionSelectionRequest.java
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 미션 선택 요청 DTO 클래스입니다.
|
||||
* 선택된 미션의 상세 정보를 포함합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 선택 요청")
|
||||
public class MissionSelectionRequest {
|
||||
|
||||
@NotBlank(message = "회원 시리얼 번호는 필수입니다.")
|
||||
@Schema(description = "회원 시리얼 번호")
|
||||
private String memberSerialNumber;
|
||||
|
||||
@NotEmpty(message = "선택된 미션 목록은 필수입니다.")
|
||||
@Size(min = 1, max = 5, message = "1개에서 5개까지 미션을 선택할 수 있습니다.")
|
||||
@Valid
|
||||
@Schema(description = "선택된 미션 상세 정보 목록")
|
||||
private List<SelectedMissionDetail> selectedMissionIds;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 미션 통계 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "미션 통계")
|
||||
public class MissionStats {
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "미션 제목")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "달성률 (%)")
|
||||
private double achievementRate;
|
||||
|
||||
@Schema(description = "완료 일수")
|
||||
private int completedDays;
|
||||
|
||||
@Schema(description = "전체 일수")
|
||||
private int totalDays;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 기간 정보 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "기간 정보")
|
||||
public class Period {
|
||||
|
||||
@Schema(description = "시작일")
|
||||
private String startDate;
|
||||
|
||||
@Schema(description = "종료일")
|
||||
private String endDate;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 추천 미션 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "추천 미션")
|
||||
public class RecommendedMission {
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "미션 제목")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "미션 설명")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "미션 카테고리")
|
||||
private String category;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 선택된 미션 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "선택된 미션")
|
||||
public class SelectedMission {
|
||||
|
||||
@Schema(description = "미션 ID")
|
||||
private String missionId;
|
||||
|
||||
@Schema(description = "미션 제목")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "미션 설명")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "시작일")
|
||||
private String startDate;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 선택된 미션의 상세 정보 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "선택된 미션 상세 정보")
|
||||
public class SelectedMissionDetail {
|
||||
|
||||
@NotBlank(message = "미션 제목은 필수입니다.")
|
||||
@Schema(description = "미션 제목", example = "목 스트레칭 (좌우 각 15초)")
|
||||
private String title;
|
||||
|
||||
@NotNull(message = "일일 목표 횟수는 필수입니다.")
|
||||
@Min(value = 1, message = "일일 목표 횟수는 최소 1회입니다.")
|
||||
@Max(value = 20, message = "일일 목표 횟수는 최대 20회입니다.")
|
||||
@Schema(description = "일일 목표 횟수", example = "3")
|
||||
private Integer daily_target_count;
|
||||
|
||||
@NotBlank(message = "미션 사유는 필수입니다.")
|
||||
@Schema(description = "미션 선정 사유", example = "장시간 모니터 사용으로 인한 목 긴장 완화 및 거북목 예방")
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 미션 제목에서 고유 ID를 생성합니다.
|
||||
* @return 생성된 미션 ID
|
||||
*/
|
||||
public String generateMissionId() {
|
||||
if (title == null) return null;
|
||||
|
||||
// 제목을 기반으로 간단한 ID 생성 (실제로는 더 정교한 로직 필요)
|
||||
return title.replaceAll("[^가-힣a-zA-Z0-9]", "_")
|
||||
.toLowerCase()
|
||||
.replaceAll("_+", "_")
|
||||
.replaceAll("^_|_$", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.healthsync.goal.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 프로필 DTO 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "사용자 프로필")
|
||||
public class UserProfile {
|
||||
|
||||
@Schema(description = "사용자 ID")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "이름")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "나이")
|
||||
private int age;
|
||||
|
||||
@Schema(description = "성별")
|
||||
private String gender;
|
||||
|
||||
@Schema(description = "직업")
|
||||
private String occupation;
|
||||
|
||||
@Schema(description = "이메일")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "전화번호")
|
||||
private String phone;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.healthsync.goal.infrastructure.adapters;
|
||||
|
||||
import com.healthsync.goal.dto.ActiveMissionsResponse;
|
||||
import com.healthsync.goal.dto.MissionHistoryResponse;
|
||||
import com.healthsync.goal.infrastructure.ports.CachePort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis 캐시와의 통신을 담당하는 어댑터 클래스입니다.
|
||||
* Clean Architecture의 Infrastructure 계층에 해당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CacheAdapter implements CachePort {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public ActiveMissionsResponse getActiveMissions(String userId) {
|
||||
try {
|
||||
String cacheKey = "active_missions:" + userId;
|
||||
//return (ActiveMissionsResponse) redisTemplate.opsForValue().get(cacheKey);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("활성 미션 캐시 조회 실패: userId={}, error={}", userId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheActiveMissions(String userId, ActiveMissionsResponse response) {
|
||||
try {
|
||||
String cacheKey = "active_missions:" + userId;
|
||||
//redisTemplate.opsForValue().set(cacheKey, response, Duration.ofMinutes(30));
|
||||
log.info("활성 미션 캐시 저장: userId={}", userId);
|
||||
} catch (Exception e) {
|
||||
log.warn("활성 미션 캐시 저장 실패: userId={}, error={}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MissionHistoryResponse getMissionHistory(String cacheKey) {
|
||||
try {
|
||||
//return (MissionHistoryResponse) redisTemplate.opsForValue().get(cacheKey);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("미션 이력 캐시 조회 실패: key={}, error={}", cacheKey, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheMissionHistory(String cacheKey, MissionHistoryResponse response) {
|
||||
try {
|
||||
//redisTemplate.opsForValue().set(cacheKey, response, Duration.ofHours(1));
|
||||
log.info("미션 이력 캐시 저장: key={}", cacheKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("미션 이력 캐시 저장 실패: key={}, error={}", cacheKey, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateUserMissionCache(String userId) {
|
||||
try {
|
||||
String activeMissionKey = "active_missions:" + userId;
|
||||
String historyKeyPattern = "mission_history:" + userId + ":*";
|
||||
|
||||
// 활성 미션 캐시 삭제
|
||||
//redisTemplate.delete(activeMissionKey);
|
||||
|
||||
// 미션 이력 캐시 삭제 (패턴 매칭)
|
||||
//redisTemplate.delete(redisTemplate.keys(historyKeyPattern));
|
||||
|
||||
log.info("사용자 미션 캐시 무효화 완료: userId={}", userId);
|
||||
} catch (Exception e) {
|
||||
log.warn("사용자 미션 캐시 무효화 실패: userId={}, error={}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.healthsync.goal.infrastructure.adapters;
|
||||
|
||||
import com.healthsync.goal.infrastructure.ports.EventPublisherPort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 이벤트 발행을 담당하는 어댑터 클래스입니다.
|
||||
* Clean Architecture의 Infrastructure 계층에 해당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EventPublisherAdapter implements EventPublisherPort {
|
||||
|
||||
// 실제 구현에서는 Spring Cloud Stream 또는 Azure Service Bus 사용
|
||||
// private final ServiceBusTemplate serviceBusTemplate;
|
||||
|
||||
@Override
|
||||
public void publishGoalSetEvent(String memberSerialNumber, List<String> missionIds) {
|
||||
try {
|
||||
log.info("목표 설정 이벤트 발행: memberSerialNumber={}, missionCount={}", memberSerialNumber, missionIds.size());
|
||||
|
||||
// 실제 구현에서는 이벤트 브로커에 발행
|
||||
// GoalSetEvent event = GoalSetEvent.builder()
|
||||
// .memberSerialNumber(memberSerialNumber)
|
||||
// .missionIds(missionIds)
|
||||
// .timestamp(LocalDateTime.now())
|
||||
// .build();
|
||||
// serviceBusTemplate.send("goal-set-topic", event);
|
||||
|
||||
log.info("목표 설정 이벤트 발행 완료: memberSerialNumber={}", memberSerialNumber);
|
||||
} catch (Exception e) {
|
||||
log.error("목표 설정 이벤트 발행 실패: memberSerialNumber={}, error={}", memberSerialNumber, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMissionCompleteEvent(String memberSerialNumber, String missionId, int streakDays) {
|
||||
try {
|
||||
log.info("미션 완료 이벤트 발행: memberSerialNumber={}, missionId={}, streakDays={}", memberSerialNumber, missionId, streakDays);
|
||||
|
||||
// 실제 구현에서는 이벤트 브로커에 발행
|
||||
// MissionCompleteEvent event = MissionCompleteEvent.builder()
|
||||
// .memberSerialNumber(memberSerialNumber)
|
||||
// .missionId(missionId)
|
||||
// .streakDays(streakDays)
|
||||
// .timestamp(LocalDateTime.now())
|
||||
// .build();
|
||||
// serviceBusTemplate.send("mission-complete-topic", event);
|
||||
|
||||
log.info("미션 완료 이벤트 발행 완료: memberSerialNumber={}, missionId={}", memberSerialNumber, missionId);
|
||||
} catch (Exception e) {
|
||||
log.error("미션 완료 이벤트 발행 실패: memberSerialNumber={}, missionId={}, error={}", memberSerialNumber, missionId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMissionResetEvent(String memberSerialNumber, String resetReason) {
|
||||
try {
|
||||
log.info("미션 재설정 이벤트 발행: memberSerialNumber={}, reason={}", memberSerialNumber, resetReason);
|
||||
|
||||
// 실제 구현에서는 이벤트 브로커에 발행
|
||||
// MissionResetEvent event = MissionResetEvent.builder()
|
||||
// .memberSerialNumber(memberSerialNumber)
|
||||
// .resetReason(resetReason)
|
||||
// .timestamp(LocalDateTime.now())
|
||||
// .build();
|
||||
// serviceBusTemplate.send("mission-reset-topic", event);
|
||||
|
||||
log.info("미션 재설정 이벤트 발행 완료: memberSerialNumber={}", memberSerialNumber);
|
||||
} catch (Exception e) {
|
||||
log.error("미션 재설정 이벤트 발행 실패: memberSerialNumber={}, error={}", memberSerialNumber, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.healthsync.goal.infrastructure.adapters;
|
||||
|
||||
import com.healthsync.goal.domain.ports.IntelligenceServicePort;
|
||||
import com.healthsync.goal.dto.CelebrationRequest;
|
||||
import com.healthsync.goal.dto.CelebrationResponse;
|
||||
import com.healthsync.goal.dto.RecommendedMission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* HealthSync_Intelligence Python Service와의 연동을 담당하는 어댑터
|
||||
*
|
||||
* 🐍 Python API 스펙:
|
||||
* - 엔드포인트: POST /api/intelligence/missions/celebrate
|
||||
* - 요청: CelebrationRequest { userId: long, missionId: long }
|
||||
* - 응답: CelebrationResponse { congratsMessage: str }
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IntelligenceServiceAdapter implements IntelligenceServicePort {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
/*@Value("${healthsync.intelligence.base-url:http://healthsync-intelligence:8080}") */
|
||||
@Value("${services.intelligence-service.url:http://localhost:8083}")
|
||||
private String intelligenceServiceBaseUrl;
|
||||
|
||||
@Override
|
||||
public CelebrationResponse celebrateMissionAchievement(CelebrationRequest request) {
|
||||
log.info("🎉 [CELEBRATION_API] Python 미션 달성 축하 요청: userId={}, missionId={}",
|
||||
request.getUserId(), request.getMissionId());
|
||||
|
||||
try {
|
||||
// 🔧 정확한 Python API 엔드포인트 경로
|
||||
String url = intelligenceServiceBaseUrl + "/api/intelligence/missions/celebrate";
|
||||
log.info("🔗 [CELEBRATION_API] 호출 URL: {}", url); // ← 이 줄 추가
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<CelebrationRequest> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
ResponseEntity<CelebrationResponse> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
CelebrationResponse.class
|
||||
);
|
||||
|
||||
CelebrationResponse celebrationResponse = response.getBody();
|
||||
|
||||
log.info("✅ [CELEBRATION_API] Python 축하 메시지 수신 완료: userId={}, message={}",
|
||||
request.getUserId(), celebrationResponse != null ? celebrationResponse.getCongratsMessage() : "null");
|
||||
|
||||
return celebrationResponse;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [CELEBRATION_API] Python 축하 API 호출 실패: userId={}, error={}", request.getUserId(), e.getMessage(), e);
|
||||
|
||||
// 🔧 Fallback: Python API 호출 실패시 기본 축하 메시지 반환
|
||||
return CelebrationResponse.builder()
|
||||
.congratsMessage("🎉 목표를 달성하셨습니다! 훌륭해요! 건강한 습관을 만들어가고 계시네요! 💪✨")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecommendedMission> getNewMissionRecommendations(String memberSerialNumber, String resetReason) {
|
||||
// 기존 메서드 구현 유지
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.healthsync.goal.infrastructure.adapters;
|
||||
|
||||
import com.healthsync.goal.dto.UserProfile;
|
||||
import com.healthsync.goal.infrastructure.ports.UserServicePort;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* User Service Mock Adapter 클래스입니다.
|
||||
* User Service가 분리되어 Mock 데이터를 제공합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserServiceAdapter implements UserServicePort {
|
||||
|
||||
@Value("${services.user-service.url:http://localhost:8081}")
|
||||
private String userServiceUrl;
|
||||
|
||||
@Override
|
||||
public UserProfile getUserProfile(String memberSerialNumber) {
|
||||
log.info("🔍 [MOCK_USER] Mock 사용자 프로필 조회: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
// ✅ Mock 사용자 프로필 생성 (User Service 없이도 동작)
|
||||
UserProfile profile = UserProfile.builder()
|
||||
.userId(memberSerialNumber)
|
||||
.name(generateMockName(memberSerialNumber))
|
||||
.age(generateMockAge(memberSerialNumber))
|
||||
.gender(generateMockGender(memberSerialNumber))
|
||||
.occupation(generateMockOccupation(memberSerialNumber))
|
||||
.email(generateMockEmail(memberSerialNumber))
|
||||
.phone("010-1234-5678")
|
||||
.build();
|
||||
|
||||
log.info("✅ [MOCK_USER] Mock 사용자 프로필 생성 완료: memberSerialNumber={}, name={}, occupation={}",
|
||||
memberSerialNumber, profile.getName(), profile.getOccupation());
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateUserExists(String memberSerialNumber) {
|
||||
log.info("🔍 [MOCK_USER] Mock 사용자 존재 확인: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
// ✅ Mock 검증 - 기본적인 유효성만 체크
|
||||
if (memberSerialNumber == null || memberSerialNumber.trim().isEmpty()) {
|
||||
log.error("❌ [MOCK_USER] 회원 시리얼 번호가 비어있음: memberSerialNumber={}", memberSerialNumber);
|
||||
throw new IllegalArgumentException("회원 시리얼 번호가 비어있습니다.");
|
||||
}
|
||||
|
||||
if (memberSerialNumber.length() < 3) {
|
||||
log.error("❌ [MOCK_USER] 회원 시리얼 번호가 너무 짧음: memberSerialNumber={}", memberSerialNumber);
|
||||
throw new IllegalArgumentException("회원 시리얼 번호는 3자 이상이어야 합니다.");
|
||||
}
|
||||
|
||||
log.info("✅ [MOCK_USER] Mock 사용자 검증 완료: memberSerialNumber={}", memberSerialNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ memberSerialNumber 기반으로 Mock 이름 생성
|
||||
*/
|
||||
private String generateMockName(String memberSerialNumber) {
|
||||
String[] names = {"김철수", "이영희", "박민수", "정수진", "홍길동", "김영수", "이민정", "박지혜"};
|
||||
int index = Math.abs(memberSerialNumber.hashCode()) % names.length;
|
||||
return names[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ memberSerialNumber 기반으로 Mock 나이 생성
|
||||
*/
|
||||
private int generateMockAge(String memberSerialNumber) {
|
||||
// 25-45세 사이로 생성
|
||||
return 25 + (Math.abs(memberSerialNumber.hashCode()) % 21);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ memberSerialNumber 기반으로 Mock 성별 생성
|
||||
*/
|
||||
private String generateMockGender(String memberSerialNumber) {
|
||||
return Math.abs(memberSerialNumber.hashCode()) % 2 == 0 ? "남성" : "여성";
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ memberSerialNumber 기반으로 Mock 직업 생성
|
||||
*/
|
||||
private String generateMockOccupation(String memberSerialNumber) {
|
||||
String[] occupations = {"개발자", "디자이너", "마케터", "영업", "기획자", "의사", "교사", "공무원"};
|
||||
int index = Math.abs(memberSerialNumber.hashCode()) % occupations.length;
|
||||
return occupations[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ memberSerialNumber 기반으로 Mock 이메일 생성
|
||||
*/
|
||||
private String generateMockEmail(String memberSerialNumber) {
|
||||
return "user" + memberSerialNumber + "@healthsync.com";
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.healthsync.goal.infrastructure.entities;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DDL의 mission_completion_history 테이블과 정확히 매핑되는 엔티티입니다.
|
||||
* DDL에서 completion_id가 자동증가가 아니므로 수동 할당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "mission_completion_history", schema = "goal_service")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MissionCompletionHistoryEntity {
|
||||
|
||||
@Id
|
||||
// ✅ DDL에 맞춰 @GeneratedValue 제거 (수동 할당)
|
||||
@Column(name = "completion_id")
|
||||
private Long completionId;
|
||||
|
||||
@Column(name = "mission_id", nullable = false)
|
||||
private Long missionId;
|
||||
|
||||
@Column(name = "member_serial_number", nullable = false)
|
||||
private Long memberSerialNumber;
|
||||
|
||||
@Column(name = "completion_date", nullable = false)
|
||||
private LocalDate completionDate;
|
||||
|
||||
@Column(name = "daily_target_count", nullable = false)
|
||||
private Integer dailyTargetCount;
|
||||
|
||||
@Column(name = "daily_completed_count", nullable = false)
|
||||
private Integer dailyCompletedCount;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 오늘 완료된 미션인지 확인합니다.
|
||||
*/
|
||||
public boolean isCompletedToday() {
|
||||
return LocalDate.now().equals(completionDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 목표 달성 여부를 확인합니다.
|
||||
*/
|
||||
public boolean isTargetAchieved() {
|
||||
return dailyCompletedCount >= dailyTargetCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 달성률을 계산합니다.
|
||||
*/
|
||||
public double getAchievementRate() {
|
||||
if (dailyTargetCount == 0) return 0.0;
|
||||
return (double) dailyCompletedCount / dailyTargetCount * 100.0;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (this.createdAt == null) {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.healthsync.goal.infrastructure.entities;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DDL의 user_mission_goal 테이블과 정확히 매핑되는 엔티티입니다.
|
||||
* DDL에서 mission_id가 자동증가가 아니므로 수동 할당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_mission_goal", schema = "goal_service")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserMissionGoalEntity {
|
||||
|
||||
@Id
|
||||
// ✅ DDL에 맞춰 @GeneratedValue 제거 (수동 할당)
|
||||
@Column(name = "mission_id")
|
||||
private Long missionId;
|
||||
|
||||
@Column(name = "member_serial_number", nullable = false)
|
||||
private Long memberSerialNumber;
|
||||
|
||||
@Column(name = "performance_date", nullable = false)
|
||||
private LocalDate performanceDate;
|
||||
|
||||
@Column(name = "mission_name", nullable = false, length = 100)
|
||||
private String missionName;
|
||||
|
||||
@Column(name = "mission_description", length = 200)
|
||||
private String missionDescription;
|
||||
|
||||
@Column(name = "daily_target_count", nullable = false)
|
||||
private Integer dailyTargetCount;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 미션을 비활성화합니다.
|
||||
*/
|
||||
public void deactivate() {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션이 활성 상태인지 확인합니다.
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return Boolean.TRUE.equals(this.isActive);
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (this.createdAt == null) {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (this.isActive == null) {
|
||||
this.isActive = true;
|
||||
}
|
||||
if (this.dailyTargetCount == null) {
|
||||
this.dailyTargetCount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.healthsync.goal.infrastructure.ports;
|
||||
|
||||
import com.healthsync.goal.dto.ActiveMissionsResponse;
|
||||
import com.healthsync.goal.dto.MissionHistoryResponse;
|
||||
|
||||
/**
|
||||
* 캐시 처리를 위한 포트 인터페이스입니다.
|
||||
* Clean Architecture의 Domain 계층에서 정의합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface CachePort {
|
||||
|
||||
/**
|
||||
* 활성 미션 캐시를 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 활성 미션 응답 (캐시 미스 시 null)
|
||||
*/
|
||||
ActiveMissionsResponse getActiveMissions(String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 활성 미션을 캐시에 저장합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param response 활성 미션 응답
|
||||
*/
|
||||
void cacheActiveMissions(String memberSerialNumber, ActiveMissionsResponse response);
|
||||
|
||||
/**
|
||||
* 사용자 미션 캐시를 무효화합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
*/
|
||||
void invalidateUserMissionCache(String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 미션 이력 캐시를 조회합니다.
|
||||
*
|
||||
* @param cacheKey 캐시 키
|
||||
* @return 미션 이력 응답 (캐시 미스 시 null)
|
||||
*/
|
||||
MissionHistoryResponse getMissionHistory(String cacheKey);
|
||||
|
||||
/**
|
||||
* 미션 이력을 캐시에 저장합니다.
|
||||
*
|
||||
* @param cacheKey 캐시 키
|
||||
* @param response 미션 이력 응답
|
||||
*/
|
||||
void cacheMissionHistory(String cacheKey, MissionHistoryResponse response);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.healthsync.goal.infrastructure.ports;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 이벤트 발행을 위한 포트 인터페이스입니다.
|
||||
* Clean Architecture의 Domain 계층에서 정의합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface EventPublisherPort {
|
||||
|
||||
/**
|
||||
* 목표 설정 이벤트를 발행합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param selectedMissionIds 선택된 미션 ID 목록
|
||||
*/
|
||||
void publishGoalSetEvent(String memberSerialNumber, List<String> selectedMissionIds);
|
||||
|
||||
/**
|
||||
* 미션 완료 이벤트를 발행합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param missionId 미션 ID
|
||||
* @param streakDays 연속 달성 일수
|
||||
*/
|
||||
void publishMissionCompleteEvent(String memberSerialNumber, String missionId, int streakDays);
|
||||
|
||||
/**
|
||||
* 미션 재설정 이벤트를 발행합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param resetReason 재설정 이유
|
||||
*/
|
||||
void publishMissionResetEvent(String memberSerialNumber, String resetReason);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.healthsync.goal.domain.ports;
|
||||
|
||||
import com.healthsync.goal.dto.CelebrationRequest;
|
||||
import com.healthsync.goal.dto.CelebrationResponse;
|
||||
import com.healthsync.goal.dto.RecommendedMission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Intelligence Service 연동을 위한 Domain Port
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface IntelligenceServicePort {
|
||||
|
||||
/**
|
||||
* 🎯 미션 달성 축하 API를 호출합니다. (Python API)
|
||||
*
|
||||
* @param request 축하 요청 (userId: long, missionId: long)
|
||||
* @return 축하 응답 (congratsMessage: str)
|
||||
*/
|
||||
CelebrationResponse celebrateMissionAchievement(CelebrationRequest request);
|
||||
|
||||
/**
|
||||
* 새로운 미션 추천을 요청합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param resetReason 재설정 사유
|
||||
* @return 추천 미션 목록
|
||||
*/
|
||||
List<RecommendedMission> getNewMissionRecommendations(String memberSerialNumber, String resetReason);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.healthsync.goal.infrastructure.ports;
|
||||
|
||||
import com.healthsync.goal.dto.UserProfile;
|
||||
|
||||
/**
|
||||
* User Service와의 통신을 위한 포트 인터페이스입니다.
|
||||
* Clean Architecture의 Domain 계층에서 정의합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface UserServicePort {
|
||||
|
||||
/**
|
||||
* 사용자 프로필을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 사용자 프로필
|
||||
*/
|
||||
UserProfile getUserProfile(String memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 사용자 존재 여부를 검증합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
*/
|
||||
void validateUserExists(String memberSerialNumber);
|
||||
}
|
||||
+573
@@ -0,0 +1,573 @@
|
||||
// goal-service/src/main/java/com/healthsync/goal/infrastructure/repositories/GoalRepositoryImpl.java
|
||||
package com.healthsync.goal.infrastructure.repositories;
|
||||
|
||||
import com.healthsync.goal.domain.repositories.GoalRepository;
|
||||
import com.healthsync.goal.dto.*;
|
||||
import com.healthsync.goal.infrastructure.entities.UserMissionGoalEntity;
|
||||
import com.healthsync.goal.infrastructure.entities.MissionCompletionHistoryEntity;
|
||||
import com.healthsync.goal.infrastructure.utils.UserIdValidator;
|
||||
import com.healthsync.goal.infrastructure.services.IdGeneratorService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 목표 데이터 저장소 구현체입니다.
|
||||
* DDL 구조에 완전히 맞춰 수정됨 + SelectedMissionDetail 처리 추가.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class GoalRepositoryImpl implements GoalRepository {
|
||||
|
||||
private final UserMissionGoalJpaRepository userMissionGoalJpaRepository;
|
||||
private final MissionCompletionJpaRepository missionCompletionJpaRepository;
|
||||
private final IdGeneratorService idGeneratorService;
|
||||
|
||||
@Override
|
||||
public String saveGoalSettings(MissionSelectionRequest request) {
|
||||
log.info("🎯 [GOAL_SETTINGS] 미션 목표 설정 저장 시작: memberSerialNumber={}, selectedMissionCount={}",
|
||||
request.getMemberSerialNumber(), request.getSelectedMissionIds().size());
|
||||
|
||||
try {
|
||||
Long memberSerialNumber = UserIdValidator.parseMemberSerialNumber(request.getMemberSerialNumber(), "saveGoalSettings");
|
||||
|
||||
// ✅ SelectedMissionDetail에서 미션 ID 목록 추출
|
||||
List<String> missionIdList = request.getSelectedMissionIds().stream()
|
||||
.map(this::extractMissionIdFromDetail)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// ✅ Mock Intelligence Service에서 선택된 미션들의 상세 정보 조회 (필요시)
|
||||
// List<MissionDetailResponse> missionDetails = intelligenceServicePort
|
||||
// .getMissionDetails(request.getMemberSerialNumber(), missionIdList);
|
||||
|
||||
// ✅ 선택된 미션들을 DDL 구조에 맞춰 저장
|
||||
List<UserMissionGoalEntity> savedMissions = request.getSelectedMissionIds().stream()
|
||||
.map(selectedMissionDetail -> {
|
||||
// ✅ DDL에 맞춰 ID를 수동 생성
|
||||
Long generatedMissionId = idGeneratorService.generateMissionId();
|
||||
|
||||
// ✅ SelectedMissionDetail에서 직접 정보 사용
|
||||
String missionTitle = selectedMissionDetail.getTitle();
|
||||
String missionDescription = generateMissionDescription(selectedMissionDetail);
|
||||
int dailyTargetCount = selectedMissionDetail.getDaily_target_count();
|
||||
|
||||
UserMissionGoalEntity userMissionGoal = UserMissionGoalEntity.builder()
|
||||
.missionId(generatedMissionId)
|
||||
.memberSerialNumber(memberSerialNumber)
|
||||
.performanceDate(LocalDate.now())
|
||||
.missionName(missionTitle)
|
||||
.missionDescription(missionDescription)
|
||||
.dailyTargetCount(dailyTargetCount)
|
||||
.isActive(true)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
log.debug("✅ Creating mission goal: missionId={}, title={}, description={}, dailyTarget={}",
|
||||
generatedMissionId, missionTitle, missionDescription, dailyTargetCount);
|
||||
|
||||
return userMissionGoalJpaRepository.save(userMissionGoal);
|
||||
})
|
||||
.toList();
|
||||
|
||||
String goalId = "GOAL_SAVED_" + System.currentTimeMillis();
|
||||
log.info("✅ [GOAL_SETTINGS] 미션 목표 설정 저장 완료: memberSerialNumber={}, goalId={}, savedCount={}",
|
||||
request.getMemberSerialNumber(), goalId, savedMissions.size());
|
||||
return goalId;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("❌ [GOAL_SETTINGS] 미션 목표 설정 저장 실패: memberSerialNumber={}, 오류={}", request.getMemberSerialNumber(), e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [GOAL_SETTINGS] 미션 목표 설정 저장 중 예상치 못한 오류: memberSerialNumber={}, 오류={}", request.getMemberSerialNumber(), e.getMessage(), e);
|
||||
throw new RuntimeException("미션 목표 설정 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DailyMission> findActiveMissionsByUserId(String memberSerialNumber) {
|
||||
log.info("🔍 [ACTIVE_MISSIONS] 활성 미션 조회 시작: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "findActiveMissionsByUserId");
|
||||
|
||||
// ✅ 실제 메서드명 사용
|
||||
List<UserMissionGoalEntity> activeMissions = userMissionGoalJpaRepository
|
||||
.findActiveByMemberSerialNumber(memberSerialNumberLong);
|
||||
|
||||
List<DailyMission> dailyMissions = activeMissions.stream()
|
||||
.map(this::convertToDailyMission)
|
||||
.toList();
|
||||
|
||||
log.info("✅ [ACTIVE_MISSIONS] 활성 미션 조회 완료: memberSerialNumber={}, missionCount={}", memberSerialNumber, dailyMissions.size());
|
||||
return dailyMissions;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [ACTIVE_MISSIONS] 활성 미션 조회 중 오류: memberSerialNumber={}, 오류={}", memberSerialNumber, e.getMessage(), e);
|
||||
throw new RuntimeException("활성 미션 조회 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateCurrentMissions(String memberSerialNumber) {
|
||||
log.info("⏹️ [DEACTIVATE_MISSIONS] 미션 비활성화 시작: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "deactivateCurrentMissions");
|
||||
|
||||
// ✅ 실제 구현 방식: 엔티티 조회 → deactivate() 호출 → saveAll()
|
||||
List<UserMissionGoalEntity> activeMissions = userMissionGoalJpaRepository
|
||||
.findActiveByMemberSerialNumber(memberSerialNumberLong);
|
||||
|
||||
activeMissions.forEach(UserMissionGoalEntity::deactivate);
|
||||
userMissionGoalJpaRepository.saveAll(activeMissions);
|
||||
|
||||
log.info("✅ [DEACTIVATE_MISSIONS] 미션 비활성화 완료: memberSerialNumber={}, updatedCount={}", memberSerialNumber, activeMissions.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [DEACTIVATE_MISSIONS] 미션 비활성화 중 오류: memberSerialNumber={}, 오류={}", memberSerialNumber, e.getMessage(), e);
|
||||
throw new RuntimeException("미션 비활성화 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordMissionCompletion(String missionId, MissionCompleteRequest request) {
|
||||
log.info("📝 [MISSION_COMPLETION] 미션 점진적 완료 기록 시작: memberSerialNumber={}, missionId={}",
|
||||
request.getMemberSerialNumber(), missionId);
|
||||
|
||||
try {
|
||||
Long memberSerialNumber = UserIdValidator.parseMemberSerialNumber(request.getMemberSerialNumber(), "recordMissionCompletion");
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
|
||||
// ✅ 1. 미션 정보 조회
|
||||
Optional<UserMissionGoalEntity> missionOpt = userMissionGoalJpaRepository
|
||||
.findByMissionIdAndMemberSerialNumber(missionIdLong, memberSerialNumber);
|
||||
|
||||
if (missionOpt.isEmpty()) {
|
||||
throw new IllegalArgumentException("미션을 찾을 수 없습니다: " + missionId);
|
||||
}
|
||||
|
||||
UserMissionGoalEntity mission = missionOpt.get();
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// ✅ 2. 오늘 완료 기록이 있는지 확인
|
||||
Optional<MissionCompletionHistoryEntity> existingOpt = missionCompletionJpaRepository
|
||||
.findByMissionIdAndMemberSerialNumberAndCompletionDate(missionIdLong, memberSerialNumber, today);
|
||||
|
||||
if (existingOpt.isPresent()) {
|
||||
// ✅ 3-A. 기존 기록이 있으면 daily_completed_count +1 증가
|
||||
MissionCompletionHistoryEntity existing = existingOpt.get();
|
||||
|
||||
// 목표 초과 방지
|
||||
if (existing.getDailyCompletedCount() >= existing.getDailyTargetCount()) {
|
||||
log.warn("⚠️ [MISSION_COMPLETION] 이미 목표를 달성한 미션: memberSerialNumber={}, missionId={}, current={}/{}",
|
||||
request.getMemberSerialNumber(), missionId,
|
||||
existing.getDailyCompletedCount(), existing.getDailyTargetCount());
|
||||
return;
|
||||
}
|
||||
|
||||
// +1 증가
|
||||
int newCompletedCount = existing.getDailyCompletedCount() + 1;
|
||||
existing.setDailyCompletedCount(newCompletedCount);
|
||||
|
||||
missionCompletionJpaRepository.save(existing);
|
||||
|
||||
log.info("✅ [MISSION_COMPLETION] 기존 기록 업데이트: memberSerialNumber={}, missionId={}, count={}/{}",
|
||||
request.getMemberSerialNumber(), missionId, newCompletedCount, existing.getDailyTargetCount());
|
||||
|
||||
} else {
|
||||
// ✅ 3-B. 기존 기록이 없으면 새로 생성 (daily_completed_count = 1로 시작)
|
||||
Long generatedCompletionId = idGeneratorService.generateCompletionId();
|
||||
|
||||
MissionCompletionHistoryEntity newCompletion = MissionCompletionHistoryEntity.builder()
|
||||
.completionId(generatedCompletionId)
|
||||
.missionId(missionIdLong)
|
||||
.memberSerialNumber(memberSerialNumber)
|
||||
.completionDate(today)
|
||||
.dailyTargetCount(mission.getDailyTargetCount()) // ✅ user_mission_goal에서 가져옴
|
||||
.dailyCompletedCount(1) // ✅ 첫 호출시 1로 시작
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
missionCompletionJpaRepository.save(newCompletion);
|
||||
|
||||
log.info("✅ [MISSION_COMPLETION] 새로운 기록 생성: memberSerialNumber={}, missionId={}, completionId={}, count=1/{}",
|
||||
request.getMemberSerialNumber(), missionId, generatedCompletionId, mission.getDailyTargetCount());
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("❌ [MISSION_COMPLETION] 미션 완료 기록 실패: memberSerialNumber={}, missionId={}, 오류={}",
|
||||
request.getMemberSerialNumber(), missionId, e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [MISSION_COMPLETION] 미션 완료 기록 중 예상치 못한 오류: memberSerialNumber={}, missionId={}, 오류={}",
|
||||
request.getMemberSerialNumber(), missionId, e.getMessage(), e);
|
||||
throw new RuntimeException("미션 완료 기록 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalCompletedCount(String memberSerialNumber, String missionId) {
|
||||
log.info("📊 [TOTAL_COUNT] 총 완료 횟수 조회 시작: memberSerialNumber={}, missionId={}", memberSerialNumber, missionId);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "getTotalCompletedCount");
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
|
||||
int count = missionCompletionJpaRepository
|
||||
.countByMemberSerialNumberAndMissionId(memberSerialNumberLong, missionIdLong);
|
||||
|
||||
log.info("✅ [TOTAL_COUNT] 총 완료 횟수 조회 완료: memberSerialNumber={}, missionId={}, count={}", memberSerialNumber, missionId, count);
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [TOTAL_COUNT] 총 완료 횟수 조회 중 오류: memberSerialNumber={}, missionId={}, 오류={}", memberSerialNumber, missionId, e.getMessage(), e);
|
||||
throw new RuntimeException("총 완료 횟수 조회 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MissionStats> findMissionHistoryByPeriod(String memberSerialNumber, String startDate, String endDate, String missionIds) {
|
||||
log.info("📊 [MISSION_HISTORY] 미션 이력 조회 시작: memberSerialNumber={}, period={}-{}", memberSerialNumber, startDate, endDate);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "findMissionHistoryByPeriod");
|
||||
|
||||
// 실제 데이터베이스 조회
|
||||
List<MissionCompletionHistoryEntity> completionHistory =
|
||||
missionCompletionJpaRepository.findByMemberSerialNumberAndDateRange(
|
||||
memberSerialNumberLong,
|
||||
LocalDate.parse(startDate),
|
||||
LocalDate.parse(endDate)
|
||||
);
|
||||
|
||||
// MissionStats로 변환 - 실제 미션 이름 조회 포함
|
||||
List<MissionStats> missionStats = completionHistory.stream()
|
||||
.collect(Collectors.groupingBy(MissionCompletionHistoryEntity::getMissionId))
|
||||
.entrySet().stream()
|
||||
.map(entry -> {
|
||||
Long missionId = entry.getKey();
|
||||
List<MissionCompletionHistoryEntity> missions = entry.getValue();
|
||||
|
||||
// 실제 미션 이름 조회
|
||||
String missionTitle = getMissionNameById(missionId);
|
||||
|
||||
return MissionStats.builder()
|
||||
.missionId(missionId.toString())
|
||||
.title(missionTitle) // 실제 DB에서 조회한 미션 이름
|
||||
.completedDays(missions.size())
|
||||
.totalDays(calculateTotalDaysInPeriod(startDate, endDate))
|
||||
.achievementRate(calculateAchievementRate(missions.size(), calculateTotalDaysInPeriod(startDate, endDate)))
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("✅ [MISSION_HISTORY] 미션 이력 조회 완료: memberSerialNumber={}, count={}", memberSerialNumber, missionStats.size());
|
||||
return missionStats;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [MISSION_HISTORY] 미션 이력 조회 중 오류: memberSerialNumber={}, 오류={}", memberSerialNumber, e.getMessage(), e);
|
||||
throw new RuntimeException("미션 이력 조회 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 ID로 실제 미션 이름을 조회합니다.
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @return 미션 이름
|
||||
*/
|
||||
private String getMissionNameById(Long missionId) {
|
||||
try {
|
||||
Optional<UserMissionGoalEntity> missionEntity = userMissionGoalJpaRepository.findByMissionId(missionId);
|
||||
if (missionEntity.isPresent()) {
|
||||
String missionName = missionEntity.get().getMissionName();
|
||||
log.debug("🔍 [MISSION_NAME] 미션 이름 조회 성공: missionId={}, name={}", missionId, missionName);
|
||||
return missionName;
|
||||
} else {
|
||||
log.warn("⚠️ [MISSION_NAME] 미션을 찾을 수 없음: missionId={}", missionId);
|
||||
return "미션 #" + missionId; // fallback
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [MISSION_NAME] 미션 이름 조회 중 오류: missionId={}, 오류={}", missionId, e.getMessage());
|
||||
return "미션 #" + missionId; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateTotalDaysInPeriod(String startDate, String endDate) {
|
||||
return (int) ChronoUnit.DAYS.between(LocalDate.parse(startDate), LocalDate.parse(endDate)) + 1;
|
||||
}
|
||||
|
||||
private double calculateAchievementRate(int completedDays, int totalDays) {
|
||||
return totalDays > 0 ? (double) completedDays / totalDays * 100.0 : 0.0;
|
||||
}
|
||||
|
||||
// === Private Helper Methods ===
|
||||
|
||||
/**
|
||||
* SelectedMissionDetail에서 미션 ID를 추출합니다.
|
||||
* 제목을 기반으로 고유한 ID를 생성합니다.
|
||||
*/
|
||||
private String extractMissionIdFromDetail(SelectedMissionDetail detail) {
|
||||
if (detail == null || detail.getTitle() == null) {
|
||||
return "mission_unknown_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// 제목을 기반으로 간단한 ID 생성
|
||||
return detail.getTitle()
|
||||
.replaceAll("[^가-힣a-zA-Z0-9]", "_")
|
||||
.toLowerCase()
|
||||
.replaceAll("_+", "_")
|
||||
.replaceAll("^_|_$", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectedMissionDetail로부터 미션 설명을 생성합니다.
|
||||
*/
|
||||
private String generateMissionDescription(SelectedMissionDetail detail) {
|
||||
return String.format("%s (일일 %d회) - %s",
|
||||
detail.getTitle(),
|
||||
detail.getDaily_target_count(),
|
||||
detail.getReason());
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ UserMissionGoalEntity를 DailyMission으로 변환하는 헬퍼 메서드
|
||||
*/
|
||||
private DailyMission convertToDailyMission(UserMissionGoalEntity entity) {
|
||||
// 오늘 완료 상태 조회
|
||||
LocalDate today = LocalDate.now();
|
||||
List<MissionCompletionHistoryEntity> todayCompletions = missionCompletionJpaRepository
|
||||
.findByMemberSerialNumberAndCompletionDate(entity.getMemberSerialNumber(), today);
|
||||
|
||||
// 이 미션의 오늘 완료 기록만 필터링
|
||||
List<MissionCompletionHistoryEntity> thisMissionCompletions = todayCompletions.stream()
|
||||
.filter(completion -> completion.getMissionId().equals(entity.getMissionId()))
|
||||
.toList();
|
||||
|
||||
// 오늘 완료 여부
|
||||
boolean completedToday = !thisMissionCompletions.isEmpty();
|
||||
|
||||
// 오늘 완료한 총 횟수
|
||||
int completedCount = thisMissionCompletions.stream()
|
||||
.mapToInt(MissionCompletionHistoryEntity::getDailyCompletedCount)
|
||||
.sum();
|
||||
|
||||
// 연속 달성 일수 계산
|
||||
int streakDays = calculateStreakDays(entity.getMemberSerialNumber(), entity.getMissionId());
|
||||
|
||||
// 미션 상태 결정
|
||||
String status;
|
||||
if (completedToday && completedCount >= entity.getDailyTargetCount()) {
|
||||
status = "COMPLETED";
|
||||
} else if (completedToday) {
|
||||
status = "PARTIAL";
|
||||
} else {
|
||||
status = "PENDING";
|
||||
}
|
||||
|
||||
return DailyMission.builder()
|
||||
.missionId(entity.getMissionId().toString())
|
||||
.title(entity.getMissionName())
|
||||
.description(entity.getMissionDescription())
|
||||
.targetCount(entity.getDailyTargetCount())
|
||||
.status(status)
|
||||
.completedToday(completedToday)
|
||||
.completedCount(completedCount)
|
||||
.streakDays(streakDays)
|
||||
.nextReminderTime("09:00")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 연속 달성 일수를 계산하는 헬퍼 메서드
|
||||
*/
|
||||
private int calculateStreakDays(Long memberSerialNumber, Long missionId) {
|
||||
// 간단한 구현: 실제로는 연속성 확인 로직 필요
|
||||
List<MissionCompletionHistoryEntity> recentCompletions = missionCompletionJpaRepository
|
||||
.findByMemberSerialNumberAndMissionId(memberSerialNumber, missionId);
|
||||
|
||||
return Math.min(recentCompletions.size(), 7); // 최대 7일로 제한
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserMissionGoalEntity findMissionByIdAndUser(String missionId, String memberSerialNumber) {
|
||||
log.info("📋 [FIND_MISSION] 미션 조회: missionId={}, memberSerialNumber={}", missionId, memberSerialNumber);
|
||||
|
||||
try {
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "findMissionByIdAndUser");
|
||||
|
||||
return userMissionGoalJpaRepository
|
||||
.findByMissionIdAndMemberSerialNumber(missionIdLong, memberSerialNumberLong)
|
||||
.orElse(null);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [FIND_MISSION] 미션 조회 오류: missionId={}, memberSerialNumber={}, 오류={}",
|
||||
missionId, memberSerialNumber, e.getMessage(), e);
|
||||
throw new RuntimeException("미션 조회 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MissionCompletionHistoryEntity findOrCreateTodayCompletion(String missionId, String memberSerialNumber, Integer dailyTargetCount) {
|
||||
log.info("📋 [TODAY_COMPLETION] 오늘 완료 기록 조회/생성: missionId={}, memberSerialNumber={}", missionId, memberSerialNumber);
|
||||
|
||||
try {
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "findOrCreateTodayCompletion");
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// 오늘 완료 기록 조회
|
||||
Optional<MissionCompletionHistoryEntity> existingOpt = missionCompletionJpaRepository
|
||||
.findByMissionIdAndMemberSerialNumberAndCompletionDate(missionIdLong, memberSerialNumberLong, today);
|
||||
|
||||
if (existingOpt.isPresent()) {
|
||||
return existingOpt.get();
|
||||
}
|
||||
|
||||
// 새로운 완료 기록 생성
|
||||
Long newCompletionId = idGeneratorService.generateCompletionId();
|
||||
|
||||
MissionCompletionHistoryEntity newCompletion = MissionCompletionHistoryEntity.builder()
|
||||
.completionId(newCompletionId)
|
||||
.missionId(missionIdLong)
|
||||
.memberSerialNumber(memberSerialNumberLong)
|
||||
.completionDate(today)
|
||||
.dailyTargetCount(dailyTargetCount)
|
||||
.dailyCompletedCount(0)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return missionCompletionJpaRepository.save(newCompletion);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [TODAY_COMPLETION] 완료 기록 조회/생성 오류: missionId={}, memberSerialNumber={}, 오류={}",
|
||||
missionId, memberSerialNumber, e.getMessage(), e);
|
||||
throw new RuntimeException("완료 기록 조회/생성 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMissionCompletion(MissionCompletionHistoryEntity completion) {
|
||||
log.info("💾 [SAVE_COMPLETION] 완료 기록 저장: completionId={}, count={}/{}",
|
||||
completion.getCompletionId(), completion.getDailyCompletedCount(), completion.getDailyTargetCount());
|
||||
|
||||
try {
|
||||
missionCompletionJpaRepository.save(completion);
|
||||
log.info("✅ [SAVE_COMPLETION] 완료 기록 저장 성공: completionId={}", completion.getCompletionId());
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [SAVE_COMPLETION] 완료 기록 저장 오류: completionId={}, 오류={}",
|
||||
completion.getCompletionId(), e.getMessage(), e);
|
||||
throw new RuntimeException("완료 기록 저장 중 오류가 발생했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculateStreakDays(String memberSerialNumber, String missionId) {
|
||||
log.info("📊 [STREAK_DAYS] 연속 달성일수 계산: memberSerialNumber={}, missionId={}", memberSerialNumber, missionId);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "calculateStreakDays");
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
|
||||
LocalDate endDate = LocalDate.now();
|
||||
LocalDate startDate = endDate.minusDays(30);
|
||||
|
||||
List<MissionCompletionHistoryEntity> recentCompletions = missionCompletionJpaRepository
|
||||
.findByMissionIdAndMemberSerialNumberAndCompletionDateBetweenOrderByCompletionDateDesc(
|
||||
missionIdLong, memberSerialNumberLong, startDate, endDate);
|
||||
|
||||
int streakDays = 0;
|
||||
LocalDate checkDate = LocalDate.now();
|
||||
|
||||
while (true) {
|
||||
boolean foundCompleted = false;
|
||||
|
||||
for (MissionCompletionHistoryEntity completion : recentCompletions) {
|
||||
if (completion.getCompletionDate().equals(checkDate) && completion.isTargetAchieved()) {
|
||||
streakDays++;
|
||||
foundCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundCompleted || checkDate.isBefore(startDate)) {
|
||||
break;
|
||||
}
|
||||
|
||||
checkDate = checkDate.minusDays(1);
|
||||
}
|
||||
|
||||
log.info("✅ [STREAK_DAYS] 연속 달성일수 계산 완료: memberSerialNumber={}, missionId={}, streakDays={}",
|
||||
memberSerialNumber, missionId, streakDays);
|
||||
|
||||
return streakDays;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [STREAK_DAYS] 연속 달성일수 계산 오류: memberSerialNumber={}, missionId={}, 오류={}",
|
||||
memberSerialNumber, missionId, e.getMessage(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🎯 오늘 해당 미션의 목표를 달성했는지 확인합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param missionId 미션 ID
|
||||
* @return 목표 달성 여부
|
||||
*/
|
||||
@Override
|
||||
public boolean isTodayTargetAchieved(String memberSerialNumber, String missionId) {
|
||||
log.info("🎯 [TARGET_CHECK] 오늘 목표 달성 여부 확인: memberSerialNumber={}, missionId={}",
|
||||
memberSerialNumber, missionId);
|
||||
|
||||
try {
|
||||
Long memberSerialNumberLong = UserIdValidator.parseMemberSerialNumber(memberSerialNumber, "isTodayTargetAchieved");
|
||||
Long missionIdLong = Long.parseLong(missionId);
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// 오늘 해당 미션의 완료 이력 조회
|
||||
List<MissionCompletionHistoryEntity> todayCompletions = missionCompletionJpaRepository
|
||||
.findByMemberSerialNumberAndMissionIdAndCompletionDate(memberSerialNumberLong, missionIdLong, today);
|
||||
|
||||
if (todayCompletions.isEmpty()) {
|
||||
log.info("📝 [TARGET_CHECK] 오늘 완료 이력 없음: memberSerialNumber={}, missionId={}",
|
||||
memberSerialNumber, missionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 가장 최근 완료 이력 확인
|
||||
MissionCompletionHistoryEntity latestCompletion = todayCompletions.get(0);
|
||||
boolean isAchieved = latestCompletion.getDailyCompletedCount() >= latestCompletion.getDailyTargetCount();
|
||||
|
||||
log.info("✅ [TARGET_CHECK] 목표 달성 여부: memberSerialNumber={}, missionId={}, completed={}/{}, achieved={}",
|
||||
memberSerialNumber, missionId,
|
||||
latestCompletion.getDailyCompletedCount(),
|
||||
latestCompletion.getDailyTargetCount(),
|
||||
isAchieved);
|
||||
|
||||
return isAchieved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ [TARGET_CHECK] 목표 달성 여부 확인 중 오류: memberSerialNumber={}, missionId={}, error={}",
|
||||
memberSerialNumber, missionId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.healthsync.goal.infrastructure.repositories;
|
||||
|
||||
import com.healthsync.goal.infrastructure.entities.MissionCompletionHistoryEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 미션 완료 기록을 위한 JPA 리포지토리입니다.
|
||||
* DDL의 mission_completion_history 테이블과 매핑됩니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Repository
|
||||
public interface MissionCompletionJpaRepository extends JpaRepository<MissionCompletionHistoryEntity, Long> {
|
||||
|
||||
/**
|
||||
* 회원 시리얼번호와 미션 ID로 완료 이력 조회 (DDL 맞춤)
|
||||
*/
|
||||
@Query("SELECT mch FROM MissionCompletionHistoryEntity mch WHERE mch.memberSerialNumber = :memberSerialNumber AND mch.missionId = :missionId ORDER BY mch.completionDate DESC")
|
||||
List<MissionCompletionHistoryEntity> findByMemberSerialNumberAndMissionId(@Param("memberSerialNumber") Long memberSerialNumber, @Param("missionId") Long missionId);
|
||||
|
||||
/**
|
||||
* 특정 날짜의 완료 이력 조회 (DDL 맞춤)
|
||||
*/
|
||||
@Query("SELECT mch FROM MissionCompletionHistoryEntity mch WHERE mch.memberSerialNumber = :memberSerialNumber AND mch.completionDate = :completionDate")
|
||||
List<MissionCompletionHistoryEntity> findByMemberSerialNumberAndCompletionDate(@Param("memberSerialNumber") Long memberSerialNumber, @Param("completionDate") LocalDate completionDate);
|
||||
|
||||
/**
|
||||
* 회원 시리얼번호와 미션 ID, 특정 날짜의 완료 여부 확인 (DDL 맞춤)
|
||||
*/
|
||||
boolean existsByMemberSerialNumberAndMissionIdAndCompletionDate(Long memberSerialNumber, Long missionId, LocalDate completionDate);
|
||||
|
||||
/**
|
||||
* 회원 시리얼번호와 미션 ID로 완료 횟수 조회 (DDL 맞춤)
|
||||
*/
|
||||
int countByMemberSerialNumberAndMissionId(Long memberSerialNumber, Long missionId);
|
||||
|
||||
/**
|
||||
* 미션 ID, 회원 시리얼 번호, 완료 날짜로 완료 기록을 조회합니다.
|
||||
*/
|
||||
@Query("SELECT mch FROM MissionCompletionHistoryEntity mch WHERE mch.missionId = :missionId AND mch.memberSerialNumber = :memberSerialNumber AND mch.completionDate = :completionDate")
|
||||
Optional<MissionCompletionHistoryEntity> findByMissionIdAndMemberSerialNumberAndCompletionDate(
|
||||
@Param("missionId") Long missionId,
|
||||
@Param("memberSerialNumber") Long memberSerialNumber,
|
||||
@Param("completionDate") LocalDate completionDate);
|
||||
|
||||
/**
|
||||
* 미션 ID와 회원 시리얼 번호로 특정 기간의 완료 기록을 날짜 내림차순으로 조회합니다.
|
||||
*/
|
||||
@Query("SELECT mch FROM MissionCompletionHistoryEntity mch WHERE mch.missionId = :missionId AND mch.memberSerialNumber = :memberSerialNumber AND mch.completionDate BETWEEN :startDate AND :endDate ORDER BY mch.completionDate DESC")
|
||||
List<MissionCompletionHistoryEntity> findByMissionIdAndMemberSerialNumberAndCompletionDateBetweenOrderByCompletionDateDesc(
|
||||
@Param("missionId") Long missionId,
|
||||
@Param("memberSerialNumber") Long memberSerialNumber,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 기간별 미션 완료 이력 조회 (findMissionHistoryByPeriod에서 사용)
|
||||
*/
|
||||
@Query("SELECT mch FROM MissionCompletionHistoryEntity mch " +
|
||||
"WHERE mch.memberSerialNumber = :memberSerialNumber " +
|
||||
"AND mch.completionDate BETWEEN :startDate AND :endDate " +
|
||||
"ORDER BY mch.completionDate DESC")
|
||||
List<MissionCompletionHistoryEntity> findByMemberSerialNumberAndDateRange(
|
||||
@Param("memberSerialNumber") Long memberSerialNumber,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* 특정 회원의 특정 미션에 대한 특정 날짜의 완료 이력을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param missionId 미션 ID
|
||||
* @param completionDate 완료 날짜
|
||||
* @return 완료 이력 목록
|
||||
*/
|
||||
List<MissionCompletionHistoryEntity> findByMemberSerialNumberAndMissionIdAndCompletionDate(
|
||||
Long memberSerialNumber, Long missionId, LocalDate completionDate);
|
||||
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.healthsync.goal.infrastructure.repositories;
|
||||
|
||||
import com.healthsync.goal.infrastructure.entities.UserMissionGoalEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* DDL의 user_mission_goal 테이블을 위한 JPA 리포지토리입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Repository
|
||||
public interface UserMissionGoalJpaRepository extends JpaRepository<UserMissionGoalEntity, Long> {
|
||||
|
||||
/**
|
||||
* 회원 시리얼 번호로 활성 미션 목표 조회
|
||||
*/
|
||||
@Query("SELECT umg FROM UserMissionGoalEntity umg WHERE umg.memberSerialNumber = :memberSerialNumber AND umg.isActive = true")
|
||||
List<UserMissionGoalEntity> findActiveByMemberSerialNumber(@Param("memberSerialNumber") Long memberSerialNumber);
|
||||
|
||||
/**
|
||||
* 특정 날짜의 미션 목표 조회
|
||||
*/
|
||||
@Query("SELECT umg FROM UserMissionGoalEntity umg WHERE umg.memberSerialNumber = :memberSerialNumber AND umg.performanceDate = :performanceDate AND umg.isActive = true")
|
||||
List<UserMissionGoalEntity> findByMemberSerialNumberAndPerformanceDate(@Param("memberSerialNumber") Long memberSerialNumber, @Param("performanceDate") LocalDate performanceDate);
|
||||
|
||||
/**
|
||||
* 미션 ID로 조회 (명시적 쿼리 추가)
|
||||
*/
|
||||
@Query("SELECT umg FROM UserMissionGoalEntity umg WHERE umg.missionId = :missionId")
|
||||
Optional<UserMissionGoalEntity> findByMissionId(@Param("missionId") Long missionId);
|
||||
/**
|
||||
* 미션 ID와 회원 시리얼 번호로 미션을 조회합니다.
|
||||
*/
|
||||
@Query("SELECT umg FROM UserMissionGoalEntity umg WHERE umg.missionId = :missionId AND umg.memberSerialNumber = :memberSerialNumber")
|
||||
Optional<UserMissionGoalEntity> findByMissionIdAndMemberSerialNumber(@Param("missionId") Long missionId, @Param("memberSerialNumber") Long memberSerialNumber);
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.healthsync.goal.infrastructure.services;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* DDL에서 자동증가가 없는 ID를 생성하는 서비스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class IdGeneratorService {
|
||||
|
||||
private static final AtomicLong MISSION_ID_COUNTER = new AtomicLong(0);
|
||||
private static final AtomicLong COMPLETION_ID_COUNTER = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* 새로운 미션 ID를 생성합니다.
|
||||
* 현재 시간(밀리초) + 증가값으로 유니크한 ID 생성
|
||||
*/
|
||||
public Long generateMissionId() {
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
long counter = MISSION_ID_COUNTER.incrementAndGet();
|
||||
long id = timestamp * 1000 + (counter % 1000);
|
||||
|
||||
log.debug("Generated mission ID: {}", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개선된 완료 이력 ID 생성 (Snowflake 방식 참고)
|
||||
*/
|
||||
public Long generateCompletionId() {
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
long counter = COMPLETION_ID_COUNTER.incrementAndGet();
|
||||
|
||||
// ✅ 더 안전한 방식: 타임스탬프 + 시퀀스
|
||||
long id = (timestamp << 12) + (counter & 0xFFF); // 12비트 시퀀스
|
||||
|
||||
log.debug("Generated completion ID: {} (timestamp: {}, counter: {})",
|
||||
id, timestamp, counter & 0xFFF);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.healthsync.goal.infrastructure.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* member_serial_number 검증 및 변환 유틸리티 클래스입니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserIdValidator {
|
||||
|
||||
/**
|
||||
* memberSerialNumber 문자열을 Long으로 안전하게 변환합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호 문자열
|
||||
* @param methodName 호출한 메서드명 (로깅용)
|
||||
* @return 변환된 Long 값
|
||||
* @throws IllegalArgumentException 변환 실패 시
|
||||
*/
|
||||
public static Long parseMemberSerialNumber(String memberSerialNumber, String methodName) {
|
||||
if (memberSerialNumber == null || memberSerialNumber.trim().isEmpty()) {
|
||||
log.error("❌ [MEMBER_SERIAL_VALIDATION] 메서드: {}, 오류: memberSerialNumber가 null 또는 빈 문자열입니다", methodName);
|
||||
throw new IllegalArgumentException("회원 시리얼 번호가 유효하지 않습니다: null 또는 빈 값");
|
||||
}
|
||||
|
||||
try {
|
||||
Long parsedMemberSerialNumber = Long.parseLong(memberSerialNumber.trim());
|
||||
log.debug("✅ [MEMBER_SERIAL_VALIDATION] 메서드: {}, memberSerialNumber 변환 성공: {} -> {}", methodName, memberSerialNumber, parsedMemberSerialNumber);
|
||||
return parsedMemberSerialNumber;
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("❌ [MEMBER_SERIAL_VALIDATION] 메서드: {}, 포맷 오류: 입력값='{}', 오류메시지='{}'",
|
||||
methodName, memberSerialNumber, e.getMessage());
|
||||
throw new IllegalArgumentException(
|
||||
String.format("회원 시리얼 번호 형식이 올바르지 않습니다. 입력값: '%s', 숫자만 입력 가능합니다.", memberSerialNumber), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* memberSerialNumber가 유효한 숫자 형식인지 검증합니다.
|
||||
*
|
||||
* @param memberSerialNumber 검증할 회원 시리얼 번호
|
||||
* @return 유효하면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isValidMemberSerialNumber(String memberSerialNumber) {
|
||||
if (memberSerialNumber == null || memberSerialNumber.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Long.parseLong(memberSerialNumber.trim());
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 메서드와의 호환성을 위해 유지 (deprecated)
|
||||
@Deprecated
|
||||
public static Long parseUserId(String userId, String methodName) {
|
||||
return parseMemberSerialNumber(userId, methodName);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static boolean isValidUserId(String userId) {
|
||||
return isValidMemberSerialNumber(userId);
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.healthsync.goal.interface_adapters.controllers;
|
||||
|
||||
import com.healthsync.common.dto.ApiResponse;
|
||||
import com.healthsync.goal.application_services.GoalUseCase;
|
||||
import com.healthsync.goal.dto.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 목표 관리 관련 API를 제공하는 컨트롤러입니다.
|
||||
* Clean Architecture의 Interface Adapter 계층에 해당합니다.
|
||||
*
|
||||
* @author healthsync-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/goals")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "목표 관리", description = "건강 목표 설정 및 미션 관리 API")
|
||||
public class GoalController {
|
||||
|
||||
private final GoalUseCase goalUseCase;
|
||||
|
||||
/**
|
||||
* 미션을 선택하고 목표를 설정합니다.
|
||||
* 이제 미션의 상세 정보(제목, 일일 목표 횟수, 사유)를 함께 받습니다.
|
||||
*
|
||||
* @param request 미션 선택 요청 (상세 정보 포함)
|
||||
* @return 목표 설정 결과
|
||||
*/
|
||||
@PostMapping("/missions/select")
|
||||
@Operation(summary = "미션 선택 및 목표 설정",
|
||||
description = "사용자가 선택한 미션의 상세 정보로 건강 목표를 설정합니다. " +
|
||||
"미션 제목, 일일 목표 횟수, 선정 사유를 모두 포함해야 합니다.")
|
||||
public ResponseEntity<ApiResponse<GoalSetupResponse>> selectMissions(@Valid @RequestBody MissionSelectionRequest request) {
|
||||
log.info("미션 선택 및 목표 설정 요청: memberSerialNumber={}, missionCount={}",
|
||||
request.getMemberSerialNumber(), request.getSelectedMissionIds().size());
|
||||
|
||||
// 각 미션의 상세 정보 로깅
|
||||
request.getSelectedMissionIds().forEach(mission ->
|
||||
log.info("선택된 미션: title={}, daily_target={}, reason={}",
|
||||
mission.getTitle(), mission.getDaily_target_count(), mission.getReason())
|
||||
);
|
||||
|
||||
GoalSetupResponse response = goalUseCase.selectMissions(request);
|
||||
|
||||
log.info("미션 선택 및 목표 설정 완료: memberSerialNumber={}, goalId={}",
|
||||
request.getMemberSerialNumber(), response.getGoalId());
|
||||
return ResponseEntity.ok(ApiResponse.success("목표 설정이 완료되었습니다.", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정된 활성 미션을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @return 활성 미션 목록
|
||||
*/
|
||||
@GetMapping("/missions/active")
|
||||
@Operation(summary = "설정된 목표 조회", description = "사용자의 현재 활성 미션과 진행 상황을 조회합니다")
|
||||
public ResponseEntity<ApiResponse<ActiveMissionsResponse>> getActiveMissions(@RequestParam String memberSerialNumber) {
|
||||
log.info("활성 미션 조회 요청: memberSerialNumber={}", memberSerialNumber);
|
||||
|
||||
ActiveMissionsResponse response = goalUseCase.getActiveMissions(memberSerialNumber);
|
||||
|
||||
log.info("활성 미션 조회 완료: memberSerialNumber={}, totalMissions={}", memberSerialNumber, response.getTotalMissions());
|
||||
return ResponseEntity.ok(ApiResponse.success("활성 미션 조회가 완료되었습니다.", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 완료를 처리합니다.
|
||||
*
|
||||
* @param missionId 미션 ID
|
||||
* @param request 미션 완료 요청
|
||||
* @return 미션 완료 결과
|
||||
*/
|
||||
@PutMapping("/missions/{missionId}/complete")
|
||||
@Operation(summary = "미션 완료 처리", description = "사용자의 미션 완료를 기록하고 성과를 업데이트합니다")
|
||||
public ResponseEntity<ApiResponse<MissionCompleteResponse>> completeMission(
|
||||
@PathVariable String missionId,
|
||||
@Valid @RequestBody MissionCompleteRequest request) {
|
||||
log.info("미션 완료 처리 요청: memberSerialNumber={}, missionId={}", request.getMemberSerialNumber(), missionId);
|
||||
|
||||
MissionCompleteResponse response = goalUseCase.completeMission(missionId, request);
|
||||
|
||||
log.info("미션 완료 처리 완료: memberSerialNumber={}, missionId={}, streakDays={}",
|
||||
request.getMemberSerialNumber(), missionId, response.getNewStreakDays());
|
||||
return ResponseEntity.ok(ApiResponse.success("미션 완료가 기록되었습니다.", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션 달성 이력을 조회합니다.
|
||||
*
|
||||
* @param memberSerialNumber 회원 시리얼 번호
|
||||
* @param startDate 시작일
|
||||
* @param endDate 종료일
|
||||
* @param missionIds 미션 ID 목록
|
||||
* @return 미션 달성 이력
|
||||
*/
|
||||
@GetMapping("/missions/history")
|
||||
@Operation(summary = "미션 달성 이력 조회", description = "지정한 기간의 미션 달성 이력과 통계를 조회합니다")
|
||||
public ResponseEntity<ApiResponse<MissionHistoryResponse>> getMissionHistory(
|
||||
@RequestParam String memberSerialNumber,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) String missionIds) {
|
||||
log.info("미션 이력 조회 요청: memberSerialNumber={}, period={} to {}", memberSerialNumber, startDate, endDate);
|
||||
|
||||
MissionHistoryResponse response = goalUseCase.getMissionHistory(memberSerialNumber, startDate, endDate, missionIds);
|
||||
|
||||
log.info("미션 이력 조회 완료: memberSerialNumber={}, achievementRate={}", memberSerialNumber, response.getTotalAchievementRate());
|
||||
return ResponseEntity.ok(ApiResponse.success("미션 이력 조회가 완료되었습니다.", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 미션을 재설정합니다.
|
||||
*
|
||||
* @param request 미션 재설정 요청
|
||||
* @return 미션 재설정 결과
|
||||
*/
|
||||
@PostMapping("/missions/reset")
|
||||
@Operation(summary = "목표 재설정", description = "현재 미션을 중단하고 새로운 미션으로 재설정합니다")
|
||||
public ResponseEntity<ApiResponse<MissionResetResponse>> resetMissions(@Valid @RequestBody MissionResetRequest request) {
|
||||
log.info("미션 재설정 요청: memberSerialNumber={}, reason={}", request.getMemberSerialNumber(), request.getReason());
|
||||
|
||||
MissionResetResponse response = goalUseCase.resetMissions(request);
|
||||
|
||||
log.info("미션 재설정 완료: memberSerialNumber={}, newRecommendationCount={}",
|
||||
request.getMemberSerialNumber(), response.getNewRecommendations().size());
|
||||
return ResponseEntity.ok(ApiResponse.success("미션 재설정이 완료되었습니다.", response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8084} # 🔧 포트도 8084로 수정 (8082는 health-service)
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: goal-service
|
||||
|
||||
# main:
|
||||
# allow-bean-definition-overriding: true
|
||||
|
||||
# ✅ docker-compose.yml의 환경변수명 그대로 사용
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/healthsync}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
format_sql: true
|
||||
default_schema: goal_service
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6380} # 🔧 Azure Redis SSL 포트
|
||||
password: ${REDIS_PASSWORD:HUezXQsxbphIeBy8FV9JDA3WaZDwOozGEAzCaByUk40=}
|
||||
timeout: 2000ms
|
||||
ssl:
|
||||
enabled: ${REDIS_SSL_ENABLED:true} # 🔧 SSL 활성화
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true # Swagger UI 활성화
|
||||
path: /swagger-ui.html # Swagger UI 접근 경로
|
||||
disable-swagger-default-url: false # 기본 Swagger URL 사용
|
||||
operations-sorter: method # API 메소드별 정렬
|
||||
tags-sorter: alpha # 태그 알파벳 순 정렬
|
||||
doc-expansion: none # 문서 확장 방식 (none/list/full)
|
||||
api-docs:
|
||||
enabled: true # API 문서 생성 활성화
|
||||
path: /v3/api-docs # OpenAPI 3.0 JSON 문서 경로
|
||||
show-actuator: true # Actuator 엔드포인트 포함
|
||||
packages-to-scan: com.healthsync.goal.interface_adapters.controllers # 스캔할 패키지 명시
|
||||
|
||||
services:
|
||||
user-service:
|
||||
url: ${USER_SERVICE_URL:http://localhost:8081}
|
||||
timeout: ${USER_SERVICE_TIMEOUT:30}
|
||||
|
||||
# 🆕 Intelligence Service 설정 추가
|
||||
intelligence-service:
|
||||
url: ${INTELLIGENCE_SERVICE_URL:http://localhost:8083}
|
||||
timeout: ${INTELLIGENCE_SERVICE_TIMEOUT:30}
|
||||
|
||||
# JWT 설정
|
||||
jwt:
|
||||
secret-key: ${JWT_SECRET:healthsync-secret-key-2024-very-long-secret-key}
|
||||
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_VALIDITY:86400000}
|
||||
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
com.healthsync.goal: DEBUG # 🔧 DEBUG로 변경
|
||||
org.springframework.web: DEBUG # 🔧 DEBUG로 변경
|
||||
org.springframework.web.servlet.mvc.method.annotation: DEBUG # 🔧 매핑 정보 확인
|
||||
org.springframework.data.redis: DEBUG # Spring Data Redis 전체
|
||||
org.springframework.data.redis.connection: TRACE # Redis 연결 관련
|
||||
org.springframework.data.redis.core: DEBUG # RedisTemplate 관련
|
||||
|
||||
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# ✅ management는 logging과 같은 레벨이어야 함 (logging 하위가 아님)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: true
|
||||
Reference in New Issue
Block a user