diff --git a/AIProject/iCo/Core/Local/UserDefaults/AppStorageKey.swift b/AIProject/iCo/Core/Local/UserDefaults/AppStorageKey.swift index bb2ee99e..9de59bf1 100644 --- a/AIProject/iCo/Core/Local/UserDefaults/AppStorageKey.swift +++ b/AIProject/iCo/Core/Local/UserDefaults/AppStorageKey.swift @@ -22,8 +22,6 @@ enum AppStorageKey { static let cacheCoinRecomURL = "cacheCoinRecomURL" /// AI 추천 코인 URL 캐시 시각 저장 키 static let cacheCoinRecomTimestamp = "cacheCoinRecomTimestamp" - /// 오늘의 브리핑 캐시 시각 저장 키 - static let cacheBriefTodayTimestamp = "cacheBriefTodayTimestamp" /// 커뮤니티 브리핑 캐시 시각 저장 키 static let cacheBriefCommunityTimestamp = "cacheBriefCommunityTimestamp" /// 위젯에 북마크된 데이터 저장 키 diff --git a/AIProject/iCo/Data/API/Gemini/LLMAPIService.swift b/AIProject/iCo/Data/API/Gemini/LLMAPIService.swift index 8470ac17..80e22611 100644 --- a/AIProject/iCo/Data/API/Gemini/LLMAPIService.swift +++ b/AIProject/iCo/Data/API/Gemini/LLMAPIService.swift @@ -179,65 +179,6 @@ extension LLMAPIService { return try await fetchDTO(prompt: prompt, action: .coinReportGeneration) } - /// 2시간 단위 전체 시장 요약 데이터를 가져옵니다. - /// 캐시가 유효하면 캐시를 우선 사용하고, 없거나 만료되면 새로 요청 후 캐싱합니다. - /// - /// - Parameter ignoreCache: 캐시 여부 - /// - Returns: 디코딩된 DTO - func fetchTodayInsight(ignoreCache: Bool = false) async throws -> Insight { - let now = Date.now - let interval: TimeInterval = 60 * 60 - - if !ignoreCache { - if let lastTimestamp = UserDefaults.standard.value(forKey: AppStorageKey.cacheBriefTodayTimestamp) as? String, let savedDate = Date.dateAndTimeFormatter.date(from: lastTimestamp) { - let cacheURL = URL(string: "https://cache.local/dashboard/today/\(lastTimestamp)")! - let request = URLRequest(url: cacheURL, cachePolicy: .returnCacheDataElseLoad) - - if let cachedResponse = URLCache.shared.cachedResponse(for: request), - now.timeIntervalSince(savedDate) < interval { - do { - let dto: InsightDTO = try JSONDecoder().decode(InsightDTO.self, from: cachedResponse.data) - return dto.toDomain() - } catch let decodingError as DecodingError { - throw NetworkError.decodingError(decodingError) - } - - } - } - } - - let cacheURL = URL(string: "https://cache.local/dashboard/today/\(now.dateAndTime)")! - let request = URLRequest(url: cacheURL, cachePolicy: .returnCacheDataElseLoad) - - let prompt = Prompt.generateTodayInsight() - let dto: InsightDTO = try await fetchDTO(prompt: prompt, action: .dashboardBriefingGeneration) - - do { - let jsonData = try JSONEncoder().encode(dto) - - let response = URLResponse( - url: cacheURL, - mimeType: "application/json", - expectedContentLength: jsonData.count, - textEncodingName: "utf-8" - ) - let cacheEntry = CachedURLResponse(response: response, data: jsonData) - URLCache.shared.storeCachedResponse(cacheEntry, for: request) - } catch { - throw NetworkError.encodingError - } - - if let lastTimestamp = UserDefaults.standard.value(forKey: AppStorageKey.cacheBriefTodayTimestamp) as? String { - let oldCacheURL = URL(string: "https://cache.local/dashboard/today/\(lastTimestamp)")! - let oldRequest = URLRequest(url: oldCacheURL, cachePolicy: .returnCacheDataElseLoad) - URLCache.shared.removeCachedResponse(for: oldRequest) - } - - UserDefaults.standard.set(now.dateAndTime, forKey: AppStorageKey.cacheBriefTodayTimestamp) - - return dto.toDomain() - } - /// 커뮤니티(예: Reddit) 게시글 요약을 기반으로 감정(`Sentiment`)과 요약을 생성합니다. /// 캐시가 유효하면 캐시를 우선 사용하고, 없거나 만료되면 새로 요청 후 캐싱합니다. /// diff --git a/AIProject/iCo/Domain/Interface/LLMProvider.swift b/AIProject/iCo/Domain/Interface/LLMProvider.swift index d6e15cf3..e8bf42a4 100644 --- a/AIProject/iCo/Domain/Interface/LLMProvider.swift +++ b/AIProject/iCo/Domain/Interface/LLMProvider.swift @@ -20,7 +20,6 @@ protocol LLMRecommendCoinFetching { protocol LLMProvider: LLMReportFetching, LLMRecommendCoinFetching { func postAnswer(content: String, action: LLMAction) async throws -> LLMResponseDTO func fetchRecommendCoins(preference: String, bookmarkCoins: String, ignoreCache: Bool) async throws -> [RecommendCoinDTO] - func fetchTodayInsight(ignoreCache: Bool) async throws -> Insight func fetchCommunityInsight(from post: String, now: Date, ignoreCache: Bool) async throws -> Insight func fetchBookmarkBriefing(for coins: [BookmarkEntity], character: RiskTolerance) async throws -> PortfolioBriefingDTO } diff --git a/AIProject/iCo/Domain/Model/Common/Prompt.swift b/AIProject/iCo/Domain/Model/Common/Prompt.swift index 33fc9a7b..acdd698a 100644 --- a/AIProject/iCo/Domain/Model/Common/Prompt.swift +++ b/AIProject/iCo/Domain/Model/Common/Prompt.swift @@ -14,7 +14,6 @@ enum Prompt { case generateTodayNews(coinKName: String, today: String = Date().dateAndTime) case generateWeeklyTrends(coinKName: String, today: String = Date().dateAndTime) case extractCoinID(text: String) - case generateTodayInsight(today: String = Date().dateAndTime) case generateCommunityInsight(redditPost: String) case generateBookmarkBriefing(importance: String, bookmarks: String) @@ -76,17 +75,6 @@ enum Prompt { 아래의 문자열에서 가상화폐를 찾아. 빈 배열에 모든 화폐의 심볼을 담고 “,” 로 구분해서 반환해. 응답에 다른 설명은 배제해. \(text) """ - case.generateTodayInsight(let today): - """ - struct InsightDTO: Codable { - let todaysSentiment: String - let summary: String - } - - \(today) 기준 최근 2시간동안 한국 암호화폐 뉴스 분석 후 분위기(호재, 악재, 중립)와 그렇게 판단한 이유 200자로 요약 - 이유는 호재라면 긍정 요인, 악재라면 부정 요인만 요약, 중립이라면 긍정, 부정 요인을 자연스럽게 연결해 요약 - 위 JSON 형식으로 작성 (답변은 한글, 마크다운 금지, 출처 제외) - """ case.generateCommunityInsight(let redditPost): """ \(redditPost) @@ -97,7 +85,7 @@ enum Prompt { let summary: String } - 커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외) + 커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 형식으로 작성해서 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외) """ case .generateBookmarkBriefing(let importance, let bookmarks): """ diff --git a/AIProject/iCo/Features/Dashboard/View/AIBriefingView.swift b/AIProject/iCo/Features/Dashboard/View/AIBriefingView.swift index 63064818..49c15f68 100644 --- a/AIProject/iCo/Features/Dashboard/View/AIBriefingView.swift +++ b/AIProject/iCo/Features/Dashboard/View/AIBriefingView.swift @@ -9,7 +9,7 @@ import SwiftUI /// 대시보드에서 AI 브리핑 섹션을 보여주는 뷰입니다. /// -/// 오늘의 인사이트, 커뮤니티 반응, 공포 탐욕 지수으로 구성합니다. +/// 오늘의 커뮤니티 반응, 공포 탐욕 지수, 거래대금/상승률 TOP5로 구성합니다. struct AIBriefingView: View { @Environment(\.horizontalSizeClass) var hSizeClass @Environment(\.verticalSizeClass) var vSizeClass diff --git a/AIProject/iCo/Features/Dashboard/View/TopCoinListView.swift b/AIProject/iCo/Features/Dashboard/View/TopCoinListView.swift index 6399963d..016568e0 100644 --- a/AIProject/iCo/Features/Dashboard/View/TopCoinListView.swift +++ b/AIProject/iCo/Features/Dashboard/View/TopCoinListView.swift @@ -103,7 +103,7 @@ struct TopCoinListSection: View { CoinView(symbol: coin.coinSymbol, size: 40) .padding(.trailing, 8) - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 4) { Text(viewModel.koreanName(for: coin.id)) .font(.ico16Sb) .lineLimit(1) diff --git a/AIProject/iCo/Features/Dashboard/ViewModel/InsightViewModel.swift b/AIProject/iCo/Features/Dashboard/ViewModel/InsightViewModel.swift index 0d2c6c97..c4358cf0 100644 --- a/AIProject/iCo/Features/Dashboard/ViewModel/InsightViewModel.swift +++ b/AIProject/iCo/Features/Dashboard/ViewModel/InsightViewModel.swift @@ -7,25 +7,21 @@ import SwiftUI -/// 오늘의 코인 시장/커뮤니티 분위기를 제공하는 뷰 모델입니다. +/// 오늘의 코인 커뮤니티 분위기를 제공하는 뷰 모델입니다. /// /// AI 또는 커뮤니티 기반의 분위기를 비동기적으로 불러오고, /// 감정(`Sentiment`)과 요약(`summary`)을 제공합니다. /// /// - Properties: -/// - overall: 오늘의 전체 시장 분위기(`FetchState`) /// - community: 커뮤니티 기반 분위기(`FetchState`) final class InsightViewModel: ObservableObject { - @AppStorage(AppStorageKey.cacheBriefTodayTimestamp) private var cacheBriefTodayTimestamp: String = "" @AppStorage(AppStorageKey.cacheBriefCommunityTimestamp) private var cacheBriefCommunityTimestamp: String = "" - @Published var overall: FetchState = .loading @Published var community: FetchState = .loading private let llmService = LLMAPIService() private let redditAPIService = RedditAPIService() - private var overallTask: Task? private var communityTask: Task? init() { @@ -36,14 +32,9 @@ final class InsightViewModel: ObservableObject { cancelAll() Task { @MainActor in - overall = .loading community = .loading } - overallTask = Task { - try await llmService.fetchTodayInsight() - } - communityTask = Task { [weak self] in try await withTaskCancellationHandler( operation: { @@ -58,8 +49,6 @@ final class InsightViewModel: ObservableObject { } Task { - await updateOverallUI() - try? await Task.sleep(for: .milliseconds(350)) // UI가 순차적으로 적용되는 효과를 주기 위한 딜레이 await updateCommunityUI() } } @@ -71,20 +60,6 @@ final class InsightViewModel: ObservableObject { return try await llmService.fetchCommunityInsight(from: communityData.communitySummary, ignoreCache: ignoreCache) } - // overall만 다시 시도 - func retryOverall() { - if overall.isLoading { return } - overallTask?.cancel() - overallTask = nil - - Task { - await MainActor.run { self.overall = .loading } - try? await Task.sleep(for: .milliseconds(350)) // 새로고침 효과를 주기 위한 딜레이 - overallTask = Task { try await llmService.fetchTodayInsight(ignoreCache: true) } - await updateOverallUI() - } - } - // community만 다시 시도 func retryCommunity() { if community.isLoading { return } @@ -100,16 +75,11 @@ final class InsightViewModel: ObservableObject { } } - func cancelOverall() { - overallTask?.cancel() - } - func cancelCommunity() { communityTask?.cancel() } func cancelAll() { - overallTask?.cancel() communityTask?.cancel() } @@ -119,15 +89,6 @@ final class InsightViewModel: ObservableObject { } extension InsightViewModel { - private func updateOverallUI() async { - await TaskResultHandler.apply( - of: overallTask, - update: { [weak self] state in - self?.overall = state - } - ) - } - private func updateCommunityUI() async { await TaskResultHandler.apply( of: communityTask, @@ -141,15 +102,6 @@ extension InsightViewModel { extension InsightViewModel { var sectionDataSource: [ReportSectionData] { [ -// ReportSectionData( -// id: "overall", -// icon: "bitcoinsign.bank.building", -// title: "전반적인 시장의 분위기", -// state: overall, -// timestamp: Date.dateAndTimeFormatter.date(from: cacheBriefTodayTimestamp), -// onCancel: { [weak self] in self?.cancelOverall() }, -// onRetry: { [weak self] in self?.retryOverall() } -// ), ReportSectionData( id: "community", icon: "shareplay",