From 04c32417e7ec92d37f036d5511750619bc810380 Mon Sep 17 00:00:00 2001 From: P82288200 Date: Fri, 20 Jun 2025 06:54:13 +0000 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=97=AD=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/API 설계서.txt | 26 + design/내부 시퀀스 설계서 - Goal Service.txt | 206 +++ .../내부 시퀀스 설계서 - Health Service.txt | 179 ++ ...부 시퀀스 설계서 - Intelligence Service.txt | 257 +++ design/내부 시퀀스 설계서 - User Service.txt | 177 ++ design/논리 아키텍처.txt | 172 ++ design/데이터설계서 - Goal Service.txt | 291 ++++ design/데이터설계서 - Health Service.txt | 303 ++++ .../데이터설계서 - Intelligence Service.txt | 310 ++++ design/데이터설계서 - User Service.txt | 173 ++ design/물리아키텍처.txt | 227 +++ design/외부 시퀀스 다이어그램.txt | 140 ++ design/클래스 설계서.txt | 1534 +++++++++++++++++ 13 files changed, 3995 insertions(+) create mode 100644 design/API 설계서.txt create mode 100644 design/내부 시퀀스 설계서 - Goal Service.txt create mode 100644 design/내부 시퀀스 설계서 - Health Service.txt create mode 100644 design/내부 시퀀스 설계서 - Intelligence Service.txt create mode 100644 design/내부 시퀀스 설계서 - User Service.txt create mode 100644 design/논리 아키텍처.txt create mode 100644 design/데이터설계서 - Goal Service.txt create mode 100644 design/데이터설계서 - Health Service.txt create mode 100644 design/데이터설계서 - Intelligence Service.txt create mode 100644 design/데이터설계서 - User Service.txt create mode 100644 design/물리아키텍처.txt create mode 100644 design/외부 시퀀스 다이어그램.txt create mode 100644 design/클래스 설계서.txt diff --git a/design/API 설계서.txt b/design/API 설계서.txt new file mode 100644 index 0000000..4f3d353 --- /dev/null +++ b/design/API 설계서.txt @@ -0,0 +1,26 @@ +사용자 컨텍스트|User Service|USR-001|구글 로그인|AuthController|Google OAuth 인증 처리|POST|/api/users|/auth/google-login||||GoogleLoginRequest|FALSE|{googleAccessToken: String, googleIdToken: String}|LoginResponse|FALSE|{accessToken: String, refreshToken: String, userId: String, isNewUser: boolean, message: String} +사용자 컨텍스트|User Service|USR-002|프로필 완료|AuthController|사용자 프로필 정보 저장|POST|/api/users|/profile/complete||||UserRegistrationRequest|FALSE|{name: String, birthDate: String, occupation: String}|UserRegistrationResponse|FALSE|{userId: String, message: String, status: String, profileCompletedAt: String} +사용자 컨텍스트|User Service|USR-003|사용자 기본 정보 조회|AuthController|사용자 프로필 조회|GET|/api/users|/profile|||||||UserProfileResponse|FALSE|{userId: String, name: String, age: int, occupation: String, registeredAt: String, lastLoginAt: String} +사용자 컨텍스트|User Service||직업 코드 변환|UserController|직업명을 코드로 변환|GET|/api/users|/occupations/code||occupationName occupationName|||||OccupationCodeResponse|FALSE|{occupationCode: String, occupationName: String} +사용자 컨텍스트|User Service||직업명 조회|UserController|직업 코드를 명칭으로 변환|GET|/api/users|/occupations/name||occupationCode occupationCode|||||OccupationNameResponse|FALSE|{occupationCode: String, occupationName: String} +사용자 컨텍스트|User Service||전체 직업 목록|UserController|모든 직업 목록 조회|GET|/api/users|/occupations|||||||OccupationListResponse|TRUE|{occupations: [{occupationCode: String, occupationName: String, category: String}], totalCount: int} + +건강 컨텍스트|Health Service|USR-009|건강검진 연동|HealthController|건강보험공단 건강검진 데이터 연동|POST|/api/health|/checkup/sync||userId userId|||||HealthSyncResponse|FALSE|{syncedRecords: int, newRecords: int, updatedRecords: int, skippedRecords: int, lastSyncedCheckup: Object, message: String} +건강 컨텍스트|Health Service||건강검진 이력 조회|HealthController|사용자 건강검진 이력 조회|GET|/api/health|/checkup/history||limit limit|||||HealthHistoryResponse|FALSE|{checkupHistory: [{referenceYear: int, heightCm: double, weightKg: double, waistCm: double, bmi: double, systolicBp: int, diastolicBp: int, fastingGlucose: int, totalCholesterol: int, healthScore: int, riskLevel: String, abnormalIndicators: [String], analysisDate: String}], totalRecords: int, averageHealthScore: double, trendAnalysis: String, normalRangeReference: Object} +건강 컨텍스트|Health Service||건강검진 파일 업로드|HealthController|건강검진 파일 Azure Blob 저장|POST|/api/health|/checkup/upload||||CheckupFileRequest|FALSE|{userId: String, fileName: String, fileType: String, fileContent: String}|FileUploadResponse|FALSE|{fileId: String, uploadUrl: String, status: String, message: String} +건강 컨텍스트|Health Service||정상치 기준 조회|HealthController|성별별 건강검진 정상치 기준|GET|/api/health|/normal-ranges||genderCode genderCode|||||NormalRangeResponse|TRUE|{normalRanges: [{itemCode: String, itemName: String, genderCode: int, normalMin: double, normalMax: double, cautionMin: double, cautionMax: double, dangerMin: double, dangerMax: double, unit: String}], genderCode: int} + +목표 컨텍스트|Goal Service|USR-010|목표 설정 시작|GoalController|미션 선택 및 목표 설정|POST|/api/goals|/missions/select||||MissionSelectionRequest|FALSE|{userId: String, selectedMissionIds: [String]}|GoalSetupResponse|FALSE|{goalId: String, selectedMissions: [{missionId: String, title: String, description: String, startDate: String}], message: String, setupCompletedAt: String} +목표 컨텍스트|Goal Service|USR-011|설정한 목표 관리|GoalController|설정된 목표 조회|GET|/api/goals|/missions/active|||||||ActiveMissionsResponse|FALSE|{dailyMissions: [{missionId: String, title: String, description: String, status: String, completedToday: boolean, streakDays: int, nextReminderTime: String}], totalMissions: int, todayCompletedCount: int, completionRate: double} +목표 컨텍스트|Goal Service|USR-012|목표 달성 기록|GoalController|미션 완료 처리|PUT|/api/goals|/missions/{missionId}/complete|missionId missionId|||MissionCompleteRequest|FALSE|{userId: String, completed: boolean, completedAt: String, notes: String}|MissionCompleteResponse|FALSE|{message: String, status: String, achievementMessage: String, newStreakDays: int, totalCompletedCount: int, earnedPoints: int} +목표 컨텍스트|Goal Service|USR-013|목표 달성 이력|GoalController|미션 달성 이력 조회|GET|/api/goals|/missions/history||startDate startDate, endDate endDate, missionIds missionIds|||||MissionHistoryResponse|FALSE|{totalAchievementRate: double, periodAchievementRate: double, bestStreak: int, missionStats: [{missionId: String, title: String, achievementRate: double, completedDays: int, totalDays: int}], chartData: Object, period: {startDate: String, endDate: String}, insights: [String]} +목표 컨텍스트|Goal Service||목표 재설정|GoalController|미션 재설정|POST|/api/goals|/missions/reset||||MissionResetRequest|FALSE|{userId: String, reason: String, currentMissionIds: [String]}|MissionResetResponse|FALSE|{message: String, newRecommendations: [{missionId: String, title: String, description: String, category: String}], resetCompletedAt: String} + +지능형서비스 컨텍스트|Intelligence Service|USR-004|AI 3줄 요약 진단|AnalysisController|건강검진 결과 AI 3줄 요약|GET|/api/intelligence|/health/diagnosis||userId userId|||||HealthDiagnosisResponse|FALSE|{threeSentenceSummary: [String], healthScore: int, riskLevel: String, occupationConsiderations: String, analysisTimestamp: String, confidenceScore: double} +지능형서비스 컨텍스트|Intelligence Service|USR-004|AI 추천 건강 미션|AnalysisController|AI 기반 미션 추천|POST|/api/intelligence|/missions/recommend||||MissionRecommendationRequest|FALSE|{userId: String, currentHealthStatus: String, preferences: [String]}|MissionRecommendationResponse|FALSE|{missions: [{missionId: String, title: String, description: String, category: String, difficulty: String, healthBenefit: String, occupationRelevance: String, estimatedTimeMinutes: int}], recommendationReason: String, totalRecommended: int} +지능형서비스 컨텍스트|Intelligence Service|USR-005|챗봇 상담|ChatController|AI 채팅 상담|POST|/api/intelligence|/chat/consultation||||ChatRequest|FALSE|{message: String, sessionId: String, context: String}|ChatResponse|FALSE|{response: String, sessionId: String, timestamp: String, suggestedQuestions: [String], responseType: String} +지능형서비스 컨텍스트|Intelligence Service||채팅 히스토리 조회|ChatController|채팅 기록 조회|GET|/api/intelligence|/chat/history||sessionId sessionId, limit messageLimit|||||ChatHistoryResponse|FALSE|{sessionId: String, messages: [{role: String, content: String, timestamp: String}], totalMessageCount: int, cacheExpiration: String} +지능형서비스 컨텍스트|Intelligence Service|USR-007|미션 달성 축하|NotificationController|미션 달성 축하 메시지|POST|/api/intelligence|/notifications/celebration||||CelebrationRequest|FALSE|{userId: String, missionId: String, achievementType: String, consecutiveDays: int, totalAchievements: int}|CelebrationResponse|FALSE|{congratsMessage: String, achievementBadge: String, healthBenefit: String, nextMilestone: String, encouragementLevel: String, visualEffect: String} +지능형서비스 컨텍스트|Intelligence Service|USR-006|미션 독려|NotificationController|미션 독려 메시지 생성|POST|/api/intelligence|/notifications/encouragement||||EncouragementRequest|FALSE|{userId: String, missionsStatus: [{missionId: String, completed: boolean}]}|EncouragementResponse|FALSE|{message: String, motivationType: String, timing: String, personalizedTip: String, priority: String} +지능형서비스 컨텍스트|Intelligence Service||배치 알림 처리|BatchController|주기적 AI 알림 트리거|POST|/api/intelligence|/batch/notifications||||BatchNotificationRequest|FALSE|{triggerTime: String, targetUsers: [String], notificationType: String}|BatchNotificationResponse|FALSE|{processedCount: int, successCount: int, failedCount: int, nextScheduledTime: String} + diff --git a/design/내부 시퀀스 설계서 - Goal Service.txt b/design/내부 시퀀스 설계서 - Goal Service.txt new file mode 100644 index 0000000..83a5f15 --- /dev/null +++ b/design/내부 시퀀스 설계서 - Goal Service.txt @@ -0,0 +1,206 @@ +!theme mono + +skinparam sequenceArrowThickness 2 +skinparam sequenceParticipantBorderThickness 2 +skinparam sequenceActorBorderThickness 2 +skinparam sequenceGroupBorderThickness 2 + +title Goal Service 내부 시퀀스 다이어그램 (역설계 - 개발 소스 기반) + +participant "GoalController" as GoalCtrl +participant "GoalUseCase" as GoalUC +participant "GoalDomainService" as GoalDomainSvc +participant "MissionProgressDomainService" as ProgressDomainSvc +participant "GoalRepository" as GoalRepo +participant "MissionProgressRepository" as ProgressRepo +participant "IntelligenceServiceAdapter" as IntelAdapter +participant "CacheAdapter" as CacheAdapter +participant "EventPublisherAdapter" as EventAdapter +participant "PostgreSQL" as PostgreSQL +participant "Redis Cache" as Redis +participant "Azure Service Bus" as ServiceBus + +== 1. POST /api/goals/missions/select (미션 선택 및 목표 설정) == + +GoalCtrl -> GoalUC: selectMissions(missionSelectionRequest) +note right: {userId, selectedMissionIds} + +GoalUC -> GoalDomainSvc: validateMissionSelection(userId, missionIds) +GoalDomainSvc -> GoalDomainSvc: checkMaximumMissions(selectedMissionIds) +note right: 최대 5개 미션 제한 검증 + +GoalUC -> GoalRepo: findActiveGoalByUserId(userId) +GoalRepo -> PostgreSQL: SELECT * FROM user_mission_goals WHERE user_id = ? AND is_active = true +PostgreSQL -> GoalRepo: 기존 활성 목표 조회 + +alt 기존 활성 목표가 있는 경우 + GoalUC -> GoalDomainSvc: deactivateExistingGoal(existingGoal) + GoalDomainSvc -> GoalRepo: updateGoalStatus(goalId, false) + GoalRepo -> PostgreSQL: UPDATE user_mission_goals SET is_active = false +end + +GoalUC -> GoalDomainSvc: createNewGoalWithMissions(userId, selectedMissionIds) +GoalDomainSvc -> GoalDomainSvc: generateGoalId() +GoalDomainSvc -> GoalDomainSvc: createUserMissionEntities(goalId, missionIds) + +GoalDomainSvc -> GoalRepo: saveGoalWithMissions(goalEntity, missionEntities) +GoalRepo -> PostgreSQL: BEGIN TRANSACTION +GoalRepo -> PostgreSQL: INSERT INTO user_mission_goals +GoalRepo -> PostgreSQL: INSERT INTO user_missions (batch) +GoalRepo -> PostgreSQL: COMMIT + +GoalUC -> CacheAdapter: invalidateActiveMissionsCache(userId) +CacheAdapter -> Redis: DEL goals:active:{userId} + +GoalUC -> EventAdapter: publishGoalSetEvent(userId, goalId, selectedMissions) +EventAdapter -> ServiceBus: 목표 설정 이벤트 발행 + +GoalUC -> GoalCtrl: GoalSetupResponse 반환 +note right: {goalId, selectedMissions, message, setupCompletedAt} + +== 2. GET /api/goals/missions/active (활성 미션 조회) == + +GoalCtrl -> GoalUC: getActiveMissions(userId) + +GoalUC -> CacheAdapter: getCachedActiveMissions(userId) +CacheAdapter -> Redis: GET goals:active:{userId} +Redis -> CacheAdapter: 캐시된 활성 미션 또는 null + +alt 캐시 미스인 경우 + GoalUC -> GoalRepo: findActiveMissionsByUserId(userId) + GoalRepo -> PostgreSQL: SELECT um.*, mp.* FROM user_missions um LEFT JOIN mission_progress mp ON um.mission_id = mp.mission_id WHERE um.user_id = ? AND um.is_active = true + PostgreSQL -> GoalRepo: 활성 미션 및 진행 상황 + + GoalUC -> ProgressDomainSvc: calculateMissionProgress(activeMissions) + ProgressDomainSvc -> ProgressDomainSvc: calculateCompletionRate(missions) + ProgressDomainSvc -> ProgressDomainSvc: calculateStreakDays(missions) + ProgressDomainSvc -> ProgressDomainSvc: determineTodayStatus(missions) + + GoalUC -> CacheAdapter: cacheActiveMissions(userId, processedMissions) + CacheAdapter -> Redis: SETEX goals:active:{userId} 1800 {data} + note right: 30분 TTL로 캐싱 +end + +GoalUC -> GoalCtrl: ActiveMissionsResponse 반환 +note right: {dailyMissions, totalMissions, todayCompletedCount, completionRate} + +== 3. PUT /api/goals/missions/{missionId}/complete (미션 완료 처리) == + +GoalCtrl -> GoalUC: completeMission(missionId, missionCompleteRequest) +note right: {userId, completed, completedAt, notes} + +GoalUC -> GoalDomainSvc: validateMissionCompletion(userId, missionId) +GoalDomainSvc -> GoalRepo: findUserMission(userId, missionId) +GoalRepo -> PostgreSQL: SELECT * FROM user_missions WHERE user_id = ? AND mission_id = ? AND is_active = true +PostgreSQL -> GoalRepo: 사용자 미션 정보 + +GoalUC -> ProgressRepo: findTodayProgress(userId, missionId) +ProgressRepo -> PostgreSQL: SELECT * FROM mission_progress WHERE user_id = ? AND mission_id = ? AND DATE(completed_at) = CURRENT_DATE +PostgreSQL -> ProgressRepo: 오늘의 진행 상황 + +alt 오늘 이미 완료한 경우 + GoalUC -> GoalCtrl: 409 Conflict 응답 + note right: "오늘 이미 완료된 미션입니다" +else 새로운 완료 기록 + GoalUC -> ProgressDomainSvc: recordMissionCompletion(userId, missionId, completionData) + ProgressDomainSvc -> ProgressDomainSvc: calculateNewStreakDays(previousProgress) + ProgressDomainSvc -> ProgressDomainSvc: calculateEarnedPoints(mission, streakDays) + + ProgressDomainSvc -> ProgressRepo: saveMissionProgress(progressEntity) + ProgressRepo -> PostgreSQL: INSERT INTO mission_progress + PostgreSQL -> ProgressRepo: 진행 기록 저장 완료 + + GoalUC -> ProgressDomainSvc: updateMissionStatistics(userId, missionId) + ProgressDomainSvc -> GoalRepo: updateMissionStats(missionId, newStats) + GoalRepo -> PostgreSQL: UPDATE user_missions SET total_completed_count = ?, current_streak_days = ? +end + +GoalUC -> CacheAdapter: invalidateUserCaches(userId) +CacheAdapter -> Redis: DEL goals:active:{userId} goals:history:{userId} + +GoalUC -> EventAdapter: publishMissionCompletedEvent(userId, missionId, achievementData) +EventAdapter -> ServiceBus: 미션 완료 이벤트 발행 + +GoalUC -> GoalCtrl: MissionCompleteResponse 반환 +note right: {message, status, achievementMessage, newStreakDays, totalCompletedCount, earnedPoints} + +== 4. GET /api/goals/missions/history (미션 달성 이력) == + +GoalCtrl -> GoalUC: getMissionHistory(userId, startDate, endDate, missionIds) + +GoalUC -> CacheAdapter: getCachedMissionHistory(userId, period) +CacheAdapter -> Redis: GET goals:history:{userId}:{period} +Redis -> CacheAdapter: 캐시된 이력 또는 null + +alt 캐시 미스인 경우 + GoalUC -> ProgressRepo: findMissionHistoryByPeriod(userId, startDate, endDate, missionIds) + ProgressRepo -> PostgreSQL: SELECT mp.*, um.mission_title FROM mission_progress mp JOIN user_missions um ON mp.mission_id = um.mission_id WHERE mp.user_id = ? AND mp.completed_at BETWEEN ? AND ? + PostgreSQL -> ProgressRepo: 기간별 미션 이력 데이터 + + GoalUC -> ProgressDomainSvc: analyzeAchievementStatistics(historyData) + ProgressDomainSvc -> ProgressDomainSvc: calculateAchievementRates(histories) + ProgressDomainSvc -> ProgressDomainSvc: findBestStreak(histories) + ProgressDomainSvc -> ProgressDomainSvc: generateInsights(statistics) + ProgressDomainSvc -> ProgressDomainSvc: prepareChartData(histories) + + GoalUC -> CacheAdapter: cacheMissionHistory(userId, period, analysisResult) + CacheAdapter -> Redis: SETEX goals:history:{userId}:{period} 3600 {data} + note right: 1시간 TTL로 캐싱 +end + +GoalUC -> GoalCtrl: MissionHistoryResponse 반환 +note right: {totalAchievementRate, periodAchievementRate, bestStreak, missionStats, chartData, period, insights} + +== 5. POST /api/goals/missions/reset (목표 재설정) == + +GoalCtrl -> GoalUC: resetMissions(missionResetRequest) +note right: {userId, reason, currentMissionIds} + +GoalUC -> IntelAdapter: requestNewMissionRecommendations(userId, reason) +IntelAdapter -> GoalUC: 새로운 추천 미션 목록 + +GoalUC -> GoalDomainSvc: deactivateCurrentGoal(userId) +GoalDomainSvc -> GoalRepo: updateGoalStatus(userId, false) +GoalRepo -> PostgreSQL: UPDATE user_mission_goals SET is_active = false + +GoalUC -> CacheAdapter: invalidateAllUserCaches(userId) +CacheAdapter -> Redis: DEL goals:active:{userId} goals:history:{userId}* + +GoalUC -> EventAdapter: publishGoalResetEvent(userId, reason, newRecommendations) +EventAdapter -> ServiceBus: 목표 재설정 이벤트 발행 + +GoalUC -> GoalCtrl: MissionResetResponse 반환 +note right: {message, newRecommendations, resetCompletedAt} + +== 예외 처리 == + +alt 미션 개수 초과 + GoalDomainSvc -> GoalUC: MissionLimitExceededException + GoalUC -> GoalCtrl: 400 Bad Request + note right: "미션은 최대 5개까지 선택 가능합니다" +end + +alt 미션 권한 없음 + GoalDomainSvc -> GoalUC: UnauthorizedMissionException + GoalUC -> GoalCtrl: 403 Forbidden +end + +alt 이미 완료된 미션 + ProgressDomainSvc -> GoalUC: MissionAlreadyCompletedException + GoalUC -> GoalCtrl: 409 Conflict +end + +== 트랜잭션 및 캐싱 == + +note over GoalUC, CacheAdapter +**트랜잭션 관리** +- @Transactional 어노테이션 적용 +- 미션 선택/완료 시 원자성 보장 +- 예외 발생 시 자동 롤백 + +**캐싱 전략** +- 활성 미션: 30분 TTL +- 이력 데이터: 1시간 TTL +- 완료/재설정 시 관련 캐시 무효화 +- 사용자별 캐시 키 분리 +end note \ No newline at end of file diff --git a/design/내부 시퀀스 설계서 - Health Service.txt b/design/내부 시퀀스 설계서 - Health Service.txt new file mode 100644 index 0000000..6b1808b --- /dev/null +++ b/design/내부 시퀀스 설계서 - Health Service.txt @@ -0,0 +1,179 @@ +!theme mono + +skinparam sequenceArrowThickness 2 +skinparam sequenceParticipantBorderThickness 2 +skinparam sequenceActorBorderThickness 2 +skinparam sequenceGroupBorderThickness 2 + +title Health Service 내부 시퀀스 다이어그램 (역설계 - 정상치 기준 비교 포함) + +participant "HealthController" as HealthCtrl +participant "CheckupSyncUseCase" as SyncUC +participant "CheckupQueryUseCase" as QueryUC +participant "FileUploadUseCase" as FileUC +participant "HealthProfileDomainService" as HealthDomainSvc +participant "CheckupAnalysisDomainService" as AnalysisDomainSvc +participant "NormalRangeDomainService" as NormalDomainSvc +participant "HealthRepository" as HealthRepo +participant "HealthCheckupRawRepository" as RawRepo +participant "NormalRangeRepository" as NormalRepo +participant "UserServiceAdapter" as UserAdapter +participant "BlobStorageAdapter" as BlobAdapter +participant "CacheAdapter" as CacheAdapter +participant "EventPublisherAdapter" as EventAdapter +participant "PostgreSQL" as PostgreSQL +participant "Redis Cache" as Redis +participant "Azure Blob Storage" as BlobStorage +participant "Azure Service Bus" as ServiceBus + +== 1. POST /api/health/checkup/sync (건강검진 결과 연동) == + +HealthCtrl -> SyncUC: syncCheckupData(userId) + +SyncUC -> UserAdapter: getUserInfo(userId) +UserAdapter -> SyncUC: UserInfo 응답 (memberSerialNumber, gender 포함) + +SyncUC -> RawRepo: findNhisCheckupDataByMemberSerial(memberSerialNumber) +RawRepo -> PostgreSQL: SELECT * FROM health_checkup_raw WHERE member_serial_number = ? ORDER BY reference_year DESC +PostgreSQL -> RawRepo: 건강보험공단 건강검진 원본 데이터 + +SyncUC -> HealthRepo: findByMemberSerialNumber(memberSerialNumber) +PostgreSQL -> HealthRepo: 기존 가공 데이터 조회 + +alt 기존 데이터가 없거나 더 최신 원본 데이터가 있는 경우 + SyncUC -> NormalRepo: getNormalRangesByGender(userGender) + NormalRepo -> PostgreSQL: SELECT * FROM health_normal_ranges WHERE gender_code IN (0, ?) ORDER BY item_code + note right: 성별별 + 공통 정상치 기준 조회 + PostgreSQL -> NormalRepo: 성별별/공통 정상치 기준 데이터 + + SyncUC -> AnalysisDomainSvc: transformAndAnalyzeCheckupData(rawData, normalRanges, userGender) + + AnalysisDomainSvc -> AnalysisDomainSvc: validateAndConvertData(rawData) + note right: 건강보험공단 데이터 검증 및 단위 변환 + + AnalysisDomainSvc -> NormalDomainSvc: compareWithNormalRanges(checkupData, normalRanges) + NormalDomainSvc -> NormalDomainSvc: evaluateEachIndicator(indicators, ranges) + note right: **각 지표별 정상/주의/위험 판정**\n- BMI, 혈압, 혈당, 콜레스테롤 등\n- 성별별 기준 적용 + + NormalDomainSvc -> NormalDomainSvc: calculateOverallRiskLevel(indicatorResults) + note right: **종합 위험도 레벨 계산**\n- 정상: 80-100점\n- 주의: 60-79점\n- 위험: 0-59점 + + NormalDomainSvc -> NormalDomainSvc: identifyAbnormalIndicators(indicatorResults) + note right: 이상 항목 식별 및 JSON 배열 생성 + + NormalDomainSvc -> AnalysisDomainSvc: NormalRangeAnalysisResult 반환 + + AnalysisDomainSvc -> AnalysisDomainSvc: createHealthCheckupEntity(transformedData, analysisResult) + AnalysisDomainSvc -> SyncUC: 변환된 HealthCheckupEntity (정상치 비교 결과 포함) + + SyncUC -> HealthRepo: saveOrUpdateHealthCheckup(healthCheckupEntity) + HealthRepo -> PostgreSQL: INSERT/UPDATE health_checkups + note right: 정상치 비교 결과도 함께 저장\n- abnormal_indicators: JSON 배열\n- health_score: 0-100점\n- risk_level: normal/caution/danger + PostgreSQL -> HealthRepo: 저장 완료 +end + +SyncUC -> CacheAdapter: invalidateUserHealthCache(userId) +CacheAdapter -> Redis: DEL health:history:{userId} health:normal:{userId} + +SyncUC -> EventAdapter: publishHealthDataSyncedEvent(userId, syncResult) +EventAdapter -> ServiceBus: 건강데이터 동기화 이벤트 발행 + +SyncUC -> HealthCtrl: HealthSyncResponse 반환 +note right: {syncedRecords, newRecords, updatedRecords, skippedRecords, lastSyncedCheckup, message} + +== 2. GET /api/health/checkup/history (건강검진 이력 조회) == + +HealthCtrl -> QueryUC: getHealthCheckupHistory(userId, limit) + +QueryUC -> CacheAdapter: getCachedHealthHistory(userId) +CacheAdapter -> Redis: GET health:history:{userId} +Redis -> CacheAdapter: 캐시된 데이터 또는 null + +alt 캐시 미스인 경우 + QueryUC -> HealthRepo: findCheckupHistoryWithDetails(userId, limit) + HealthRepo -> PostgreSQL: SELECT * FROM health_checkups WHERE user_id = ? ORDER BY reference_year DESC LIMIT ? + PostgreSQL -> HealthRepo: 건강검진 이력 데이터 (정상치 비교 결과 포함) + + QueryUC -> AnalysisDomainSvc: calculateTrendAnalysis(checkupHistory) + AnalysisDomainSvc -> AnalysisDomainSvc: analyzeTrendsByIndicator(historyData) + note right: **트렌드 분석**\n- 연도별 변화 추이\n- 개선/악화 항목 식별\n- 평균 건강점수 계산 + + QueryUC -> CacheAdapter: cacheHealthHistory(userId, analysisResult) + CacheAdapter -> Redis: SETEX health:history:{userId} 3600 {data} + note right: 1시간 TTL로 캐싱 +end + +QueryUC -> HealthCtrl: HealthHistoryResponse 반환 +note right: {checkupHistory, totalRecords, averageHealthScore, trendAnalysis, normalRangeReference} + +== 3. POST /api/health/checkup/upload (건강검진 파일 업로드) == + +HealthCtrl -> FileUC: uploadCheckupFile(uploadRequest) +note right: {userId, fileName, fileType, fileContent} + +FileUC -> FileUC: validateFileFormat(fileType, fileContent) +note right: 파일 형식 및 크기 검증 (최대 10MB) + +FileUC -> BlobAdapter: uploadToAzureBlob(fileName, fileContent) +BlobAdapter -> BlobStorage: Azure Blob Storage Upload +BlobStorage -> BlobAdapter: 업로드 완료 응답 + +FileUC -> HealthRepo: saveFileMetadata(fileMetadata) +HealthRepo -> PostgreSQL: INSERT INTO health_files (user_id, file_name, file_url, file_type, upload_status) +PostgreSQL -> HealthRepo: 파일 메타데이터 저장 완료 + +FileUC -> HealthCtrl: FileUploadResponse 반환 +note right: {fileId, uploadUrl, status, message} + +== 4. GET /api/health/normal-ranges (정상치 기준 조회) == + +HealthCtrl -> QueryUC: getNormalRangesByGender(genderCode) + +QueryUC -> CacheAdapter: getCachedNormalRanges(genderCode) +CacheAdapter -> Redis: GET normal:ranges:{genderCode} +Redis -> CacheAdapter: 캐시된 정상치 기준 또는 null + +alt 캐시 미스인 경우 + QueryUC -> NormalRepo: getNormalRangesByGender(genderCode) + NormalRepo -> PostgreSQL: SELECT * FROM health_normal_ranges WHERE gender_code IN (0, ?) ORDER BY item_code + PostgreSQL -> NormalRepo: 성별별/공통 정상치 기준 데이터 + + QueryUC -> CacheAdapter: cacheNormalRanges(genderCode, normalRanges) + CacheAdapter -> Redis: SETEX normal:ranges:{genderCode} 86400 {data} + note right: 24시간 TTL로 캐싱 (변경 빈도 낮음) +end + +QueryUC -> HealthCtrl: NormalRangeResponse 반환 +note right: {normalRanges: [{itemCode, itemName, genderCode, normalMin, normalMax, cautionMin, cautionMax, dangerMin, dangerMax, unit}]} + +== 예외 처리 (정상치 관련 추가) == + +alt 정상치 기준 데이터 없음 + NormalRepo -> AnalysisDomainSvc: NoNormalRangeFoundException + AnalysisDomainSvc -> SyncUC: 기본 정상치 기준 사용 + note right: 하드코딩된 기본값으로 대체 +end + +alt 정상치 비교 실패 + NormalDomainSvc -> AnalysisDomainSvc: NormalRangeComparisonException + AnalysisDomainSvc -> SyncUC: 정상치 비교 없이 기본 저장 + note right: 기본 건강검진 데이터만 저장 +end + +alt 파일 업로드 실패 + BlobAdapter -> FileUC: BlobStorageException + FileUC -> HealthRepo: updateUploadStatus(fileId, "FAILED") + FileUC -> HealthCtrl: 500 Internal Server Error +end + +== 캐싱 전략 (정상치 관련 추가) == + +note over QueryUC, CacheAdapter +**확장된 캐싱 전략** +- 건강검진 이력 + 정상치 비교: 1시간 캐시 +- 정상치 기준 데이터: 24시간 캐시 (변경 빈도 낮음) +- 건강 점수 계산 결과: 포함 (이력과 함께) +- 트렌드 분석 결과: 포함 (이력과 함께) +- 파일 메타데이터: 30분 캐시 +- 정상치 기준 업데이트 시 관련 캐시 무효화 +end note \ No newline at end of file diff --git a/design/내부 시퀀스 설계서 - Intelligence Service.txt b/design/내부 시퀀스 설계서 - Intelligence Service.txt new file mode 100644 index 0000000..09883f8 --- /dev/null +++ b/design/내부 시퀀스 설계서 - Intelligence Service.txt @@ -0,0 +1,257 @@ +!theme mono + +skinparam sequenceArrowThickness 2 +skinparam sequenceParticipantBorderThickness 2 +skinparam sequenceActorBorderThickness 2 +skinparam sequenceGroupBorderThickness 2 + +title Intelligence Service 내부 시퀀스 다이어그램 (역설계 - Claude AI 기반) + +participant "AnalysisController" as AnalysisCtrl +participant "ChatController" as ChatCtrl +participant "NotificationController" as NotificationCtrl +participant "BatchController" as BatchCtrl +participant "HealthAnalysisUseCase" as HealthAnalysisUC +participant "ChatUseCase" as ChatUC +participant "NotificationUseCase" as NotificationUC +participant "BatchUseCase" as BatchUC +participant "AiAnalysisDomainService" as AiAnalysisDomainSvc +participant "ChatDomainService" as ChatDomainSvc +participant "NotificationDomainService" as NotificationDomainSvc +participant "HealthServiceAdapter" as HealthAdapter +participant "UserServiceAdapter" as UserAdapter +participant "GoalServiceAdapter" as GoalAdapter +participant "ClaudeApiAdapter" as ClaudeAdapter +participant "CacheAdapter" as CacheAdapter +participant "ChatHistoryRepository" as ChatRepo +participant "EventPublisherAdapter" as EventAdapter +participant "PostgreSQL" as PostgreSQL +participant "Redis Cache" as Redis +participant "Claude API" as ClaudeAPI +participant "Azure Service Bus" as ServiceBus + +== 1. GET /api/intelligence/health/diagnosis (AI 3줄 요약 진단) == + +AnalysisCtrl -> HealthAnalysisUC: generateHealthDiagnosis(userId) + +HealthAnalysisUC -> CacheAdapter: getCachedDiagnosis(userId) +CacheAdapter -> Redis: GET diagnosis:{userId} +Redis -> CacheAdapter: 캐시된 진단 또는 null + +alt 캐시 미스인 경우 + HealthAnalysisUC -> HealthAdapter: getLatestHealthCheckup(userId) + HealthAdapter -> HealthAnalysisUC: 최신 건강검진 데이터 (정상치 비교 결과 포함) + + HealthAnalysisUC -> UserAdapter: getUserProfile(userId) + UserAdapter -> HealthAnalysisUC: 사용자 프로필 (직업, 나이 등) + + HealthAnalysisUC -> AiAnalysisDomainSvc: createDiagnosisPrompt(healthData, userProfile) + AiAnalysisDomainSvc -> AiAnalysisDomainSvc: formatHealthDataForAI(checkupData) + note right: **AI 프롬프트 생성**\n- 건강검진 수치 및 정상치 비교 결과\n- 사용자 직업군 특성 반영\n- 3줄 요약 요청 형식 + + AiAnalysisDomainSvc -> ClaudeAdapter: requestHealthDiagnosis(prompt) + ClaudeAdapter -> ClaudeAPI: Claude AI 건강 진단 요청 + ClaudeAPI -> ClaudeAdapter: AI 생성 3줄 요약 진단 + + AiAnalysisDomainSvc -> AiAnalysisDomainSvc: parseAndValidateAIResponse(aiResponse) + AiAnalysisDomainSvc -> AiAnalysisDomainSvc: calculateConfidenceScore(healthData, aiResponse) + + HealthAnalysisUC -> CacheAdapter: cacheDiagnosis(userId, diagnosisResult) + CacheAdapter -> Redis: SETEX diagnosis:{userId} 1800 {data} + note right: 30분 TTL로 캐싱 +end + +HealthAnalysisUC -> AnalysisCtrl: HealthDiagnosisResponse 반환 +note right: {threeSentenceSummary, healthScore, riskLevel, occupationConsiderations, analysisTimestamp, confidenceScore} + +== 2. POST /api/intelligence/missions/recommend (AI 미션 추천) == + +AnalysisCtrl -> HealthAnalysisUC: recommendMissions(missionRecommendationRequest) +note right: {userId, currentHealthStatus, preferences} + +HealthAnalysisUC -> HealthAdapter: getHealthAnalysisData(userId) +HealthAdapter -> HealthAnalysisUC: 건강 상태 및 위험 요인 + +HealthAnalysisUC -> UserAdapter: getUserOccupationInfo(userId) +UserAdapter -> HealthAnalysisUC: 직업군 정보 + +HealthAnalysisUC -> AiAnalysisDomainSvc: generateMissionRecommendations(healthData, occupation, preferences) +AiAnalysisDomainSvc -> AiAnalysisDomainSvc: createPersonalizedPrompt(data) +note right: **개인화 미션 추천 프롬프트**\n- 건강 위험 요인 기반\n- 직업군별 특성 고려\n- 사용자 선호도 반영 + +AiAnalysisDomainSvc -> ClaudeAdapter: requestMissionRecommendations(prompt) +ClaudeAdapter -> ClaudeAPI: Claude AI 미션 추천 요청 +ClaudeAPI -> ClaudeAdapter: AI 생성 개인화 미션 목록 + +AiAnalysisDomainSvc -> AiAnalysisDomainSvc: parseMissionRecommendations(aiResponse) +AiAnalysisDomainSvc -> AiAnalysisDomainSvc: validateAndEnrichMissions(missions) + +HealthAnalysisUC -> AnalysisCtrl: MissionRecommendationResponse 반환 +note right: {missions, recommendationReason, totalRecommended} + +== 3. POST /api/intelligence/chat/consultation (AI 채팅 상담) == + +ChatCtrl -> ChatUC: processChat(chatRequest) +note right: {message, sessionId, context} + +ChatUC -> ChatDomainService: validateChatSession(sessionId) +ChatDomainService -> ChatRepo: findChatSession(sessionId) +ChatRepo -> PostgreSQL: SELECT * FROM chat_sessions WHERE session_id = ? + +alt 새 세션인 경우 + ChatDomainService -> ChatDomainService: createNewChatSession(userId) + ChatDomainService -> ChatRepo: saveChatSession(newSession) + ChatRepo -> PostgreSQL: INSERT INTO chat_sessions +end + +ChatUC -> ChatRepo: getChatHistory(sessionId, limit=10) +ChatRepo -> Redis: LRANGE chat:history:{sessionId} 0 9 +Redis -> ChatRepo: 최근 채팅 히스토리 + +alt 캐시 미스인 경우 + ChatRepo -> PostgreSQL: SELECT * FROM chat_messages WHERE session_id = ? ORDER BY message_order DESC LIMIT 10 + PostgreSQL -> ChatRepo: DB 채팅 히스토리 + ChatRepo -> Redis: LPUSH chat:history:{sessionId} {messages} +end + +ChatUC -> ChatDomainService: buildContextualPrompt(userMessage, chatHistory, context) +ChatDomainService -> ChatDomainService: formatChatContext(history) +note right: **채팅 컨텍스트 구성**\n- 이전 대화 맥락 유지\n- 건강 상담 톤앤매너 적용\n- 사용자 메시지 전처리 + +ChatDomainService -> ClaudeAdapter: requestChatResponse(contextualPrompt) +ClaudeAdapter -> ClaudeAPI: Claude AI 채팅 응답 요청 +ClaudeAPI -> ClaudeAdapter: AI 생성 채팅 응답 + +ChatUC -> ChatDomainService: saveChatExchange(sessionId, userMessage, aiResponse) +ChatDomainService -> ChatRepo: saveChatMessages(sessionId, messages) +ChatRepo -> PostgreSQL: INSERT INTO chat_messages (batch) +ChatRepo -> Redis: LPUSH chat:history:{sessionId} {newMessages} + +ChatUC -> ChatCtrl: ChatResponse 반환 +note right: {response, sessionId, timestamp, suggestedQuestions, responseType} + +== 4. GET /api/intelligence/chat/history (채팅 히스토리 조회) == + +ChatCtrl -> ChatUC: getChatHistory(sessionId, messageLimit) + +ChatUC -> CacheAdapter: getCachedChatHistory(sessionId) +CacheAdapter -> Redis: LRANGE chat:history:{sessionId} 0 {limit-1} +Redis -> CacheAdapter: 캐시된 채팅 히스토리 + +alt 캐시 미스인 경우 + ChatUC -> ChatRepo: findChatHistoryBySession(sessionId, messageLimit) + ChatRepo -> PostgreSQL: SELECT * FROM chat_messages WHERE session_id = ? ORDER BY message_order DESC LIMIT ? + PostgreSQL -> ChatRepo: DB 채팅 히스토리 + + ChatUC -> CacheAdapter: cacheChatHistory(sessionId, history) + CacheAdapter -> Redis: LPUSH chat:history:{sessionId} {messages} +end + +ChatUC -> ChatDomainService: formatChatHistory(rawHistory) +ChatDomainService -> ChatDomainService: maskSensitiveInfo(messages) +note right: 민감정보 마스킹 처리 + +ChatUC -> ChatCtrl: ChatHistoryResponse 반환 +note right: {sessionId, messages, totalMessageCount, cacheExpiration} + +== 5. POST /api/intelligence/notifications/celebration (미션 달성 축하) == + +NotificationCtrl -> NotificationUC: generateCelebrationMessage(celebrationRequest) +note right: {userId, missionId, achievementType, consecutiveDays, totalAchievements} + +NotificationUC -> GoalAdapter: getMissionDetails(missionId) +GoalAdapter -> NotificationUC: 미션 정보 및 사용자 진행 상황 + +NotificationUC -> NotificationDomainService: createCelebrationMessage(celebrationData) +NotificationDomainService -> NotificationDomainService: prepareCelebrationPrompt(data) +note right: **축하 메시지 프롬프트**\n- 달성 미션 정보\n- 연속 달성 일수\n- 동기부여 톤앤매너 + +NotificationDomainService -> ClaudeAdapter: requestCelebrationMessage(prompt) +ClaudeAdapter -> ClaudeAPI: AI 축하 메시지 요청 +ClaudeAPI -> ClaudeAdapter: AI 생성 축하 메시지 + +NotificationDomainService -> NotificationDomainService: enhanceCelebrationMessage(aiMessage, achievementData) +note right: **축하 메시지 강화**\n- 달성 배지 정보 추가\n- 건강 효과 설명\n- 다음 마일스톤 안내 + +NotificationUC -> NotificationCtrl: CelebrationResponse 반환 +note right: {congratsMessage, achievementBadge, healthBenefit, nextMilestone, encouragementLevel, visualEffect} + +== 6. POST /api/intelligence/notifications/encouragement (독려 메시지) == + +NotificationCtrl -> NotificationUC: generateEncouragementMessage(encouragementRequest) +note right: {userId, missionsStatus} + +NotificationUC -> GoalAdapter: getUserDailyProgress(userId) +GoalAdapter -> NotificationUC: 일일 진행 상황 + +NotificationUC -> NotificationDomainService: analyzeProgressPattern(userId, missionsStatus, dailyProgress) +NotificationDomainService -> NotificationDomainService: calculateProgressLevel(data) +NotificationDomainService -> NotificationDomainService: identifyFailurePoints(missionsStatus) + +NotificationUC -> CacheAdapter: getCachedEncouragementMessage(userId, progressLevel) +CacheAdapter -> Redis: GET encouragement:{userId}:{progressLevel} +Redis -> CacheAdapter: 캐시된 독려 메시지 또는 null + +alt 캐시 미스인 경우 + NotificationDomainService -> NotificationDomainService: createEncouragementPrompt(progressAnalysis) + NotificationDomainService -> ClaudeAdapter: requestEncouragementMessage(prompt) + ClaudeAdapter -> ClaudeAPI: AI 독려 메시지 요청 + ClaudeAPI -> ClaudeAdapter: AI 생성 독려 메시지 + + NotificationUC -> CacheAdapter: cacheEncouragementMessage(userId, progressLevel, message) + CacheAdapter -> Redis: SETEX encouragement:{userId}:{progressLevel} 1800 {message} +end + +NotificationUC -> NotificationCtrl: EncouragementResponse 반환 +note right: {message, motivationType, timing, personalizedTip, priority} + +== 7. POST /api/intelligence/batch/notifications (배치 알림 처리) == + +BatchCtrl -> BatchUC: processBatchNotifications(batchRequest) +note right: {triggerTime, targetUsers, notificationType} + +BatchUC -> GoalAdapter: getAllUsersProgress(targetUsers) +GoalAdapter -> BatchUC: 모든 대상 사용자의 진행 상황 + +loop 사용자별 처리 + BatchUC -> NotificationDomainService: generatePersonalizedNotification(user, progress) + NotificationDomainService -> ClaudeAdapter: requestBatchNotification(personalizedPrompt) + ClaudeAdapter -> ClaudeAPI: AI 개인화 알림 요청 + ClaudeAPI -> ClaudeAdapter: AI 생성 개인화 알림 +end + +BatchUC -> EventAdapter: publishBatchNotificationEvents(processedNotifications) +EventAdapter -> ServiceBus: 배치 알림 이벤트 발행 + +BatchUC -> BatchCtrl: BatchNotificationResponse 반환 +note right: {processedCount, successCount, failedCount, nextScheduledTime} + +== 예외 처리 == + +alt Claude API 호출 실패 + ClaudeAdapter -> AiAnalysisDomainSvc: ClaudeApiException + AiAnalysisDomainSvc -> HealthAnalysisUC: 기본 메시지 사용 + note right: Fallback 메시지로 대체 +end + +alt 캐시 접근 실패 + CacheAdapter -> ChatUC: CacheAccessException + ChatUC -> ChatRepo: DB에서 직접 조회 +end + +alt 세션 만료 + ChatDomainService -> ChatUC: SessionExpiredException + ChatUC -> ChatCtrl: 새 세션 생성 안내 +end + +== 성능 최적화 및 모니터링 == + +note over NotificationUC, CacheAdapter +**Claude AI 연동 최적화** +- 응답 캐싱으로 API 호출 최소화 +- Circuit Breaker 패턴 적용 +- Fallback 메시지 준비 +- 배치 처리 시 우선순위 기반 +- API 응답 시간 모니터링 +- 캐시 히트율 추적 +end note \ No newline at end of file diff --git a/design/내부 시퀀스 설계서 - User Service.txt b/design/내부 시퀀스 설계서 - User Service.txt new file mode 100644 index 0000000..340eb1a --- /dev/null +++ b/design/내부 시퀀스 설계서 - User Service.txt @@ -0,0 +1,177 @@ +!theme mono + +skinparam sequenceArrowThickness 2 +skinparam sequenceParticipantBorderThickness 2 +skinparam sequenceActorBorderThickness 2 +skinparam sequenceGroupBorderThickness 2 + +title User Service 내부 시퀀스 다이어그램 (역설계 - 개발 소스 기반) + +participant "AuthController" as AuthCtrl +participant "UserController" as UserCtrl +participant "AuthUseCase" as AuthUC +participant "UserUseCase" as UserUC +participant "AuthDomainService" as AuthDomainSvc +participant "UserDomainService" as UserDomainSvc +participant "UserRepository" as UserRepo +participant "OccupationRepository" as OccupationRepo +participant "GoogleAuthAdapter" as AuthACL +participant "JwtTokenAdapter" as JwtAdapter +participant "EventPublisherAdapter" as EventPub +participant "PostgreSQL" as DB +participant "Google SSO" as GoogleSSO +participant "Azure Service Bus" as ServiceBus + +== 1. POST /api/users/auth/google-login (구글 로그인) == + +AuthCtrl -> AuthUC: authenticateWithGoogle(googleLoginRequest) +note right: {googleAccessToken, googleIdToken} + +AuthUC -> AuthDomainSvc: validateGoogleTokens(accessToken, idToken) +AuthDomainSvc -> AuthACL: verifyGoogleToken(accessToken, idToken) +AuthACL -> GoogleSSO: Google Token Verification API +GoogleSSO -> AuthACL: Verified User Info +AuthACL -> AuthDomainSvc: GoogleUserInfo 응답 + +AuthDomainSvc -> UserRepo: findByGoogleId(googleId) +UserRepo -> DB: SELECT * FROM users WHERE google_id = ? +DB -> UserRepo: User 엔티티 또는 null + +alt 신규 사용자인 경우 + AuthDomainSvc -> AuthDomainSvc: createNewUser(googleUserInfo) + AuthDomainSvc -> UserRepo: saveUser(newUser) + UserRepo -> DB: INSERT INTO users + DB -> UserRepo: 저장 완료 + + AuthUC -> EventPub: publishUserRegisteredEvent(user) + EventPub -> ServiceBus: 신규 사용자 등록 이벤트 발행 +end + +AuthUC -> JwtAdapter: generateTokens(user) +JwtAdapter -> AuthUC: {accessToken, refreshToken} + +AuthUC -> UserRepo: updateLastLoginAt(user.id) +UserRepo -> DB: UPDATE users SET last_login_at = NOW() +DB -> UserRepo: 업데이트 완료 + +AuthUC -> AuthCtrl: LoginResponse 반환 +note right: {accessToken, refreshToken, userId, isNewUser, message} + +== 2. POST /api/users/profile/complete (프로필 완료) == + +AuthCtrl -> UserUC: completeUserProfile(userRegistrationRequest) +note right: JWT에서 추출한 userId + {name, birthDate, occupation} + +UserUC -> UserDomainSvc: validateProfileData(profileData) +UserDomainSvc -> OccupationRepo: validateOccupationCode(occupation) +OccupationRepo -> DB: SELECT * FROM occupation_types WHERE occupation_code = ? +DB -> OccupationRepo: Occupation 엔티티 + +UserDomainSvc -> UserRepo: findById(userId) +UserRepo -> DB: SELECT * FROM users WHERE id = ? +DB -> UserRepo: User 엔티티 + +UserDomainSvc -> UserDomainSvc: updateUserProfile(user, profileData) +UserDomainSvc -> UserRepo: updateUser(updatedUser) +UserRepo -> DB: UPDATE users SET name = ?, birth_date = ?, occupation = ?, updated_at = NOW() +DB -> UserRepo: 업데이트 완료 + +UserUC -> EventPub: publishUserProfileUpdatedEvent(user) +EventPub -> ServiceBus: 회원정보 업데이트 이벤트 발행 + +UserUC -> AuthCtrl: UserRegistrationResponse 반환 +note right: {userId, message, status, profileCompletedAt} + +== 3. GET /api/users/profile (사용자 기본 정보 조회) == + +AuthCtrl -> UserUC: getUserProfile(userId) +note right: JWT에서 추출한 userId + +UserUC -> UserRepo: findById(userId) +UserRepo -> DB: SELECT * FROM users WHERE id = ? +DB -> UserRepo: User 엔티티 + +UserUC -> UserDomainSvc: calculateAge(user.birthDate) +note right: 생년월일로부터 나이 계산 + +UserUC -> OccupationRepo: findOccupationName(user.occupation) +OccupationRepo -> DB: SELECT occupation_name FROM occupation_types WHERE occupation_code = ? +DB -> OccupationRepo: 직업명 + +UserUC -> AuthCtrl: UserProfileResponse 반환 +note right: {userId, name, age, occupation, registeredAt, lastLoginAt} + +== 4. GET /api/users/occupations (직업 목록 조회) == + +UserCtrl -> UserUC: getAllOccupations() + +UserUC -> OccupationRepo: findAllOccupations() +OccupationRepo -> DB: SELECT * FROM occupation_types ORDER BY category, occupation_name +DB -> OccupationRepo: Occupation 목록 + +UserUC -> UserCtrl: OccupationListResponse 반환 +note right: {occupations: [{occupationCode, occupationName, category}], totalCount} + +== 5. GET /api/users/occupations/name (직업명 조회) == + +UserCtrl -> UserUC: getOccupationName(occupationCode) + +UserUC -> OccupationRepo: findOccupationByCode(occupationCode) +OccupationRepo -> DB: SELECT * FROM occupation_types WHERE occupation_code = ? +DB -> OccupationRepo: Occupation 엔티티 + +UserUC -> UserCtrl: OccupationNameResponse 반환 +note right: {occupationCode, occupationName} + +== 6. GET /api/users/occupations/code (직업코드 조회) == + +UserCtrl -> UserUC: getOccupationCode(occupationName) + +UserUC -> OccupationRepo: findOccupationByName(occupationName) +OccupationRepo -> DB: SELECT * FROM occupation_types WHERE occupation_name = ? +DB -> OccupationRepo: Occupation 엔티티 + +UserUC -> UserCtrl: OccupationCodeResponse 반환 +note right: {occupationCode, occupationName} + +== 예외 처리 == + +alt 인증 실패 시 + AuthACL -> AuthUC: AuthenticationException + AuthUC -> AuthCtrl: 401 Unauthorized 응답 +end + +alt 사용자 정보 없음 + UserRepo -> UserUC: UserNotFoundException + UserUC -> AuthCtrl: 404 Not Found 응답 +end + +alt 입력 데이터 검증 실패 + UserDomainSvc -> UserUC: ValidationException + UserUC -> AuthCtrl: 400 Bad Request 응답 + note right: 유효성 검증 오류 메시지 포함 +end + +alt 직업 코드 없음 + OccupationRepo -> UserUC: OccupationNotFoundException + UserUC -> UserCtrl: 404 Not Found 응답 +end + +== 트랜잭션 처리 == + +note over UserUC, UserRepo +**트랜잭션 범위** +- 사용자 생성/수정 작업 +- 이벤트 발행은 트랜잭션 커밋 후 +- @Transactional 어노테이션 적용 +end note + +== 보안 처리 == + +note over AuthUC, AuthACL +**보안 고려사항** +- JWT 토큰 생성 시 적절한 만료시간 설정 +- Google SSO 응답 데이터 검증 +- 개인정보 로깅 제외 +- 비밀번호 없이 OAuth만 사용 +end note \ No newline at end of file diff --git a/design/논리 아키텍처.txt b/design/논리 아키텍처.txt new file mode 100644 index 0000000..8d01203 --- /dev/null +++ b/design/논리 아키텍처.txt @@ -0,0 +1,172 @@ +!theme mono + +skinparam componentStyle rectangle +skinparam componentFontSize 12 +skinparam arrowFontSize 11 +skinparam arrowThickness 2 + +title HealthSync 역설계 논리 아키텍처 (개발 소스 기반) + +' 클라이언트 (Backends for Frontends 패턴) +package "클라이언트 계층" #lightcyan { + [React 모바일 웹앱] as MobileApp +} + +' API Gateway & Service Mesh (Gateway Routing 패턴) +package "Gateway 계층" #lightyellow { + [API Gateway\n(Spring Cloud Gateway)] as APIGateway + [Service Mesh (Istio)] as ServiceMesh +} + +' Clean Architecture 기반 마이크로서비스 +package "사용자 컨텍스트" #lightgreen { + [User Service\n(Clean Architecture)] as UserService + note right of UserService + **주요 기능** + • Google SSO 인증 + • 사용자 프로필 관리 + • 직업군별 코드 변환 + • JWT 토큰 생성/검증 + end note +} + +package "건강 컨텍스트" #lightgreen { + [Health Service\n(Clean Architecture)] as HealthService + note right of HealthService + **주요 기능** + • 건강검진 데이터 연동 + • 정상치 기준 비교 분석 + • Azure Blob 파일 업로드 + • 건강 위험도 계산 + • 캐싱 기반 성능 최적화 + end note +} + +package "목표 컨텍스트" #lightgreen { + [Goal Service\n(Clean Architecture)] as GoalService + note right of GoalService + **주요 기능** + • 미션 선택/관리 + • 목표 달성 추적 + • 이력 분석 + • 진행률 계산 + end note +} + +package "지능형서비스 컨텍스트" #lightgreen { + [Intelligence Service\n(Clean Architecture)] as IntelligenceService + note right of IntelligenceService + **주요 기능** + • Claude AI 기반 건강 진단 + • 개인화 미션 추천 + • 챗봇 상담 + • 독려 메시지 생성 + • 배치 알림 처리 + end note +} + +' 통합 데이터 저장소 +package "데이터 저장소" #lightpink { + [PostgreSQL\n(통합 DB)] as PostgreSQL + [Redis Cache] as Redis + [Azure Blob Storage\n(건강검진 파일)] as BlobStorage + + note right of PostgreSQL + **통합 스키마** + • User 테이블 + • Health 관련 테이블 + • Goal 관련 테이블 + • Normal Range 테이블 (신규) + • Chat History 테이블 + end note +} + +' 메시징 인프라 (Event-Driven Architecture) +package "메시징 인프라" #lightsalmon { + [Azure Service Bus\n(Event Store)] as EventStore + [Command Queue] as CommandQueue +} + +' 외부 시스템 연동 (Anti-Corruption Layer 패턴) +package "외부 시스템" #lightblue { + [Google SSO] as GoogleSSO + [Claude API] as ClaudeAPI +} + +package "보호 계층" #orange { + [Auth ACL] as AuthACL + [AI Service ACL] as AIACL +} + +' === 핵심 처리 흐름 (개발 소스 기반) === + +' 1. 클라이언트 요청 +MobileApp -[#blue,thickness=3]-> APIGateway : "1. RESTful API 요청" + +' 2. Gateway Routing +APIGateway -[#green,thickness=2]-> ServiceMesh : "2. 서비스 라우팅 & 로드밸런싱" + +' 3. Clean Architecture 기반 서비스 호출 +ServiceMesh -[#purple,thickness=2]-> UserService : "3a. /api/users/* - 인증 & 프로필" +ServiceMesh -[#purple,thickness=2]-> HealthService : "3b. /api/health/* - 건강데이터" +ServiceMesh -[#purple,thickness=2]-> GoalService : "3c. /api/goals/* - 목표관리" +ServiceMesh -[#purple,thickness=2]-> IntelligenceService : "3d. /api/intelligence/* - AI 분석" + +' 4. 통합 데이터 저장 (PostgreSQL) +UserService -[#red,thickness=2]-> PostgreSQL : "4a. 사용자 & 직업코드 데이터" +HealthService -[#red,thickness=2]-> PostgreSQL : "4b. 건강검진 & 정상치 데이터" +GoalService -[#red,thickness=2]-> PostgreSQL : "4c. 목표 & 미션 데이터" +IntelligenceService -[#red,thickness=2]-> PostgreSQL : "4d. 채팅 & 분석 이력" + +' 5. Azure Blob Storage 연동 +HealthService -[#brown,thickness=2]-> BlobStorage : "5. 건강검진 파일 저장" + +' 6. Redis 캐싱 (Cache-Aside 패턴) +HealthService -[#orange,thickness=2]-> Redis : "6a. 건강이력 캐싱 (1시간)" +IntelligenceService -[#orange,thickness=2]-> Redis : "6b. AI 분석결과 캐싱 (30분)" +GoalService -[#orange,thickness=2]-> Redis : "6c. 활성미션 캐싱 (30분)" + +' 7. 외부 시스템 연동 (ACL 패턴) +UserService -[#navy,thickness=2]-> AuthACL : "7a. Google 인증 요청" +AuthACL -[#navy,thickness=2]-> GoogleSSO : "7b. OAuth 2.0 인증" +IntelligenceService -[#navy,thickness=2]-> AIACL : "7c. Claude AI 요청" +AIACL -[#navy,thickness=2]-> ClaudeAPI : "7d. AI 분석/채팅 API" + +' 8. 이벤트 기반 통신 +UserService -[#magenta,thickness=2,dashed]-> EventStore : "8a. 사용자 등록/수정 이벤트" +HealthService -[#magenta,thickness=2,dashed]-> EventStore : "8b. 건강데이터 동기화 이벤트" +GoalService -[#magenta,thickness=2,dashed]-> EventStore : "8c. 목표달성 이벤트" + +' 9. Command/Query 분리 +EventStore -[#cyan,thickness=2,dotted]-> CommandQueue : "9a. 배치 명령 큐" +CommandQueue -[#cyan,thickness=2,dotted]-> IntelligenceService : "9b. 주기적 알림 처리" + +' === 패턴 적용 노트 === +note right of APIGateway + **Gateway 패턴** + • JWT 토큰 검증 + • Rate Limiting + • CORS 처리 + • 로드밸런싱 +end note + +note right of PostgreSQL + **통합 DB 패턴** + • 마이크로서비스별 스키마 분리 + • 트랜잭션 일관성 + • 정상치 기준 중앙 관리 +end note + +note right of Redis + **캐싱 패턴** + • Cache-Aside + • TTL 기반 만료 + • 성능 최적화 +end note + +note right of EventStore + **이벤트 패턴** + • 비동기 통신 + • 서비스 간 결합도 최소화 + • 확장성 확보 +end note \ No newline at end of file diff --git a/design/데이터설계서 - Goal Service.txt b/design/데이터설계서 - Goal Service.txt new file mode 100644 index 0000000..768efff --- /dev/null +++ b/design/데이터설계서 - Goal Service.txt @@ -0,0 +1,291 @@ +!theme mono + +skinparam classAttributeIconSize 0 +skinparam classFontSize 12 +skinparam classAttributeFontSize 11 + +title HealthSync 역설계 - Goal Service 데이터 설계서 + +package "Goal Service Database Schema" as goal_db #lightcoral { + + entity "user_mission_goals" as user_mission_goal { + * goal_id : VARCHAR(50) <> + -- + * user_id : VARCHAR(50) + * is_active : BOOLEAN + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + -- + + 인덱스: idx_user_id_active + + 인덱스: idx_created_at + } + + entity "user_missions" as user_mission { + * id : BIGINT <> + -- + * goal_id : VARCHAR(50) <> + * user_id : VARCHAR(50) + * mission_id : VARCHAR(50) + * mission_title : VARCHAR(200) + * mission_description : TEXT + * category : VARCHAR(50) + * difficulty : VARCHAR(20) + * estimated_time_minutes : INTEGER + * is_active : BOOLEAN + * current_streak_days : INTEGER + * total_completed_count : INTEGER + * start_date : DATE + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + -- + + 인덱스: idx_goal_id + + 인덱스: idx_user_id_active + + 인덱스: idx_mission_id + + 인덱스: idx_category + + 인덱스: idx_start_date + } + + entity "mission_progress" as mission_progress { + * id : BIGINT <> + -- + * user_id : VARCHAR(50) + * mission_id : VARCHAR(50) <> + * completed_at : TIMESTAMP + * notes : TEXT + * earned_points : INTEGER + * consecutive_days : INTEGER + * created_at : TIMESTAMP + -- + + 인덱스: idx_user_mission_date + + 인덱스: idx_completed_at + + 인덱스: idx_mission_id + + 인덱스: idx_earned_points + } + + entity "mission_completion_history" as mission_completion_history { + * id : BIGINT <> + -- + * user_id : VARCHAR(50) + * mission_id : VARCHAR(50) + * goal_id : VARCHAR(50) + * completion_date : DATE + * completion_time : TIME + * completion_status : VARCHAR(20) + * notes : TEXT + * earned_points : INTEGER + * streak_days_at_completion : INTEGER + * created_at : TIMESTAMP + -- + + 인덱스: idx_user_id_date + + 인덱스: idx_mission_completion + + 인덱스: idx_goal_id_date + } + + note right of user_mission_goal + **사용자 목표 관리** + • 사용자별 목표 설정 단위 + • 한 번에 하나의 활성 목표만 가능 + • 목표 재설정 시 기존 목표 비활성화 + • goal_id는 UUID 형태로 생성 + end note + + note right of user_mission + **사용자별 미션 정보** + • 목표에 포함된 개별 미션들 + • Intelligence Service에서 추천받은 미션 + • 최대 5개 미션까지 선택 가능 + • 난이도별 포인트 차등 적용 + • 연속 달성 일수 추적 + end note + + note right of mission_progress + **미션 수행 기록** + • 일별 미션 완료 기록 + • 같은 날 중복 완료 방지 + • 연속 달성 일수 계산 기준 + • 포인트 적립 내역 + end note + + note right of mission_completion_history + **미션 완료 이력 (분석용)** + • 장기간 통계 분석용 테이블 + • 사용자별 달성 패턴 분석 + • 미션별 효과성 분석 + • 시간대별 완료 패턴 추적 + end note +} + +package "관계 정의" as relationships { + user_mission_goal ||--o{ user_mission : goal_id + user_mission ||--o{ mission_progress : mission_id + user_mission ||--o{ mission_completion_history : mission_id + user_mission_goal ||--o{ mission_completion_history : goal_id + + note as n1 + **외래키 관계** + user_missions.goal_id → user_mission_goals.goal_id + mission_progress.mission_id → user_missions.mission_id + mission_completion_history.mission_id → user_missions.mission_id + mission_completion_history.goal_id → user_mission_goals.goal_id + + **참조 무결성** + • CASCADE 업데이트 + • RESTRICT 삭제 (이력 보존) + + **논리적 관계** + • User Service users ↔ user_mission_goals (사용자별 목표) + • Intelligence Service ↔ user_missions (미션 추천) + • 한 사용자는 하나의 활성 목표만 보유 + • 하나의 목표는 여러 미션을 포함 + end note +} + +package "데이터 타입 및 제약조건" as constraints { + note as n2 + **user_mission_goals 제약조건** + • goal_id: UUID 형태, PRIMARY KEY + • user_id: User Service와 일치, NOT NULL + • is_active: DEFAULT TRUE + • 사용자당 활성 목표는 1개만 (UNIQUE 제약) + + **user_missions 제약조건** + • id: AUTO_INCREMENT + • goal_id: NOT NULL, 존재하는 goal_id + • mission_id: Intelligence Service에서 제공 + • mission_title: 최대 200자, NOT NULL + • category: 'EXERCISE', 'NUTRITION', 'STRESS_MANAGEMENT', 'SLEEP', 'PREVENTIVE_CARE' + • difficulty: 'EASY', 'MEDIUM', 'HARD' + • estimated_time_minutes: 1~180분 범위 + • current_streak_days: 0 이상 + • total_completed_count: 0 이상 + • 목표당 최대 5개 미션 (체크 제약) + + **mission_progress 제약조건** + • id: AUTO_INCREMENT + • user_id: NOT NULL + • mission_id: 존재하는 mission_id + • completed_at: NOT NULL + • earned_points: 0 이상 + • consecutive_days: 1 이상 + • 같은 날 같은 미션 중복 완료 방지 (UNIQUE 제약) + + **mission_completion_history 제약조건** + • completion_status: 'COMPLETED', 'PARTIAL', 'SKIPPED' + • earned_points: 0 이상 + • streak_days_at_completion: 0 이상 + • completion_time: 00:00~23:59 + end note +} + +package "미션 카테고리 및 난이도" as mission_categories { + note as n3 + **미션 카테고리별 예시** + + **EXERCISE (운동)** + • 계단 오르기 10분 + • 산책 30분 + • 스트레칭 15분 + • 홈트레이닝 20분 + + **NUTRITION (영양)** + • 물 8잔 마시기 + • 금연하기 + • 금주하기 + • 건강한 간식 선택 + + **STRESS_MANAGEMENT (스트레스 관리)** + • 명상 10분 + • 심호흡 연습 + • 일기 쓰기 + • 취미 시간 갖기 + + **SLEEP (수면)** + • 규칙적인 취침시간 + • 11시 전 잠들기 + • 수면 8시간 유지 + • 카페인 섭취 줄이기 + + **PREVENTIVE_CARE (예방관리)** + • 혈압 측정하기 + • 체중 기록하기 + • 건강검진 예약 + • 병원 방문하기 + + **난이도별 포인트** + • EASY: 10~20 포인트 + • MEDIUM: 25~40 포인트 + • HARD: 50~80 포인트 + • 연속 달성 보너스: +5 포인트/일 + end note +} + +package "성능 및 운영 고려사항" as performance { + note as n4 + **인덱스 전략** + • user_mission_goals: (user_id, is_active) 복합 인덱스 + • user_missions: (user_id, is_active) 복합 인덱스 + • mission_progress: (user_id, mission_id, DATE(completed_at)) 복합 인덱스 + • mission_completion_history: (user_id, completion_date) 복합 인덱스 + + **파티셔닝** + • mission_progress: completed_at 기준 월별 파티셔닝 + • mission_completion_history: completion_date 기준 월별 파티셔닝 + + **아카이빙** + • 1년 이상 된 mission_progress 데이터 아카이빙 + • 비활성 목표 데이터는 history 테이블로 이관 + + **캐싱 전략** + • 활성 미션 목록: Redis 캐싱 (30분) + • 오늘 완료한 미션: Redis 캐싱 (실시간) + • 연속 달성 일수: Redis 캐싱 (6시간) + • 미션별 통계: Redis 캐싱 (1시간) + + **배치 처리** + • 매일 자정에 연속 달성 일수 재계산 + • 주간/월간 달성률 통계 생성 + • 장기 미완료 미션 알림 발송 + + **모니터링 지표** + • 일일 미션 완료율 + • 평균 연속 달성 일수 + • 카테고리별 인기도 + • 사용자별 참여율 + • 목표 재설정 빈도 + end note +} + +package "데이터 분석 및 인사이트" as analytics { + note as n5 + **사용자 행동 분석** + • 미션 완료 시간대 패턴 + • 요일별 달성률 차이 + • 연속 달성 중단 요인 분석 + • 난이도별 지속 가능성 + + **미션 효과성 분석** + • 카테고리별 건강 개선 효과 + • 직업군별 선호 미션 유형 + • 연령대별 적합한 미션 난이도 + • 계절별 미션 선호도 변화 + + **개인화 추천 개선** + • 과거 달성 패턴 기반 추천 + • 유사 사용자 그룹 분석 + • 실패 미션 유형 회피 + • 성공 확률 높은 미션 우선 추천 + + **동기부여 전략** + • 포인트 시스템 효과성 + • 연속 달성 보상 최적화 + • 사회적 비교 효과 분석 + • 축하 메시지 반응 측정 + + **KPI 지표** + • 월간 활성 사용자 수 (MAU) + • 평균 목표 지속 기간 + • 미션 완료율 (전체/개인별) + • 사용자 리텐션율 + • 건강 개선 지표 상관관계 + end note +} \ No newline at end of file diff --git a/design/데이터설계서 - Health Service.txt b/design/데이터설계서 - Health Service.txt new file mode 100644 index 0000000..52cf3e6 --- /dev/null +++ b/design/데이터설계서 - Health Service.txt @@ -0,0 +1,303 @@ +!theme mono + +skinparam classAttributeIconSize 0 +skinparam classFontSize 12 +skinparam classAttributeFontSize 11 + +title HealthSync 역설계 - Health Service 데이터 설계서 + +package "Health Service Database Schema" as health_db #lightblue { + + entity "health_checkups" as health_checkup { + * id : BIGINT <> + -- + * user_id : VARCHAR(50) + * member_serial_number : BIGINT + * raw_id : BIGINT <> + * reference_year : INTEGER + * height_cm : DECIMAL(5,2) + * weight_kg : DECIMAL(5,2) + * waist_cm : DECIMAL(5,2) + * bmi : DECIMAL(4,2) + * visual_acuity_left : DECIMAL(3,1) + * visual_acuity_right : DECIMAL(3,1) + * hearing_avg : DECIMAL(4,1) + * systolic_bp : INTEGER + * diastolic_bp : INTEGER + * fasting_glucose : INTEGER + * total_cholesterol : INTEGER + * triglyceride : INTEGER + * hdl_cholesterol : INTEGER + * ldl_cholesterol : INTEGER + * hemoglobin : DECIMAL(4,1) + * urine_protein : INTEGER + * serum_creatinine : DECIMAL(4,1) + * ast : INTEGER + * alt : INTEGER + * gamma_gtp : INTEGER + * smoking_status : INTEGER + * drinking_status : INTEGER + * risk_level : VARCHAR(20) + * abnormal_indicators : JSON + * health_score : INTEGER + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + -- + + 인덱스: idx_user_id_year + + 인덱스: idx_member_serial_number + + 인덱스: idx_risk_level + + 인덱스: idx_health_score + } + + entity "health_checkup_raw" as health_checkup_raw { + * raw_id : BIGINT <> + -- + * member_serial_number : BIGINT + * reference_year : INTEGER + * birth_date : DATE + * name : VARCHAR(50) + * region_code : INTEGER + * gender_code : INTEGER + * age : INTEGER + * height : INTEGER + * weight : INTEGER + * waist_circumference : INTEGER + * visual_acuity_left : DECIMAL(3,1) + * visual_acuity_right : DECIMAL(3,1) + * hearing_left : INTEGER + * hearing_right : INTEGER + * systolic_bp : INTEGER + * diastolic_bp : INTEGER + * fasting_glucose : INTEGER + * total_cholesterol : INTEGER + * triglyceride : INTEGER + * hdl_cholesterol : INTEGER + * ldl_cholesterol : INTEGER + * hemoglobin : DECIMAL(4,1) + * urine_protein : INTEGER + * serum_creatinine : DECIMAL(4,1) + * ast : INTEGER + * alt : INTEGER + * gamma_gtp : INTEGER + * smoking_status : INTEGER + * drinking_status : INTEGER + * created_at : TIMESTAMP + -- + + 인덱스: idx_member_serial_year + + 인덱스: idx_name_birthdate + + 인덱스: idx_gender_age + } + + entity "health_normal_ranges" as health_normal_range { + * item_code : VARCHAR(20) <> + * gender_code : INTEGER <> + -- + * item_name : VARCHAR(100) + * normal_min : DECIMAL(10,2) + * normal_max : DECIMAL(10,2) + * caution_min : DECIMAL(10,2) + * caution_max : DECIMAL(10,2) + * danger_min : DECIMAL(10,2) + * danger_max : DECIMAL(10,2) + * unit : VARCHAR(20) + * description : TEXT + * is_active : BOOLEAN + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + -- + + 인덱스: idx_item_code + + 인덱스: idx_gender_code + } + + entity "health_files" as health_file { + * file_id : VARCHAR(50) <> + -- + * user_id : VARCHAR(50) + * file_name : VARCHAR(255) + * file_type : VARCHAR(50) + * file_url : VARCHAR(500) + * file_size : BIGINT + * upload_status : VARCHAR(20) + * uploaded_at : TIMESTAMP + * processed_at : TIMESTAMP + -- + + 인덱스: idx_user_id + + 인덱스: idx_upload_status + + 인덱스: idx_uploaded_at + } + + note right of health_checkup + **가공된 건강검진 데이터** + • User Service의 사용자와 연결 + • 정상치 기준과 비교 분석 완료 + • 건강 점수 및 위험도 계산 결과 + • 이상 항목을 JSON 배열로 저장 + • 1명당 1개 레코드 (최신 데이터만) + end note + + note right of health_checkup_raw + **건강보험공단 원본 데이터** + • 연도별 검진 데이터 보관 + • 가공 전 원본 데이터 유지 + • 개인정보 포함 (이름, 생년월일) + • 성별/나이별 통계 분석 용도 + • 여러 연도 데이터 보관 가능 + end note + + note right of health_normal_range + **건강검진 정상치 기준** + • 성별별 정상치 기준 관리 + • gender_code: 0(공통), 1(남성), 2(여성) + • 정상/주의/위험 3단계 범위 + • 의료진 검토 후 업데이트 + • 마스터 데이터 성격 + end note + + note right of health_file + **업로드된 건강검진 파일** + • Azure Blob Storage 연동 + • PDF, 이미지 파일 지원 + • 업로드 상태 추적 + • OCR 처리 결과 연동 준비 + end note +} + +package "관계 정의" as relationships { + health_checkup ||--|| health_checkup_raw : raw_id + health_checkup }|--|| health_normal_range : 정상치_비교 + health_file }|--|| health_checkup : 파일_연동 + + note as n1 + **외래키 관계** + health_checkups.raw_id → health_checkup_raw.raw_id + + **논리적 관계** + • health_checkups ↔ health_normal_ranges (정상치 비교) + • health_files ↔ health_checkups (파일-데이터 연결) + • User Service users ↔ health_checkups (사용자별 데이터) + + **참조 무결성** + • raw_id는 NOT NULL (원본 데이터 필수) + • user_id는 User Service와 일관성 유지 + • member_serial_number로 건보공단 데이터 연결 + end note +} + +package "데이터 타입 및 제약조건" as constraints { + note as n2 + **health_checkups 제약조건** + • id: AUTO_INCREMENT + • user_id: 최대 50자, NOT NULL + • member_serial_number: User Service와 일치 + • reference_year: 1990~현재년도 + • bmi: 계산값, 10.0~50.0 범위 + • blood pressure: systolic > diastolic + • risk_level: 'normal', 'caution', 'danger' + • health_score: 0~100 점수 + • abnormal_indicators: JSON 배열 형태 + + **health_checkup_raw 제약조건** + • raw_id: AUTO_INCREMENT + • member_serial_number: NOT NULL + • reference_year: NOT NULL + • gender_code: 1(남성), 2(여성) + • age: 0~120 범위 + • 모든 수치 데이터: 음수 불가 + + **health_normal_ranges 제약조건** + • 복합 기본키: (item_code, gender_code) + • gender_code: 0(공통), 1(남성), 2(여성) + • normal_min ≤ normal_max + • caution 범위는 normal 범위 밖 + • danger 범위는 caution 범위 밖 + + **health_files 제약조건** + • file_id: UUID 형태 + • file_type: 'pdf', 'jpg', 'png' 등 + • file_size: 바이트 단위, 최대 10MB + • upload_status: 'uploading', 'completed', 'failed' + end note +} + +package "정상치 기준 예시 데이터" as normal_ranges_data { + note as n3 + **주요 항목별 정상치 기준 (성인 남성 기준)** + + | item_code | normal_range | caution_range | danger_range | + |-----------|-------------|---------------|--------------| + | BMI | 18.5~24.9 | 25.0~29.9 또는 <18.5 | ≥30.0 | + | SYSTOLIC_BP | 90~119 | 120~139 | ≥140 | + | DIASTOLIC_BP | 60~79 | 80~89 | ≥90 | + | FASTING_GLUCOSE | 70~99 | 100~125 | ≥126 | + | TOTAL_CHOLESTEROL | <200 | 200~239 | ≥240 | + | HDL_CHOLESTEROL | ≥40 | 35~39 | <35 | + | LDL_CHOLESTEROL | <130 | 130~159 | ≥160 | + | TRIGLYCERIDE | <150 | 150~199 | ≥200 | + | AST | <40 | 40~80 | >80 | + | ALT | <40 | 40~80 | >80 | + | GAMMA_GTP | <60 | 60~100 | >100 | + | HEMOGLOBIN | 13.0~17.0 | 12.0~12.9 | <12.0 | + + **성별 차이** + • 여성은 HDL 콜레스테롤 ≥50, 혈색소 12.0~15.0 + • 임신 가능 연령대 여성은 별도 기준 적용 + end note +} + +package "성능 및 운영 고려사항" as performance { + note as n4 + **인덱스 전략** + • health_checkups: (user_id, reference_year) 복합 인덱스 + • health_checkup_raw: (member_serial_number, reference_year) 복합 인덱스 + • health_normal_ranges: item_code 단일 인덱스 + • health_files: (user_id, uploaded_at) 복합 인덱스 + + **파티셔닝** + • health_checkup_raw: reference_year 기준 연도별 파티셔닝 + • health_files: uploaded_at 기준 월별 파티셔닝 + + **아카이빙** + • 5년 이상 된 raw 데이터는 별도 아카이브 테이블로 이관 + • 업로드 실패 파일은 30일 후 자동 삭제 + + **캐싱 전략** + • health_normal_ranges: Redis에 전체 캐싱 (24시간) + • 사용자별 최신 건강검진: Redis 캐싱 (1시간) + • 건강 점수 계산 결과: Redis 캐싱 (6시간) + + **모니터링 지표** + • 정상치 기준 업데이트 빈도 + • 평균 건강 점수 추이 + • 위험군 사용자 비율 + • 파일 업로드 성공률 + end note +} + +package "데이터 보안 및 규정 준수" as security { + note as n5 + **개인정보 보호** + • health_checkup_raw.name: 암호화 저장 + • health_checkup_raw.birth_date: 마스킹 처리 + • 의료 데이터 접근 시 감사 로그 기록 + + **의료정보 보안** + • 의료법 및 개인정보보호법 준수 + • 건강검진 데이터 3년 보존 의무 + • 개인식별정보와 건강정보 분리 저장 + + **접근 제어** + • 의료진만 정상치 기준 수정 가능 + • 사용자 본인 데이터만 조회 가능 + • 관리자 접근 시 의료진 승인 필요 + + **데이터 무결성** + • 건강검진 데이터 변조 방지 + • 원본 데이터 불변성 보장 + • 정상치 기준 변경 시 이력 관리 + + **백업 및 복구** + • 건강검진 데이터 일일 백업 + • 정상치 기준 변경 전 백업 + • 재해 복구 시 의료진 검증 필수 + end note +} \ No newline at end of file diff --git a/design/데이터설계서 - Intelligence Service.txt b/design/데이터설계서 - Intelligence Service.txt new file mode 100644 index 0000000..d5c0d27 --- /dev/null +++ b/design/데이터설계서 - Intelligence Service.txt @@ -0,0 +1,310 @@ +!theme mono + +skinparam classAttributeIconSize 0 +skinparam classFontSize 12 +skinparam classAttributeFontSize 11 + +title HealthSync 역설계 - Intelligence Service 데이터 설계서 + +package "Intelligence Service Database Schema" as intelligence_db #lightseagreen { + + entity "chat_sessions" as chat_session { + * session_id : VARCHAR(50) <> + -- + * user_id : VARCHAR(50) + * status : VARCHAR(20) + * created_at : TIMESTAMP + * last_activity_at : TIMESTAMP + * message_count : INTEGER + * context : TEXT + * session_type : VARCHAR(30) + * expires_at : TIMESTAMP + -- + + 인덱스: idx_user_id_status + + 인덱스: idx_last_activity + + 인덱스: idx_expires_at + } + + entity "chat_messages" as chat_message { + * message_id : VARCHAR(50) <> + -- + * session_id : VARCHAR(50) <> + * role : VARCHAR(20) + * content : TEXT + * timestamp : TIMESTAMP + * message_order : INTEGER + * is_sensitive : BOOLEAN + * token_count : INTEGER + * response_time_ms : INTEGER + -- + + 인덱스: idx_session_order + + 인덱스: idx_timestamp + + 인덱스: idx_role + } + + entity "analysis_results" as analysis_result { + * id : BIGINT <> + -- + * user_id : VARCHAR(50) + * analysis_type : VARCHAR(50) + * result : TEXT + * confidence : DECIMAL(5,4) + * created_at : TIMESTAMP + * metadata : JSON + * source_data_hash : VARCHAR(64) + * claude_model_version : VARCHAR(20) + * processing_time_ms : INTEGER + -- + + 인덱스: idx_user_analysis_type + + 인덱스: idx_created_at + + 인덱스: idx_confidence + + 인덱스: idx_source_hash + } + + entity "notification_logs" as notification_log { + * id : BIGINT <> + -- + * user_id : VARCHAR(50) + * mission_id : VARCHAR(50) + * notification_type : VARCHAR(30) + * message : TEXT + * sent_at : TIMESTAMP + * delivery_status : VARCHAR(20) + * response_generated_by : VARCHAR(20) + * processing_time_ms : INTEGER + * user_reaction : VARCHAR(20) + * effectiveness_score : DECIMAL(3,2) + -- + + 인덱스: idx_user_id_type + + 인덱스: idx_sent_at + + 인덱스: idx_delivery_status + + 인덱스: idx_effectiveness + } + + entity "claude_api_usage" as claude_api_usage { + * id : BIGINT <> + -- + * request_id : VARCHAR(50) + * user_id : VARCHAR(50) + * api_endpoint : VARCHAR(100) + * request_type : VARCHAR(50) + * input_tokens : INTEGER + * output_tokens : INTEGER + * cost_usd : DECIMAL(10,6) + * response_time_ms : INTEGER + * success : BOOLEAN + * error_message : TEXT + * created_at : TIMESTAMP + -- + + 인덱스: idx_user_id_date + + 인덱스: idx_request_type + + 인덱스: idx_success + + 인덱스: idx_cost + } + + note right of chat_session + **AI 채팅 세션 관리** + • 사용자별 대화 컨텍스트 유지 + • 세션별 상태 관리 (활성/비활성/만료) + • 컨텍스트 정보로 개인화 대화 + • 자동 만료 기능 (24시간) + end note + + note right of chat_message + **채팅 메시지 저장** + • 사용자와 AI 간 모든 대화 기록 + • 메시지 순서 보장 + • 민감정보 플래그 관리 + • Claude API 토큰 사용량 추적 + end note + + note right of analysis_result + **AI 분석 결과 저장** + • 건강 진단, 미션 추천 등 분석 결과 + • 신뢰도 점수로 품질 관리 + • 소스 데이터 해시로 중복 방지 + • Claude 모델 버전별 성능 추적 + end note + + note right of notification_log + **알림 발송 이력** + • 축하, 독려 메시지 발송 기록 + • 사용자 반응 및 효과성 측정 + • 개인화 알림 성능 분석 + • A/B 테스트 결과 수집 + end note + + note right of claude_api_usage + **Claude API 사용량 추적** + • API 호출별 비용 및 성능 모니터링 + • 토큰 사용량 기반 과금 관리 + • 에러 발생 패턴 분석 + • 사용자별 API 사용 통계 + end note +} + +package "관계 정의" as relationships { + chat_session ||--o{ chat_message : session_id + chat_session }o--|| analysis_result : 분석_요청 + notification_log }o--|| analysis_result : 알림_생성 + claude_api_usage }o--|| analysis_result : API_호출 + claude_api_usage }o--|| chat_message : API_호출 + + note as n1 + **외래키 관계** + chat_messages.session_id → chat_sessions.session_id + + **논리적 관계** + • User Service users ↔ chat_sessions (사용자별 채팅) + • Goal Service missions ↔ notification_logs (미션별 알림) + • Health Service checkups ↔ analysis_results (건강 분석) + + **참조 무결성** + • CASCADE 삭제 (세션 삭제 시 메시지도 삭제) + • 사용자 ID는 User Service와 일관성 유지 + • 미션 ID는 Goal Service와 연동 + + **데이터 일관성** + • 세션별 메시지 순서 보장 + • API 사용량과 실제 호출 일치 + • 알림 발송 성공/실패 상태 추적 + end note +} + +package "데이터 타입 및 제약조건" as constraints { + note as n2 + **chat_sessions 제약조건** + • session_id: UUID 형태, PRIMARY KEY + • user_id: User Service와 일치, NOT NULL + • status: 'ACTIVE', 'INACTIVE', 'EXPIRED', 'TERMINATED' + • session_type: 'HEALTH_CONSULTATION', 'GENERAL_CHAT', 'MISSION_GUIDANCE' + • message_count: 0 이상, 최대 100개 메시지 + • expires_at: created_at + 24시간 + + **chat_messages 제약조건** + • message_id: UUID 형태, PRIMARY KEY + • role: 'USER', 'ASSISTANT', 'SYSTEM' + • content: 최대 10,000자 + • message_order: 세션 내 순차 증가 + • token_count: 0 이상 + • response_time_ms: 0 이상 + + **analysis_results 제약조건** + • analysis_type: 'HEALTH_DIAGNOSIS', 'MISSION_RECOMMENDATION', 'RISK_ASSESSMENT' + • confidence: 0.0000~1.0000 범위 + • result: 최대 50,000자 + • source_data_hash: SHA-256 해시값 + • claude_model_version: 'claude-3-sonnet', 'claude-3-opus' 등 + + **notification_logs 제약조건** + • notification_type: 'CELEBRATION', 'ENCOURAGEMENT', 'REMINDER', 'ACHIEVEMENT' + • delivery_status: 'SENT', 'DELIVERED', 'FAILED', 'PENDING' + • response_generated_by: 'CLAUDE_AI', 'TEMPLATE', 'FALLBACK' + • user_reaction: 'POSITIVE', 'NEGATIVE', 'NEUTRAL', 'NO_RESPONSE' + • effectiveness_score: 0.00~5.00 범위 + + **claude_api_usage 제약조건** + • request_type: 'CHAT', 'ANALYSIS', 'RECOMMENDATION', 'NOTIFICATION' + • input_tokens: 0 이상 + • output_tokens: 0 이상 + • cost_usd: 0.000001 이상 (최소 과금 단위) + • success: 성공/실패 여부 + end note +} + +package "AI 모델 및 프롬프트 관리" as ai_models { + note as n3 + **Claude AI 모델별 특성** + + **claude-3-sonnet (기본 모델)** + • 건강 상담 및 일반 채팅 + • 비용 효율적, 응답 속도 빠름 + • 토큰당 비용: $0.003 (input), $0.015 (output) + + **claude-3-opus (고급 모델)** + • 복잡한 건강 분석 및 진단 + • 높은 정확도, 상세한 분석 + • 토큰당 비용: $0.015 (input), $0.075 (output) + + **프롬프트 최적화** + • 건강 상담: 의료진 톤앤매너 적용 + • 미션 추천: 개인화 요소 강조 + • 독려 메시지: 동기부여 심리학 적용 + • 축하 메시지: 긍정적 감정 표현 + + **컨텍스트 관리** + • 최근 10개 메시지 유지 + • 사용자 프로필 정보 포함 + • 건강 상태 요약 정보 제공 + • 이전 분석 결과 참조 + end note +} + +package "성능 및 운영 고려사항" as performance { + note as n4 + **인덱스 전략** + • chat_sessions: (user_id, status) 복합 인덱스 + • chat_messages: (session_id, message_order) 복합 인덱스 + • analysis_results: (user_id, analysis_type, created_at) 복합 인덱스 + • notification_logs: (user_id, sent_at) 복합 인덱스 + • claude_api_usage: (user_id, DATE(created_at)) 복합 인덱스 + + **파티셔닝** + • chat_messages: created_at 기준 월별 파티셔닝 + • claude_api_usage: created_at 기준 월별 파티셔닝 + • notification_logs: sent_at 기준 월별 파티셔닝 + + **캐싱 전략** + • 활성 채팅 세션: Redis 캐싱 (30분) + • 최근 분석 결과: Redis 캐싱 (1시간) + • Claude API 응답: Redis 캐싱 (10분) + • 자주 사용되는 프롬프트: Redis 캐싱 (24시간) + + **비용 최적화** + • 중복 분석 요청 캐싱으로 API 호출 최소화 + • 토큰 사용량 모니터링 및 알림 + • 모델별 비용 효율성 분석 + • 배치 처리로 API 호출 효율화 + + **모니터링 지표** + • 일일 Claude API 비용 + • 평균 응답 시간 + • API 성공률 + • 사용자별 토큰 사용량 + • 분석 결과 신뢰도 분포 + end note +} + +package "데이터 보안 및 개인정보 보호" as security { + note as n5 + **개인정보 보호** + • 채팅 내용 중 민감정보 자동 마스킹 + • 의료 정보 포함 메시지 암호화 저장 + • 사용자 식별 정보와 채팅 내용 분리 + • 90일 경과 채팅 기록 자동 삭제 + + **AI 윤리 및 안전** + • 의료 조언 범위 제한 명시 + • 응급 상황 감지 시 안내 메시지 + • 편향성 방지를 위한 프롬프트 검증 + • 부적절한 응답 필터링 + + **API 보안** + • Claude API 키 암호화 저장 + • API 호출 시 Rate Limiting 적용 + • 요청/응답 내용 로깅 (개인정보 제외) + • 이상 사용 패턴 모니터링 + + **감사 및 추적** + • 모든 AI 분석 결과 이력 보관 + • 의료 관련 분석 시 추가 검증 + • 사용자 신고 시 조치 이력 관리 + • 정기적인 AI 응답 품질 검토 + + **규정 준수** + • 의료기기법 비대상 확인 + • 개인정보보호법 준수 + • AI 윤리 가이드라인 적용 + • 의료 광고 규제 준수 + end note +} \ No newline at end of file diff --git a/design/데이터설계서 - User Service.txt b/design/데이터설계서 - User Service.txt new file mode 100644 index 0000000..7f05c27 --- /dev/null +++ b/design/데이터설계서 - User Service.txt @@ -0,0 +1,173 @@ +!theme mono + +skinparam classAttributeIconSize 0 +skinparam classFontSize 12 +skinparam classAttributeFontSize 11 + +title HealthSync 역설계 - User Service 데이터 설계서 + +package "User Service Database Schema" as user_db #lightgreen { + + entity "users" as user { + * member_serial_number : BIGINT <> + -- + * google_id : VARCHAR(255) <> + * name : VARCHAR(100) + * birth_date : DATE + * occupation : VARCHAR(50) <> + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + * last_login_at : TIMESTAMP + -- + + 인덱스: idx_google_id + + 인덱스: idx_occupation + + 인덱스: idx_created_at + } + + entity "occupation_types" as occupation_type { + * occupation_code : VARCHAR(20) <> + -- + * occupation_name : VARCHAR(100) + * category : VARCHAR(50) + * is_active : BOOLEAN + * created_at : TIMESTAMP + * updated_at : TIMESTAMP + -- + + 인덱스: idx_occupation_name + + 인덱스: idx_category + } + + note right of user + **사용자 테이블 특징** + • Google OAuth 기반 인증 + • member_serial_number는 + 건강보험공단 데이터 연동 키 + • occupation은 직업 코드 참조 + • 소프트 삭제 미적용 (물리 삭제) + end note + + note right of occupation_type + **직업 유형 테이블 특징** + • 표준 직업 분류 기반 + • 카테고리별 그룹핑 + • 활성/비활성 상태 관리 + • 마스터 데이터 성격 + end note +} + +package "관계 정의" as relationships { + user ||--o{ occupation_type : occupation + + note as n1 + **외래키 관계** + users.occupation → occupation_types.occupation_code + + **참조 무결성** + • CASCADE 업데이트 + • RESTRICT 삭제 (직업 유형 삭제 시 제한) + + **데이터 정합성** + • occupation 필드는 NULL 허용 (프로필 미완성 상태) + • birth_date는 NOT NULL (필수 정보) + • google_id는 UNIQUE 제약 (중복 가입 방지) + end note +} + +package "데이터 타입 및 제약조건" as constraints { + note as n2 + **users 테이블 제약조건** + • member_serial_number: AUTO_INCREMENT + • google_id: UNIQUE, NOT NULL + • name: NOT NULL, 최대 100자 + • birth_date: NOT NULL, 1900-01-01 이후 + • occupation: NULL 허용, 최대 50자 + • created_at: DEFAULT CURRENT_TIMESTAMP + • updated_at: ON UPDATE CURRENT_TIMESTAMP + • last_login_at: NULL 허용 + + **occupation_types 테이블 제약조건** + • occupation_code: PRIMARY KEY, 최대 20자 + • occupation_name: UNIQUE, NOT NULL, 최대 100자 + • category: NOT NULL, 최대 50자 + • is_active: DEFAULT TRUE + • created_at: DEFAULT CURRENT_TIMESTAMP + • updated_at: ON UPDATE CURRENT_TIMESTAMP + end note +} + +package "초기 데이터" as initial_data { + note as n3 + **occupation_types 초기 데이터 예시** + + | occupation_code | occupation_name | category | + |----------------|----------------|----------| + | OFF001 | 사무직 | 사무관리 | + | MFG001 | 제조업 생산직 | 제조생산 | + | SVC001 | 서비스업 | 서비스 | + | EDU001 | 교육직 | 전문직 | + | MED001 | 의료진 | 전문직 | + | IT001 | IT개발자 | 전문직 | + | SAL001 | 영업직 | 영업마케팅 | + | DRV001 | 운전직 | 운송 | + | CON001 | 건설업 | 건설 | + | FRE001 | 프리랜서 | 기타 | + + **카테고리 분류** + • 사무관리: 앉아서 일하는 직종 + • 제조생산: 신체 활동이 많은 직종 + • 서비스: 고객 응대 직종 + • 전문직: 전문 지식 요구 직종 + • 영업마케팅: 외부 활동 직종 + • 운송: 이동이 많은 직종 + • 건설: 육체 노동 직종 + • 기타: 분류하기 어려운 직종 + end note +} + +package "성능 및 운영 고려사항" as performance { + note as n4 + **인덱스 전략** + • users.google_id: 로그인 시 빠른 조회 + • users.occupation: 직업별 통계 조회 + • users.created_at: 가입 일자별 조회 + • occupation_types.occupation_name: 직업명 검색 + + **파티셔닝** + • 현재 단계에서는 미적용 + • 사용자 수 증가 시 created_at 기준 월별 파티셔닝 고려 + + **백업 및 복구** + • users: 매일 풀백업 (개인정보 보호) + • occupation_types: 주간 백업 (마스터 데이터) + + **모니터링 지표** + • 신규 가입자 수 (일별/월별) + • 활성 사용자 수 (last_login_at 기준) + • 직업별 사용자 분포 + • 평균 프로필 완성률 + end note +} + +package "데이터 보안" as security { + note as n5 + **개인정보 보호** + • name: 암호화 저장 고려 (실명) + • birth_date: 날짜 정보 마스킹 + • google_id: 해시 처리 또는 암호화 + + **접근 제어** + • 개인정보 조회 시 로그 기록 + • 관리자 접근 시 승인 프로세스 + • API 호출 시 사용자 본인 확인 + + **데이터 보존** + • 회원 탈퇴 시 개인정보 즉시 삭제 + • 로그인 기록 90일 보존 + • 감사 로그 1년 보존 + + **GDPR/개인정보보호법 준수** + • 데이터 처리 목적 명시 + • 동의 철회 시 데이터 삭제 + • 데이터 이동권 지원 + end note +} \ No newline at end of file diff --git a/design/물리아키텍처.txt b/design/물리아키텍처.txt new file mode 100644 index 0000000..67caea4 --- /dev/null +++ b/design/물리아키텍처.txt @@ -0,0 +1,227 @@ +!theme mono + +skinparam componentStyle rectangle +skinparam componentFontSize 12 +skinparam arrowFontSize 11 +skinparam arrowThickness 2 + +title HealthSync 역설계 - Azure Cloud 물리 아키텍처 + +' 외부 사용자 및 시스템 +package "External Systems" as external #lightgray { + [모바일 사용자] as MobileUser + [Google OAuth] as GoogleOAuth + [Claude AI API] as ClaudeAPI + [건강보험공단\n데이터] as NhisData +} + +' Azure Front Door & CDN +package "Global Network" as global #lightblue { + [Azure Front Door] as FrontDoor + [Azure CDN] as CDN +} + +' Azure Application Gateway & Load Balancer +package "Load Balancing" as lb #lightyellow { + [Application Gateway\n(WAF 포함)] as AppGateway + [Azure Load Balancer] as LoadBalancer +} + +' AKS Cluster +package "Azure Kubernetes Service (AKS)" as aks #lightgreen { + package "Ingress Controller" as ingress { + [NGINX Ingress\nController] as IngressController + } + + package "Application Pods" as pods { + [Gateway Pod] as GatewayPod + [User Service Pod] as UserPod + [Health Service Pod] as HealthPod + [Goal Service Pod] as GoalPod + [Intelligence Service Pod] as IntellPod + [Batch Service Pod] as BatchPod + } + + package "Supporting Services" as support { + [Istio Service Mesh] as ServiceMesh + [Prometheus\n& Grafana] as Monitoring + [FluentD] as Logging + } +} + +' Azure Database Services +package "Database Services" as database #lightpink { + [Azure Database\nfor PostgreSQL\n(Flexible Server)] as PostgreSQL + [Azure Cache\nfor Redis\n(Premium)] as Redis + [Azure Blob Storage\n(Hot Tier)] as BlobStorage +} + +' Azure Messaging & Events +package "Messaging & Events" as messaging #lightsalmon { + [Azure Service Bus\n(Standard)] as ServiceBus + [Azure Event Hub\n(Standard)] as EventHub +} + +' Azure Security & Identity +package "Security & Identity" as security #orange { + [Azure Key Vault] as KeyVault + [Azure Active Directory\nB2C] as AzureAD + [Azure Application\nInsights] as AppInsights +} + +' Azure DevOps & CI/CD +package "DevOps & CI/CD" as devops #lightsteelblue { + [Azure DevOps\nPipelines] as AzureDevOps + [Azure Container\nRegistry (ACR)] as ACR + [GitHub Repository] as GitHub + [Azure Monitor] as AzureMonitor +} + +' Azure Backup & Recovery +package "Backup & DR" as backup #thistle { + [Azure Backup] as AzureBackup + [Azure Site Recovery] as SiteRecovery + [Geo-Redundant\nStorage] as GeoStorage +} + +' === 외부 접근 및 CDN (1-3) === +MobileUser -[#blue,thickness=3]-> FrontDoor : "1. 모바일 앱 접근" +FrontDoor -[#blue,thickness=2]-> CDN : "2. 정적 자원 캐싱" +CDN -[#blue,thickness=2]-> AppGateway : "3. 동적 요청 라우팅" + +' === 로드밸런싱 및 보안 (4-5) === +AppGateway -[#green,thickness=2]-> LoadBalancer : "4. WAF 보안 필터링" +LoadBalancer -[#green,thickness=2]-> IngressController : "5. AKS 클러스터 진입" + +' === AKS 내부 라우팅 (6-8) === +IngressController -[#purple,thickness=2]-> GatewayPod : "6. API Gateway 라우팅" +GatewayPod -[#purple,thickness=2]-> ServiceMesh : "7. Service Mesh 통신" +ServiceMesh -[#purple,thickness=2]-> UserPod : "8a. User Service 호출" +ServiceMesh -[#purple,thickness=2]-> HealthPod : "8b. Health Service 호출" +ServiceMesh -[#purple,thickness=2]-> GoalPod : "8c. Goal Service 호출" +ServiceMesh -[#purple,thickness=2]-> IntellPod : "8d. Intelligence Service 호출" + +' === 데이터베이스 연결 (9-11) === +UserPod -[#red,thickness=2]-> PostgreSQL : "9a. 사용자 데이터 처리" +HealthPod -[#red,thickness=2]-> PostgreSQL : "9b. 건강검진 데이터 처리" +GoalPod -[#red,thickness=2]-> PostgreSQL : "9c. 목표/미션 데이터 처리" +IntellPod -[#red,thickness=2]-> PostgreSQL : "9d. AI 분석/채팅 데이터 처리" + +' === 캐싱 및 스토리지 (10-11) === +HealthPod -[#orange,thickness=2]-> Redis : "10a. 건강검진 결과 캐싱" +IntellPod -[#orange,thickness=2]-> Redis : "10b. AI 응답 캐싱" +GoalPod -[#orange,thickness=2]-> Redis : "10c. 활성 미션 캐싱" +HealthPod -[#brown,thickness=2]-> BlobStorage : "11. 건강검진 파일 저장" + +' === 메시징 및 이벤트 (12-13) === +UserPod -[#magenta,thickness=2]-> ServiceBus : "12a. 사용자 이벤트 발행" +HealthPod -[#magenta,thickness=2]-> ServiceBus : "12b. 건강데이터 이벤트 발행" +GoalPod -[#magenta,thickness=2]-> ServiceBus : "12c. 목표달성 이벤트 발행" +IntellPod -[#magenta,thickness=2]-> EventHub : "13. 대량 이벤트 스트리밍" + +' === 외부 API 연동 (14-16) === +UserPod -[#navy,thickness=2]-> GoogleOAuth : "14. Google SSO 인증" +IntellPod -[#navy,thickness=2]-> ClaudeAPI : "15. AI 분석/채팅 요청" +HealthPod -[#darkgreen,thickness=2]-> NhisData : "16. 건강보험공단 데이터 연동" + +' === 배치 처리 (17) === +BatchPod -[#darkred,thickness=2]-> ServiceBus : "17a. 배치 이벤트 구독" +BatchPod -[#darkred,thickness=2]-> IntellPod : "17b. 주기적 AI 알림 요청" +BatchPod -[#darkred,thickness=2]-> ClaudeAPI : "17c. 배치 AI 메시지 생성" + +' === 보안 및 키 관리 (18-20) === +GatewayPod -[#darkgray,thickness=1,dotted]-> KeyVault : "18a. Gateway 설정" +UserPod -[#darkgray,thickness=1,dotted]-> KeyVault : "18b. JWT Secret" +HealthPod -[#darkgray,thickness=1,dotted]-> KeyVault : "18c. DB 연결 정보" +IntellPod -[#darkgray,thickness=1,dotted]-> KeyVault : "18d. Claude API Key" +BatchPod -[#darkgray,thickness=1,dotted]-> KeyVault : "18e. 스케줄러 설정" + +' === 모니터링 및 로깅 (19-20) === +Monitoring -[#cyan,thickness=1,dashed]-> AppInsights : "19. 메트릭 수집" +Logging -[#cyan,thickness=1,dashed]-> AzureMonitor : "20. 로그 집계" + +' === CI/CD 파이프라인 (21-25) === +GitHub -[#teal,thickness=2]-> AzureDevOps : "21. 코드 푸시 트리거" +AzureDevOps -[#teal,thickness=2]-> ACR : "22. 컨테이너 빌드 & 푸시" +AzureDevOps -[#teal,thickness=2]-> GitHub : "23. 배포 YAML 업데이트" +GitHub -[#teal,thickness=2]-> aks : "24. GitOps 배포 동기화" +AzureDevOps -[#teal,thickness=2]-> AzureMonitor : "25. 배포 모니터링" + +' === 백업 및 재해복구 (26-28) === +PostgreSQL -[#purple,thickness=1,dashed]-> AzureBackup : "26. 데이터베이스 백업" +BlobStorage -[#purple,thickness=1,dashed]-> GeoStorage : "27. 파일 지역 복제" +aks -[#purple,thickness=1,dashed]-> SiteRecovery : "28. 클러스터 재해복구" + +' === 주요 특징 및 처리 흐름 설명 === +note top of FrontDoor + **글로벌 진입점** + • 전 세계 사용자 접근 최적화 + • DDoS 보호 및 SSL 터미네이션 + • 지연시간 최소화 +end note + +note top of AKS + **컨테이너 오케스트레이션** + • 마이크로서비스 자동 스케일링 + • Service Mesh로 서비스 간 통신 + • 무중단 배포 및 롤백 + • 리소스 효율적 관리 +end note + +note top of PostgreSQL + **통합 데이터베이스** + • 단일 PostgreSQL로 모든 서비스 데이터 통합 + • Flexible Server로 성능 최적화 + • 자동 백업 및 포인트-인-타임 복구 + • Read Replica로 읽기 성능 향상 +end note + +note top of Redis + **고성능 캐싱** + • Premium 티어로 99.9% 가용성 + • 클러스터링으로 확장성 확보 + • 영구 저장으로 데이터 보호 + • 지역 복제로 재해 대비 +end note + +note top of ServiceBus + **신뢰성 있는 메시징** + • 이벤트 기반 아키텍처 구현 + • 메시지 순서 보장 + • Dead Letter Queue로 실패 처리 + • Auto-scaling으로 부하 대응 +end note + +note top of KeyVault + **중앙 보안 관리** + • 모든 시크릿 중앙 관리 + • 하드웨어 보안 모듈 (HSM) + • 액세스 정책 세밀 제어 + • 감사 로그 자동 기록 +end note + +note bottom of AzureDevOps + **완전 자동화 CI/CD** + • 코드 커밋 → 자동 빌드 + • 컨테이너 이미지 스캔 + • 단계별 배포 승인 + • Blue-Green 배포 지원 + • 자동 롤백 기능 +end note + +note bottom of AzureMonitor + **통합 모니터링** + • 실시간 성능 메트릭 + • 사용자 정의 대시보드 + • 자동 알림 및 경고 + • 로그 분석 및 검색 + • 비용 최적화 인사이트 +end note + +note bottom of external + **외부 시스템 연동** + • Google OAuth: 안전한 사용자 인증 + • Claude AI: 고품질 AI 분석 + • 건강보험공단: 신뢰성 있는 건강 데이터 + • Circuit Breaker 패턴으로 안정성 확보 +end note \ No newline at end of file diff --git a/design/외부 시퀀스 다이어그램.txt b/design/외부 시퀀스 다이어그램.txt new file mode 100644 index 0000000..1dd9b54 --- /dev/null +++ b/design/외부 시퀀스 다이어그램.txt @@ -0,0 +1,140 @@ +!theme mono + +skinparam sequenceArrowThickness 2 +skinparam sequenceParticipantBorderThickness 2 +skinparam sequenceActorBorderThickness 2 +skinparam sequenceGroupBorderThickness 2 + +title HealthSync 역설계 - 외부 시퀀스 다이어그램 (개발 소스 기반) + +participant "React 모바일 앱" as MobileApp +participant "API Gateway" as Gateway +participant "User Service" as UserSvc +participant "Health Service" as HealthSvc +participant "Goal Service" as GoalSvc +participant "Intelligence Service" as IntelSvc +participant "Google SSO" as GoogleSSO +participant "Claude API" as ClaudeAPI +participant "Azure Blob Storage" as BlobStorage +participant "Redis Cache" as Redis +participant "PostgreSQL" as DB + +== 1. 사용자 인증 및 등록 == + +MobileApp -> Gateway: POST /api/users/auth/google-login (구글 로그인) +Gateway -> UserSvc: POST /auth/google-login +UserSvc -> GoogleSSO: OAuth 2.0 인증 요청 +UserSvc -> DB: 사용자 조회/생성 +UserSvc -> UserSvc: JWT 토큰 생성 + +MobileApp -> Gateway: POST /api/users/profile/complete (프로필 완료) +Gateway -> UserSvc: POST /profile/complete +UserSvc -> DB: 사용자 프로필 업데이트 + +MobileApp -> Gateway: GET /api/users/profile (프로필 조회) +Gateway -> UserSvc: GET /profile +UserSvc -> DB: 사용자 정보 조회 + +== 2. 건강검진 데이터 관리 == + +MobileApp -> Gateway: POST /api/health/checkup/sync (건강검진 연동) +Gateway -> HealthSvc: POST /checkup/sync +HealthSvc -> DB: 건강보험공단 원본 데이터 조회 +HealthSvc -> DB: 성별별 정상치 기준 조회 +HealthSvc -> DB: 정상치 비교 분석 후 저장 +HealthSvc -> Redis: 분석 결과 캐싱 + +MobileApp -> Gateway: GET /api/health/checkup/history (건강검진 이력) +Gateway -> HealthSvc: GET /checkup/history +HealthSvc -> Redis: 캐시 조회 +alt 캐시 미스 + HealthSvc -> DB: 건강검진 이력 조회 + HealthSvc -> Redis: 결과 캐싱 +end + +MobileApp -> Gateway: POST /api/health/checkup/upload (파일 업로드) +Gateway -> HealthSvc: POST /checkup/upload +HealthSvc -> BlobStorage: Azure Blob 파일 저장 +HealthSvc -> DB: 파일 메타데이터 저장 + +== 3. AI 기반 건강 분석 == + +MobileApp -> Gateway: GET /api/intelligence/health/diagnosis (AI 3줄 요약) +Gateway -> IntelSvc: GET /health/diagnosis +IntelSvc -> HealthSvc: 건강검진 데이터 조회 (서비스 간 통신) +IntelSvc -> ClaudeAPI: AI 분석 요청 +IntelSvc -> Redis: 분석 결과 캐싱 + +MobileApp -> Gateway: POST /api/intelligence/missions/recommend (미션 추천) +Gateway -> IntelSvc: POST /missions/recommend +IntelSvc -> HealthSvc: 건강상태 조회 +IntelSvc -> UserSvc: 사용자 직업정보 조회 +IntelSvc -> ClaudeAPI: 개인화 미션 추천 요청 + +== 4. 목표 설정 및 관리 == + +MobileApp -> Gateway: POST /api/goals/missions/select (미션 선택) +Gateway -> GoalSvc: POST /missions/select +GoalSvc -> DB: 목표 설정 저장 + +MobileApp -> Gateway: GET /api/goals/missions/active (활성 미션 조회) +Gateway -> GoalSvc: GET /missions/active +GoalSvc -> Redis: 활성 미션 캐시 조회 +alt 캐시 미스 + GoalSvc -> DB: 활성 미션 조회 + GoalSvc -> Redis: 결과 캐싱 +end + +MobileApp -> Gateway: PUT /api/goals/missions/{missionId}/complete (미션 완료) +Gateway -> GoalSvc: PUT /missions/{missionId}/complete +GoalSvc -> DB: 미션 완료 처리 +GoalSvc -> Redis: 관련 캐시 무효화 + +MobileApp -> Gateway: GET /api/goals/missions/history (미션 이력) +Gateway -> GoalSvc: GET /missions/history +GoalSvc -> DB: 미션 달성 이력 조회 + +== 5. AI 챗봇 상담 == + +MobileApp -> Gateway: POST /api/intelligence/chat/consultation (AI 채팅) +Gateway -> IntelSvc: POST /chat/consultation +IntelSvc -> DB: 채팅 히스토리 조회 +IntelSvc -> ClaudeAPI: AI 챗봇 응답 요청 +IntelSvc -> DB: 채팅 메시지 저장 +IntelSvc -> Redis: 채팅 히스토리 캐싱 + +MobileApp -> Gateway: GET /api/intelligence/chat/history (채팅 이력) +Gateway -> IntelSvc: GET /chat/history +IntelSvc -> Redis: 캐시된 채팅 이력 조회 + +== 6. 알림 및 독려 시스템 == + +MobileApp -> Gateway: POST /api/intelligence/notifications/celebration (축하 메시지) +Gateway -> IntelSvc: POST /notifications/celebration +IntelSvc -> GoalSvc: 미션 달성 정보 조회 +IntelSvc -> ClaudeAPI: 축하 메시지 생성 요청 + +Gateway -> IntelSvc: POST /batch/notifications (배치 알림 처리) +IntelSvc -> GoalSvc: 모든 사용자 진행상황 조회 +IntelSvc -> ClaudeAPI: 개인화 독려 메시지 생성 +IntelSvc -> Redis: 알림 결과 캐싱 + +== 7. 목표 재설정 == + +MobileApp -> Gateway: POST /api/goals/missions/reset (목표 재설정) +Gateway -> GoalSvc: POST /missions/reset +GoalSvc -> IntelSvc: 새로운 미션 추천 요청 +GoalSvc -> DB: 기존 목표 비활성화 및 새 목표 설정 + +== 주요 특징 == +note over MobileApp, DB +**개발 소스 기반 주요 특징** +• JWT 기반 인증 (모든 API) +• Redis 캐싱으로 성능 최적화 +• Claude AI 연동 강화 +• 정상치 기준 비교 분석 +• Azure Blob 파일 저장 +• 서비스 간 HTTP 통신 +• 이벤트 기반 비동기 처리 +• 예외 처리 및 회복 전략 +end note \ No newline at end of file diff --git a/design/클래스 설계서.txt b/design/클래스 설계서.txt new file mode 100644 index 0000000..effbd16 --- /dev/null +++ b/design/클래스 설계서.txt @@ -0,0 +1,1534 @@ +!theme mono + +skinparam classAttributeIconSize 0 +skinparam classFontSize 12 +skinparam classAttributeFontSize 11 + +title HealthSync 역설계 - Clean Architecture 기반 클래스 설계서 + +package "healthsync-root" { + + package "common" { + class JwtConfig { + +secretKey: String + +accessTokenValidityInSeconds: long + +refreshTokenValidityInSeconds: long + +generateToken(userDetails: UserDetails): String + +validateToken(token: String): boolean + +extractUsername(token: String): String + } + + class SecurityConfig { + +jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint + +jwtRequestFilter: JwtRequestFilter + +passwordEncoder(): BCryptPasswordEncoder + +authenticationManager(): AuthenticationManager + } + + class GlobalExceptionHandler { + +handleBusinessException(ex: BusinessException): ResponseEntity + +handleValidationException(ex: ValidationException): ResponseEntity + +handleAuthenticationException(ex: AuthenticationException): ResponseEntity + +handleGenericException(ex: Exception): ResponseEntity + } + + class ApiResponse { + +success: boolean + +data: T + +message: String + +timestamp: LocalDateTime + +of(data: T): ApiResponse + +error(message: String): ApiResponse + } + + abstract class BaseEntity { + +id: Long + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +version: Long + } + + class BusinessException { + +errorCode: ErrorCode + +message: String + +BusinessException(errorCode: ErrorCode, message: String) + } + + enum ErrorCode { + USER_NOT_FOUND, + INVALID_CREDENTIALS, + HEALTH_DATA_NOT_FOUND, + MISSION_NOT_FOUND, + UNAUTHORIZED_ACCESS, + VALIDATION_FAILED + } + } + + package "user-service" { + class UserServiceApplication { + +main(args: String[]): void + } + + package "interface-adapters" { + package "controllers" { + class AuthController { + -authUseCase: AuthUseCase + -userUseCase: UserUseCase + +googleLogin(request: GoogleLoginRequest): ResponseEntity + +completeProfile(request: UserRegistrationRequest): ResponseEntity + +getProfile(): ResponseEntity + } + + class UserController { + -userUseCase: UserUseCase + +getAllOccupations(): ResponseEntity + +getOccupationName(occupationCode: String): ResponseEntity + +getOccupationCode(occupationName: String): ResponseEntity + } + } + + package "adapters" { + class GoogleAuthAdapter { + -googleTokenVerifier: GoogleIdTokenVerifier + +verifyGoogleToken(accessToken: String, idToken: String): GoogleUserInfo + +extractUserInfo(idToken: GoogleIdToken): GoogleUserInfo + } + + class JwtTokenAdapter { + -jwtConfig: JwtConfig + +generateTokens(user: User): TokenPair + +validateToken(token: String): boolean + +extractUserId(token: String): String + } + + class EventPublisherAdapter { + -serviceBusTemplate: ServiceBusTemplate + +publishUserRegisteredEvent(user: User): void + +publishUserProfileUpdatedEvent(user: User): void + } + } + } + + package "application-services" { + class AuthUseCase { + -authDomainService: AuthDomainService + -userRepository: UserRepository + -jwtTokenAdapter: JwtTokenAdapter + -eventPublisher: EventPublisherAdapter + +authenticateWithGoogle(request: GoogleLoginRequest): LoginResponse + +refreshToken(refreshToken: String): TokenPair + } + + class UserUseCase { + -userDomainService: UserDomainService + -userRepository: UserRepository + -occupationRepository: OccupationRepository + -eventPublisher: EventPublisherAdapter + +completeUserProfile(request: UserRegistrationRequest): UserRegistrationResponse + +getUserProfile(userId: String): UserProfileResponse + +getAllOccupations(): OccupationListResponse + +convertOccupationCodeToName(code: String): String + +convertOccupationNameToCode(name: String): String + } + } + + package "domain" { + package "model" { + class User { + +memberSerialNumber: Long + +googleId: String + +name: String + +birthDate: LocalDate + +occupation: String + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +lastLoginAt: LocalDateTime + +calculateAge(): int + +isProfileComplete(): boolean + +updateProfile(name: String, birthDate: LocalDate, occupation: String): void + } + + class OccupationType { + +occupationCode: String + +occupationName: String + +category: String + +isValidCode(code: String): boolean + } + + class GoogleUserInfo { + +googleId: String + +email: String + +name: String + +picture: String + +isValid(): boolean + } + } + + package "services" { + class AuthDomainService { + +validateGoogleTokens(accessToken: String, idToken: String): GoogleUserInfo + +createNewUser(googleUserInfo: GoogleUserInfo): User + +updateLastLogin(user: User): User + } + + class UserDomainService { + +validateProfileData(profileData: UserRegistrationRequest): void + +updateUserProfile(user: User, profileData: UserRegistrationRequest): User + +calculateAge(birthDate: LocalDate): int + } + } + + package "repositories" { + interface UserRepository { + +findByGoogleId(googleId: String): Optional + +findById(id: Long): Optional + +save(user: User): User + +updateLastLoginAt(id: Long): void + +existsByGoogleId(googleId: String): boolean + } + + interface OccupationRepository { + +findAll(): List + +findByOccupationCode(code: String): Optional + +findByOccupationName(name: String): Optional + +validateOccupationCode(code: String): boolean + } + } + } + + package "infrastructure" { + package "repositories" { + class UserRepositoryImpl { + -entityManager: EntityManager + +findByGoogleId(googleId: String): Optional + +findById(id: Long): Optional + +save(user: User): User + +updateLastLoginAt(id: Long): void + +existsByGoogleId(googleId: String): boolean + } + + class OccupationRepositoryImpl { + -entityManager: EntityManager + +findAll(): List + +findByOccupationCode(code: String): Optional + +findByOccupationName(name: String): Optional + +validateOccupationCode(code: String): boolean + } + } + + package "entities" { + class UserEntity { + +memberSerialNumber: Long + +googleId: String + +name: String + +birthDate: LocalDate + +occupation: String + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +lastLoginAt: LocalDateTime + +toDomain(): User + +fromDomain(user: User): UserEntity + } + + class OccupationTypeEntity { + +occupationCode: String + +occupationName: String + +category: String + +toDomain(): OccupationType + } + } + } + + package "dto" { + class GoogleLoginRequest { + +googleAccessToken: String + +googleIdToken: String + } + + class LoginResponse { + +accessToken: String + +refreshToken: String + +userId: String + +isNewUser: boolean + +message: String + } + + class UserRegistrationRequest { + +name: String + +birthDate: String + +occupation: String + } + + class UserRegistrationResponse { + +userId: String + +message: String + +status: String + +profileCompletedAt: String + } + + class UserProfileResponse { + +userId: String + +name: String + +age: int + +occupation: String + +registeredAt: String + +lastLoginAt: String + } + + class OccupationListResponse { + +occupations: List + +totalCount: int + } + + class OccupationDto { + +occupationCode: String + +occupationName: String + +category: String + } + } + } + + package "health-service" { + class HealthServiceApplication { + +main(args: String[]): void + } + + package "interface-adapters" { + package "controllers" { + class HealthController { + -checkupSyncUseCase: CheckupSyncUseCase + -checkupQueryUseCase: CheckupQueryUseCase + -fileUploadUseCase: FileUploadUseCase + +syncCheckupData(userId: String): ResponseEntity + +getCheckupHistory(userId: String, limit: int): ResponseEntity + +uploadCheckupFile(request: CheckupFileRequest): ResponseEntity + +getNormalRanges(genderCode: int): ResponseEntity + } + } + + package "adapters" { + class UserServiceAdapter { + -userServiceClient: UserServiceClient + +getUserInfo(userId: String): UserInfo + +getUserBasicInfo(userId: String): UserBasicInfo + } + + class BlobStorageAdapter { + -blobServiceClient: BlobServiceClient + +uploadFile(fileContent: String, fileName: String): String + +generateSasToken(containerName: String, fileName: String): String + +downloadFile(blobUrl: String): byte[] + } + + class CacheAdapter { + -redisTemplate: RedisTemplate + +getCachedHistory(userId: String): HealthHistoryResponse + +cacheHistory(userId: String, response: HealthHistoryResponse): void + +getCachedNormalRanges(genderCode: int): List + +cacheNormalRanges(genderCode: int, ranges: List): void + +invalidateUserHealthCache(userId: String): void + } + + class EventPublisherAdapter { + -serviceBusTemplate: ServiceBusTemplate + +publishHealthDataSyncedEvent(userId: String, syncResult: HealthSyncResult): void + +publishFileUploadedEvent(userId: String, fileInfo: FileInfo): void + } + } + } + + package "application-services" { + class CheckupSyncUseCase { + -healthProfileDomainService: HealthProfileDomainService + -checkupAnalysisDomainService: CheckupAnalysisDomainService + -healthRepository: HealthRepository + -healthCheckupRawRepository: HealthCheckupRawRepository + -normalRangeRepository: NormalRangeRepository + -userServiceAdapter: UserServiceAdapter + -cacheAdapter: CacheAdapter + -eventPublisher: EventPublisherAdapter + +syncCheckupData(userId: String): HealthSyncResponse + +processRawCheckupData(rawData: List, userInfo: UserInfo): HealthSyncResult + } + + class CheckupQueryUseCase { + -healthRepository: HealthRepository + -normalRangeRepository: NormalRangeRepository + -checkupAnalysisDomainService: CheckupAnalysisDomainService + -cacheAdapter: CacheAdapter + +getHealthCheckupHistory(userId: String, limit: int): HealthHistoryResponse + +getNormalRangesByGender(genderCode: int): NormalRangeResponse + +analyzeHealthTrend(checkupHistory: List): TrendAnalysis + } + + class FileUploadUseCase { + -blobStorageAdapter: BlobStorageAdapter + -healthRepository: HealthRepository + -eventPublisher: EventPublisherAdapter + +uploadCheckupFile(request: CheckupFileRequest): FileUploadResponse + +validateFileFormat(fileType: String, fileContent: String): void + +saveFileMetadata(fileInfo: FileInfo): HealthFile + } + } + + package "domain" { + package "model" { + class HealthCheckup { + +id: Long + +userId: String + +memberSerialNumber: Long + +rawId: Long + +referenceYear: int + +heightCm: double + +weightKg: double + +waistCm: double + +bmi: double + +visualAcuityLeft: double + +visualAcuityRight: double + +hearingAvg: double + +systolicBp: int + +diastolicBp: int + +fastingGlucose: int + +totalCholesterol: int + +triglyceride: int + +hdlCholesterol: int + +ldlCholesterol: int + +hemoglobin: double + +urineProtein: int + +serumCreatinine: double + +ast: int + +alt: int + +gammaGtp: int + +smokingStatus: int + +drinkingStatus: int + +riskLevel: String + +abnormalIndicators: String + +healthScore: int + +createdAt: LocalDateTime + +calculateBMI(): double + +determineRiskLevel(): String + +isNormalBMI(): boolean + +isNormalBloodPressure(): boolean + } + + class HealthCheckupRaw { + +rawId: Long + +memberSerialNumber: Long + +referenceYear: int + +birthDate: LocalDate + +name: String + +regionCode: int + +genderCode: int + +age: int + +height: int + +weight: int + +waistCircumference: int + +visualAcuityLeft: double + +visualAcuityRight: double + +hearingLeft: int + +hearingRight: int + +systolicBp: int + +diastolicBp: int + +fastingGlucose: int + +totalCholesterol: int + +triglyceride: int + +hdlCholesterol: int + +ldlCholesterol: int + +hemoglobin: double + +urineProtein: int + +serumCreatinine: double + +ast: int + +alt: int + +gammaGtp: int + +smokingStatus: int + +drinkingStatus: int + +isValidData(): boolean + +convertToStandardUnits(): HealthCheckupRaw + } + + class NormalRange { + +itemCode: String + +itemName: String + +genderCode: int + +normalMin: double + +normalMax: double + +cautionMin: double + +cautionMax: double + +dangerMin: double + +dangerMax: double + +unit: String + +description: String + +evaluateValue(value: double): RangeStatus + +isApplicableForGender(genderCode: int): boolean + } + + enum RangeStatus { + NORMAL, CAUTION, DANGER, UNKNOWN + } + + class HealthFile { + +fileId: String + +userId: String + +fileName: String + +fileType: String + +fileUrl: String + +fileSize: long + +uploadStatus: String + +uploadedAt: LocalDateTime + +isValidFileType(): boolean + +generateSasUrl(): String + } + } + + package "services" { + class HealthProfileDomainService { + +validateHealthData(checkupData: HealthCheckupRaw): void + +transformRawToCheckup(rawData: HealthCheckupRaw, userInfo: UserInfo): HealthCheckup + +calculateHealthScore(checkup: HealthCheckup, normalRanges: List): int + +determineOverallRiskLevel(indicatorResults: Map): String + } + + class CheckupAnalysisDomainService { + +analyzeWithNormalRanges(checkup: HealthCheckup, normalRanges: List): AnalysisResult + +compareWithNormalRange(value: double, range: NormalRange): RangeStatus + +identifyAbnormalIndicators(analysisResults: Map): List + +calculateTrendAnalysis(checkupHistory: List): TrendAnalysis + +generateHealthInsights(checkup: HealthCheckup): List + } + + class NormalRangeDomainService { + +validateNormalRangeData(range: NormalRange): void + +getRangesByGender(genderCode: int, allRanges: List): List + +parseRangeValue(rangeString: String): RangeValue + +formatRangeDisplay(range: NormalRange): String + } + } + + package "repositories" { + interface HealthRepository { + +findByMemberSerialNumber(memberSerialNumber: Long): Optional + +findCheckupHistoryWithDetails(userId: String, limit: int): List + +saveOrUpdateHealthCheckup(entity: HealthCheckup): HealthCheckup + +findExistingCheckupRecords(userId: String): List + +saveFileMetadata(fileMetadata: HealthFile): HealthFile + +findFilesByUserId(userId: String): List + } + + interface HealthCheckupRawRepository { + +findNhisCheckupDataByMemberSerial(memberSerialNumber: Long): List + +findTop5ByNameAndBirthDateOrderByReferenceYearDescCreatedAtDesc(name: String, birthDate: LocalDate): List + +findByMemberSerialNumberAndReferenceYear(memberSerialNumber: Long, year: int): Optional + } + + interface NormalRangeRepository { + +getNormalRangesByGender(genderCode: int): List + +findByItemCodeAndGender(itemCode: String, genderCode: int): Optional + +findAllActiveRanges(): List + +updateNormalRange(range: NormalRange): NormalRange + } + } + } + + package "infrastructure" { + package "repositories" { + class HealthRepositoryImpl { + -entityManager: EntityManager + +findByMemberSerialNumber(memberSerialNumber: Long): Optional + +findCheckupHistoryWithDetails(userId: String, limit: int): List + +saveOrUpdateHealthCheckup(entity: HealthCheckup): HealthCheckup + +findExistingCheckupRecords(userId: String): List + +saveFileMetadata(fileMetadata: HealthFile): HealthFile + +findFilesByUserId(userId: String): List + } + + class HealthCheckupRawRepositoryImpl { + -entityManager: EntityManager + +findNhisCheckupDataByMemberSerial(memberSerialNumber: Long): List + +findTop5ByNameAndBirthDateOrderByReferenceYearDescCreatedAtDesc(name: String, birthDate: LocalDate): List + +findByMemberSerialNumberAndReferenceYear(memberSerialNumber: Long, year: int): Optional + } + + class NormalRangeRepositoryImpl { + -entityManager: EntityManager + +getNormalRangesByGender(genderCode: int): List + +findByItemCodeAndGender(itemCode: String, genderCode: int): Optional + +findAllActiveRanges(): List + +updateNormalRange(range: NormalRange): NormalRange + } + } + + package "entities" { + class HealthCheckupEntity { + +id: Long + +userId: String + +memberSerialNumber: Long + +rawId: Long + +referenceYear: int + +heightCm: double + +weightKg: double + +waistCm: double + +bmi: double + +visualAcuityLeft: double + +visualAcuityRight: double + +hearingAvg: double + +systolicBp: int + +diastolicBp: int + +fastingGlucose: int + +totalCholesterol: int + +triglyceride: int + +hdlCholesterol: int + +ldlCholesterol: int + +hemoglobin: double + +urineProtein: int + +serumCreatinine: double + +ast: int + +alt: int + +gammaGtp: int + +smokingStatus: int + +drinkingStatus: int + +riskLevel: String + +abnormalIndicators: String + +healthScore: int + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +toDomain(): HealthCheckup + +fromDomain(checkup: HealthCheckup): HealthCheckupEntity + } + + class HealthCheckupRawEntity { + +rawId: Long + +memberSerialNumber: Long + +referenceYear: int + +birthDate: LocalDate + +name: String + +regionCode: int + +genderCode: int + +age: int + +height: int + +weight: int + +waistCircumference: int + +visualAcuityLeft: double + +visualAcuityRight: double + +hearingLeft: int + +hearingRight: int + +systolicBp: int + +diastolicBp: int + +fastingGlucose: int + +totalCholesterol: int + +triglyceride: int + +hdlCholesterol: int + +ldlCholesterol: int + +hemoglobin: double + +urineProtein: int + +serumCreatinine: double + +ast: int + +alt: int + +gammaGtp: int + +smokingStatus: int + +drinkingStatus: int + +createdAt: LocalDateTime + +toDomain(): HealthCheckupRaw + } + + class NormalRangeEntity { + +itemCode: String + +itemName: String + +genderCode: int + +normalMin: double + +normalMax: double + +cautionMin: double + +cautionMax: double + +dangerMin: double + +dangerMax: double + +unit: String + +description: String + +isActive: boolean + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +toDomain(): NormalRange + } + + class HealthFileEntity { + +fileId: String + +userId: String + +fileName: String + +fileType: String + +fileUrl: String + +fileSize: long + +uploadStatus: String + +uploadedAt: LocalDateTime + +toDomain(): HealthFile + } + } + } + + package "dto" { + class HealthSyncResponse { + +syncedRecords: int + +newRecords: int + +updatedRecords: int + +skippedRecords: int + +lastSyncedCheckup: Object + +message: String + } + + class HealthHistoryResponse { + +checkupHistory: List + +totalRecords: int + +averageHealthScore: double + +trendAnalysis: String + +normalRangeReference: Object + } + + class CheckupHistoryDto { + +referenceYear: int + +heightCm: double + +weightKg: double + +waistCm: double + +bmi: double + +systolicBp: int + +diastolicBp: int + +fastingGlucose: int + +totalCholesterol: int + +healthScore: int + +riskLevel: String + +abnormalIndicators: List + +analysisDate: String + } + + class CheckupFileRequest { + +userId: String + +fileName: String + +fileType: String + +fileContent: String + } + + class FileUploadResponse { + +fileId: String + +uploadUrl: String + +status: String + +message: String + } + + class NormalRangeResponse { + +normalRanges: List + +genderCode: int + } + + class NormalRangeDto { + +itemCode: String + +itemName: String + +genderCode: int + +normalMin: double + +normalMax: double + +cautionMin: double + +cautionMax: double + +dangerMin: double + +dangerMax: double + +unit: String + } + } + } +} + +' 관계 설정 +AuthController --> AuthUseCase +AuthController --> UserUseCase +UserController --> UserUseCase +AuthUseCase --> AuthDomainService +AuthUseCase --> UserRepository +UserUseCase --> UserDomainService +UserUseCase --> UserRepository +UserUseCase --> OccupationRepository +AuthDomainService --> GoogleAuthAdapter +HealthController --> CheckupSyncUseCase +HealthController --> CheckupQueryUseCase +HealthController --> FileUploadUseCase +CheckupSyncUseCase --> HealthProfileDomainService +CheckupSyncUseCase --> CheckupAnalysisDomainService +CheckupSyncUseCase --> HealthRepository +CheckupSyncUseCase --> HealthCheckupRawRepository +CheckupSyncUseCase --> NormalRangeRepository +CheckupAnalysisDomainService --> NormalRangeDomainService + + package "goal-service" { + class GoalServiceApplication { + +main(args: String[]): void + } + + package "interface-adapters" { + package "controllers" { + class GoalController { + -goalUseCase: GoalUseCase + +selectMissions(request: MissionSelectionRequest): ResponseEntity + +getActiveMissions(userId: String): ResponseEntity + +completeMission(missionId: String, request: MissionCompleteRequest): ResponseEntity + +getMissionHistory(userId: String, startDate: String, endDate: String, missionIds: String): ResponseEntity + +resetMissions(request: MissionResetRequest): ResponseEntity + } + } + + package "adapters" { + class IntelligenceServiceAdapter { + -intelligenceServiceClient: IntelligenceServiceClient + +requestNewMissionRecommendations(userId: String, reason: String): List + +getMissionRecommendations(userId: String, preferences: List): List + } + + class UserServiceAdapter { + -userServiceClient: UserServiceClient + +getUserInfo(userId: String): UserInfo + +getUserOccupationInfo(userId: String): OccupationInfo + } + + class CacheAdapter { + -redisTemplate: RedisTemplate + +getCachedActiveMissions(userId: String): ActiveMissionsResponse + +cacheActiveMissions(userId: String, response: ActiveMissionsResponse): void + +getCachedMissionHistory(userId: String, period: String): MissionHistoryResponse + +cacheMissionHistory(userId: String, period: String, response: MissionHistoryResponse): void + +invalidateUserCaches(userId: String): void + +invalidateActiveMissionsCache(userId: String): void + } + + class EventPublisherAdapter { + -serviceBusTemplate: ServiceBusTemplate + +publishGoalSetEvent(userId: String, goalId: String, missions: List): void + +publishMissionCompletedEvent(userId: String, missionId: String, achievementData: AchievementData): void + +publishGoalResetEvent(userId: String, reason: String, newRecommendations: List): void + } + } + } + + package "application-services" { + class GoalUseCase { + -goalDomainService: GoalDomainService + -missionProgressDomainService: MissionProgressDomainService + -goalRepository: GoalRepository + -missionProgressRepository: MissionProgressRepository + -intelligenceServiceAdapter: IntelligenceServiceAdapter + -cacheAdapter: CacheAdapter + -eventPublisher: EventPublisherAdapter + +selectMissions(request: MissionSelectionRequest): GoalSetupResponse + +getActiveMissions(userId: String): ActiveMissionsResponse + +completeMission(missionId: String, request: MissionCompleteRequest): MissionCompleteResponse + +getMissionHistory(userId: String, startDate: String, endDate: String, missionIds: String): MissionHistoryResponse + +resetMissions(request: MissionResetRequest): MissionResetResponse + } + } + + package "domain" { + package "model" { + class UserMissionGoal { + +goalId: String + +userId: String + +isActive: boolean + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +activateGoal(): void + +deactivateGoal(): void + } + + class UserMission { + +id: Long + +goalId: String + +userId: String + +missionId: String + +missionTitle: String + +missionDescription: String + +category: String + +difficulty: String + +estimatedTimeMinutes: int + +isActive: boolean + +currentStreakDays: int + +totalCompletedCount: int + +startDate: LocalDate + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +updateStreakDays(newStreak: int): void + +incrementCompletedCount(): void + +resetStreak(): void + } + + class MissionProgress { + +id: Long + +userId: String + +missionId: String + +completedAt: LocalDateTime + +notes: String + +earnedPoints: int + +consecutiveDays: int + +isValidProgress(): boolean + +calculateEarnedPoints(difficulty: String, streak: int): int + } + + class ActiveMission { + +missionId: String + +title: String + +description: String + +status: String + +completedToday: boolean + +streakDays: int + +nextReminderTime: String + +category: String + +difficulty: String + +isCompletedToday(): boolean + +getProgressStatus(): String + } + + class MissionStatistics { + +missionId: String + +title: String + +achievementRate: double + +completedDays: int + +totalDays: int + +bestStreak: int + +averageCompletionTime: String + +calculateAchievementRate(): double + +getBestStreakPeriod(): String + } + + class AchievementData { + +missionId: String + +achievementType: String + +consecutiveDays: int + +totalAchievements: int + +earnedPoints: int + +achievedAt: LocalDateTime + +isSignificantAchievement(): boolean + } + } + + package "services" { + class GoalDomainService { + +validateMissionSelection(userId: String, missionIds: List): void + +checkMaximumMissions(missionIds: List): void + +deactivateExistingGoal(goal: UserMissionGoal): UserMissionGoal + +createNewGoalWithMissions(userId: String, missionIds: List): UserMissionGoal + +generateGoalId(): String + +validateMissionCompletion(userId: String, missionId: String): void + } + + class MissionProgressDomainService { + +calculateMissionProgress(missions: List): List + +calculateCompletionRate(missions: List): double + +calculateStreakDays(missions: List): Map + +determineTodayStatus(missions: List): Map + +recordMissionCompletion(userId: String, missionId: String, completionData: MissionCompleteRequest): MissionProgress + +calculateNewStreakDays(previousProgress: List): int + +calculateEarnedPoints(mission: UserMission, streakDays: int): int + +updateMissionStatistics(userId: String, missionId: String): void + +analyzeAchievementStatistics(historyData: List): MissionStatistics + +generateInsights(statistics: MissionStatistics): List + +prepareChartData(histories: List): Object + } + } + + package "repositories" { + interface GoalRepository { + +findActiveGoalByUserId(userId: String): Optional + +saveGoalWithMissions(goal: UserMissionGoal, missions: List): UserMissionGoal + +updateGoalStatus(goalId: String, isActive: boolean): void + +findActiveMissionsByUserId(userId: String): List + +findUserMission(userId: String, missionId: String): Optional + +updateMissionStats(missionId: String, stats: MissionStatistics): void + } + + interface MissionProgressRepository { + +findTodayProgress(userId: String, missionId: String): Optional + +saveMissionProgress(progress: MissionProgress): MissionProgress + +findMissionHistoryByPeriod(userId: String, startDate: LocalDate, endDate: LocalDate, missionIds: List): List + +findProgressByUserAndMission(userId: String, missionId: String): List + +calculateAchievementRates(userId: String, period: String): Map + } + } + } + + package "infrastructure" { + package "repositories" { + class GoalRepositoryImpl { + -entityManager: EntityManager + +findActiveGoalByUserId(userId: String): Optional + +saveGoalWithMissions(goal: UserMissionGoal, missions: List): UserMissionGoal + +updateGoalStatus(goalId: String, isActive: boolean): void + +findActiveMissionsByUserId(userId: String): List + +findUserMission(userId: String, missionId: String): Optional + +updateMissionStats(missionId: String, stats: MissionStatistics): void + } + + class MissionProgressRepositoryImpl { + -entityManager: EntityManager + +findTodayProgress(userId: String, missionId: String): Optional + +saveMissionProgress(progress: MissionProgress): MissionProgress + +findMissionHistoryByPeriod(userId: String, startDate: LocalDate, endDate: LocalDate, missionIds: List): List + +findProgressByUserAndMission(userId: String, missionId: String): List + +calculateAchievementRates(userId: String, period: String): Map + } + } + + package "entities" { + class UserMissionGoalEntity { + +goalId: String + +userId: String + +isActive: boolean + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +toDomain(): UserMissionGoal + +fromDomain(goal: UserMissionGoal): UserMissionGoalEntity + } + + class UserMissionEntity { + +id: Long + +goalId: String + +userId: String + +missionId: String + +missionTitle: String + +missionDescription: String + +category: String + +difficulty: String + +estimatedTimeMinutes: int + +isActive: boolean + +currentStreakDays: int + +totalCompletedCount: int + +startDate: LocalDate + +createdAt: LocalDateTime + +updatedAt: LocalDateTime + +toDomain(): UserMission + +fromDomain(mission: UserMission): UserMissionEntity + } + + class MissionProgressEntity { + +id: Long + +userId: String + +missionId: String + +completedAt: LocalDateTime + +notes: String + +earnedPoints: int + +consecutiveDays: int + +createdAt: LocalDateTime + +toDomain(): MissionProgress + +fromDomain(progress: MissionProgress): MissionProgressEntity + } + } + } + + package "dto" { + class MissionSelectionRequest { + +userId: String + +selectedMissionIds: List + } + + class GoalSetupResponse { + +goalId: String + +selectedMissions: List + +message: String + +setupCompletedAt: String + } + + class SelectedMissionDto { + +missionId: String + +title: String + +description: String + +startDate: String + } + + class ActiveMissionsResponse { + +dailyMissions: List + +totalMissions: int + +todayCompletedCount: int + +completionRate: double + } + + class ActiveMissionDto { + +missionId: String + +title: String + +description: String + +status: String + +completedToday: boolean + +streakDays: int + +nextReminderTime: String + } + + class MissionCompleteRequest { + +userId: String + +completed: boolean + +completedAt: String + +notes: String + } + + class MissionCompleteResponse { + +message: String + +status: String + +achievementMessage: String + +newStreakDays: int + +totalCompletedCount: int + +earnedPoints: int + } + + class MissionHistoryResponse { + +totalAchievementRate: double + +periodAchievementRate: double + +bestStreak: int + +missionStats: List + +chartData: Object + +period: PeriodDto + +insights: List + } + + class MissionStatDto { + +missionId: String + +title: String + +achievementRate: double + +completedDays: int + +totalDays: int + } + + class PeriodDto { + +startDate: String + +endDate: String + } + + class MissionResetRequest { + +userId: String + +reason: String + +currentMissionIds: List + } + + class MissionResetResponse { + +message: String + +newRecommendations: List + +resetCompletedAt: String + } + + class MissionRecommendationDto { + +missionId: String + +title: String + +description: String + +category: String + } + } + } + + package "intelligence-service" { + class IntelligenceServiceApplication { + +main(args: String[]): void + } + + package "interface-adapters" { + package "controllers" { + class AnalysisController { + -healthAnalysisUseCase: HealthAnalysisUseCase + +generateHealthDiagnosis(userId: String): ResponseEntity + +recommendMissions(request: MissionRecommendationRequest): ResponseEntity + } + + class ChatController { + -chatUseCase: ChatUseCase + +processChat(request: ChatRequest): ResponseEntity + +getChatHistory(sessionId: String, messageLimit: int): ResponseEntity + } + + class NotificationController { + -notificationUseCase: NotificationUseCase + +generateCelebrationMessage(request: CelebrationRequest): ResponseEntity + +generateEncouragementMessage(request: EncouragementRequest): ResponseEntity + } + + class BatchController { + -batchUseCase: BatchUseCase + +processBatchNotifications(request: BatchNotificationRequest): ResponseEntity + } + } + + package "adapters" { + class HealthServiceAdapter { + -healthServiceClient: HealthServiceClient + +getLatestHealthCheckup(userId: String): HealthCheckupData + +getHealthAnalysisData(userId: String): HealthAnalysisData + +getHealthTrendData(userId: String): HealthTrendData + } + + class UserServiceAdapter { + -userServiceClient: UserServiceClient + +getUserProfile(userId: String): UserProfile + +getUserOccupationInfo(userId: String): OccupationInfo + } + + class GoalServiceAdapter { + -goalServiceClient: GoalServiceClient + +getMissionDetails(missionId: String): MissionDetails + +getUserDailyProgress(userId: String): DailyProgress + +getAllUsersProgress(targetUsers: List): Map + } + + class ClaudeApiAdapter { + -claudeApiClient: ClaudeApiClient + -circuitBreaker: CircuitBreaker + +requestHealthDiagnosis(prompt: String): String + +requestMissionRecommendations(prompt: String): String + +requestChatResponse(prompt: String): String + +requestCelebrationMessage(prompt: String): String + +requestEncouragementMessage(prompt: String): String + +requestBatchNotification(prompt: String): String + } + + class CacheAdapter { + -redisTemplate: RedisTemplate + +getCachedDiagnosis(userId: String): HealthDiagnosisResponse + +cacheDiagnosis(userId: String, response: HealthDiagnosisResponse): void + +getCachedChatHistory(sessionId: String): List + +cacheChatHistory(sessionId: String, history: List): void + +getCachedEncouragementMessage(userId: String, progressLevel: String): String + +cacheEncouragementMessage(userId: String, progressLevel: String, message: String): void + +invalidateUserCaches(userId: String): void + } + + class EventPublisherAdapter { + -serviceBusTemplate: ServiceBusTemplate + +publishHealthAnalysisEvent(userId: String, analysisResult: AnalysisResult): void + +publishChatSessionEvent(sessionId: String, eventType: String): void + +publishBatchNotificationEvents(notifications: List): void + } + } + } + + package "application-services" { + class HealthAnalysisUseCase { + -aiAnalysisDomainService: AiAnalysisDomainService + -healthServiceAdapter: HealthServiceAdapter + -userServiceAdapter: UserServiceAdapter + -claudeApiAdapter: ClaudeApiAdapter + -cacheAdapter: CacheAdapter + +generateHealthDiagnosis(userId: String): HealthDiagnosisResponse + +recommendMissions(request: MissionRecommendationRequest): MissionRecommendationResponse + } + + class ChatUseCase { + -chatDomainService: ChatDomainService + -chatHistoryRepository: ChatHistoryRepository + -claudeApiAdapter: ClaudeApiAdapter + -cacheAdapter: CacheAdapter + +processChat(request: ChatRequest): ChatResponse + +getChatHistory(sessionId: String, messageLimit: int): ChatHistoryResponse + } + + class NotificationUseCase { + -notificationDomainService: NotificationDomainService + -goalServiceAdapter: GoalServiceAdapter + -claudeApiAdapter: ClaudeApiAdapter + -cacheAdapter: CacheAdapter + +generateCelebrationMessage(request: CelebrationRequest): CelebrationResponse + +generateEncouragementMessage(request: EncouragementRequest): EncouragementResponse + } + + class BatchUseCase { + -notificationDomainService: NotificationDomainService + -goalServiceAdapter: GoalServiceAdapter + -claudeApiAdapter: ClaudeApiAdapter + -eventPublisher: EventPublisherAdapter + +processBatchNotifications(request: BatchNotificationRequest): BatchNotificationResponse + } + } + + package "domain" { + package "model" { + class AiDiagnosis { + +userId: String + +healthScore: int + +riskLevel: String + +threeSentenceSummary: List + +occupationConsiderations: String + +analysisTimestamp: LocalDateTime + +confidenceScore: double + +isHighRisk(): boolean + +requiresImmediateAttention(): boolean + } + + class ChatSession { + +sessionId: String + +userId: String + +status: SessionStatus + +createdAt: LocalDateTime + +lastActivityAt: LocalDateTime + +messageCount: int + +context: String + +isActive(): boolean + +updateLastActivity(): void + +incrementMessageCount(): void + } + + class ChatMessage { + +messageId: String + +sessionId: String + +role: MessageRole + +content: String + +timestamp: LocalDateTime + +messageOrder: int + +isUserMessage(): boolean + +isAssistantMessage(): boolean + +maskSensitiveInfo(): ChatMessage + } + + class Mission { + +missionId: String + +title: String + +description: String + +category: MissionCategory + +difficulty: DifficultyLevel + +healthBenefit: String + +occupationRelevance: String + +estimatedTimeMinutes: int + +isApplicableForOccupation(occupation: String): boolean + +calculateDifficultyScore(): int + } + + class AnalysisResult { + +userId: String + +analysisType: String + +result: String + +confidence: double + +createdAt: LocalDateTime + +metadata: Map + +isHighConfidence(): boolean + +getFormattedResult(): String + } + + enum SessionStatus { + ACTIVE, INACTIVE, EXPIRED, TERMINATED + } + + enum MessageRole { + USER, ASSISTANT, SYSTEM + } + + enum MissionCategory { + EXERCISE, NUTRITION, STRESS_MANAGEMENT, SLEEP, PREVENTIVE_CARE + } + + enum DifficultyLevel { + EASY, MEDIUM, HARD + } + } + + package "services" { + class AiAnalysisDomainService { + +createDiagnosisPrompt(healthData: HealthCheckupData, userProfile: UserProfile): String + +formatHealthDataForAI(checkupData: HealthCheckupData): String + +parseAndValidateAIResponse(aiResponse: String): AiDiagnosis + +calculateConfidenceScore(healthData: HealthCheckupData, aiResponse: String): double + +generateMissionRecommendations(healthData: HealthAnalysisData, occupation: String, preferences: List): List + +createPersonalizedPrompt(data: MissionRecommendationRequest): String + +parseMissionRecommendations(aiResponse: String): List + +validateAndEnrichMissions(missions: List): List + } + + class ChatDomainService { + +validateChatSession(sessionId: String): ChatSession + +createNewChatSession(userId: String): ChatSession + +buildContextualPrompt(userMessage: String, chatHistory: List, context: String): String + +formatChatContext(history: List): String + +saveChatExchange(sessionId: String, userMessage: String, aiResponse: String): void + +formatChatHistory(rawHistory: List): List + +maskSensitiveInfo(messages: List): List + } + + class NotificationDomainService { + +createCelebrationMessage(celebrationData: CelebrationRequest): String + +prepareCelebrationPrompt(data: CelebrationRequest): String + +enhanceCelebrationMessage(aiMessage: String, achievementData: AchievementData): CelebrationResponse + +analyzeProgressPattern(userId: String, missionsStatus: List, dailyProgress: DailyProgress): ProgressAnalysis + +calculateProgressLevel(data: ProgressAnalysis): String + +identifyFailurePoints(missionsStatus: List): List + +createEncouragementPrompt(progressAnalysis: ProgressAnalysis): String + +generatePersonalizedNotification(user: UserInfo, progress: DailyProgress): String + } + } + + package "repositories" { + interface ChatHistoryRepository { + +findChatSession(sessionId: String): Optional + +saveChatSession(session: ChatSession): ChatSession + +getChatHistory(sessionId: String, limit: int): List + +saveChatMessages(sessionId: String, messages: List): void + +findChatHistoryBySession(sessionId: String, messageLimit: int): List + +updateSessionActivity(sessionId: String): void + } + + interface AnalysisRepository { + +saveAnalysisResult(result: AnalysisResult): AnalysisResult + +findAnalysisHistoryByUser(userId: String): List + +findLatestAnalysis(userId: String, analysisType: String): Optional + +deleteOldAnalysisResults(beforeDate: LocalDateTime): void + } + } + } + + package "infrastructure" { + package "repositories" { + class ChatHistoryRepositoryImpl { + -entityManager: EntityManager + +findChatSession(sessionId: String): Optional + +saveChatSession(session: ChatSession): ChatSession + +getChatHistory(sessionId: String, limit: int): List + +saveChatMessages(sessionId: String, messages: List): void + +findChatHistoryBySession(sessionId: String, messageLimit: int): List + +updateSessionActivity(sessionId: String): void + } + + class AnalysisRepositoryImpl { + -entityManager: EntityManager + +saveAnalysisResult(result: AnalysisResult): AnalysisResult + +findAnalysisHistoryByUser(userId: String): List + +findLatestAnalysis(userId: String, analysisType: String): Optional + +deleteOldAnalysisResults(beforeDate: LocalDateTime): void + } + } + + package "entities" { + class ChatSessionEntity { + +sessionId: String + +userId: String + +status: String + +createdAt: LocalDateTime + +lastActivityAt: LocalDateTime + +messageCount: int + +context: String + +toDomain(): ChatSession + +fromDomain(session: ChatSession): ChatSessionEntity + } + + class ChatMessageEntity { + +messageId: String + +sessionId: String + +role: String + +content: String + +timestamp: LocalDateTime + +messageOrder: int + +toDomain(): ChatMessage + +fromDomain(message: ChatMessage): ChatMessageEntity + } + + class AnalysisResultEntity { + +id: Long + +userId: String + +analysisType: String + +result: String + +confidence: double + +createdAt: LocalDateTime + +metadata: String + +toDomain(): AnalysisResult + +fromDomain(result: AnalysisResult): AnalysisResultEntity + } + } + } + + package "dto" { + class HealthDiagnosisResponse { + +threeSentenceSummary: List + +healthScore: int + +riskLevel: String + +occupationConsiderations: String + +analysisTimestamp: String + +confidenceScore: double + } + + class MissionRecommendationRequest { + +userId: String + +currentHealthStatus: String + +preferences: List + } + + class MissionRecommendationResponse { + +missions: List + +recommendationReason: String + +totalRecommended: int + } + + class MissionDto { + +missionId: String + +title: String + +description: String + +category: String + +difficulty: String + +healthBenefit: String + +occupationRelevance: String + +estimatedTimeMinutes: int + } + + class ChatRequest { + +message: String + +sessionId: String + +context: String + } + + class ChatResponse { + +response: String + +sessionId: String + +timestamp: String + +suggestedQuestions: List + +responseType: String + } + + class ChatHistoryResponse { + +sessionId: String + +messages: List + +totalMessageCount: int + +cacheExpiration: String + } + + class ChatMessageDto { + +role: String + +content: String + +timestamp: String + } + + class CelebrationRequest { + +userId: String + +missionId: String + +achievementType: String + +consecutiveDays: int + +totalAchievements: int + } + + class CelebrationResponse { + +congratsMessage: String + +achievementBadge: String + +healthBenefit: String + +nextMilestone: String + +encouragementLevel: String + +visualEffect: String + } + + class EncouragementRequest { + +userId: String + +missionsStatus: List + } + + class MissionStatusDto { + +missionId: String + +completed: boolean + } + + class EncouragementResponse { + +message: String + +motivationType: String + +timing: String + +personalizedTip: String + +priority: String + } + + class BatchNotificationRequest { + +triggerTime: String + +targetUsers: List + +notificationType: String + } + + class BatchNotificationResponse { + +processedCount: int + +successCount: int + +failedCount: int + +nextScheduledTime: String + } + } + } +} \ No newline at end of file