Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions AIProject/iCo/Core/Local/UserDefaults/AppStorageKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ enum AppStorageKey {
static let cacheCoinRecomURL = "cacheCoinRecomURL"
/// AI 추천 코인 URL 캐시 시각 저장 키
static let cacheCoinRecomTimestamp = "cacheCoinRecomTimestamp"
/// 오늘의 브리핑 캐시 시각 저장 키
static let cacheBriefTodayTimestamp = "cacheBriefTodayTimestamp"
/// 커뮤니티 브리핑 캐시 시각 저장 키
static let cacheBriefCommunityTimestamp = "cacheBriefCommunityTimestamp"
/// 위젯에 북마크된 데이터 저장 키
Expand Down
59 changes: 0 additions & 59 deletions AIProject/iCo/Data/API/Gemini/LLMAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`)과 요약을 생성합니다.
/// 캐시가 유효하면 캐시를 우선 사용하고, 없거나 만료되면 새로 요청 후 캐싱합니다.
///
Expand Down
1 change: 0 additions & 1 deletion AIProject/iCo/Domain/Interface/LLMProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
14 changes: 1 addition & 13 deletions AIProject/iCo/Domain/Model/Common/Prompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -97,7 +85,7 @@ enum Prompt {
let summary: String
}

커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외)
커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 형식으로 작성해서 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외)
"""
case .generateBookmarkBriefing(let importance, let bookmarks):
"""
Expand Down
2 changes: 1 addition & 1 deletion AIProject/iCo/Features/Dashboard/View/AIBriefingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SwiftUI

/// 대시보드에서 AI 브리핑 섹션을 보여주는 뷰입니다.
///
/// 오늘의 인사이트, 커뮤니티 반응, 공포 탐욕 지수으로 구성합니다.
/// 오늘의 커뮤니티 반응, 공포 탐욕 지수, 거래대금/상승률 TOP5로 구성합니다.
struct AIBriefingView: View {
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,21 @@

import SwiftUI

/// 오늘의 코인 시장/커뮤니티 분위기를 제공하는 뷰 모델입니다.
/// 오늘의 코인 커뮤니티 분위기를 제공하는 뷰 모델입니다.
///
/// AI 또는 커뮤니티 기반의 분위기를 비동기적으로 불러오고,
/// 감정(`Sentiment`)과 요약(`summary`)을 제공합니다.
///
/// - Properties:
/// - overall: 오늘의 전체 시장 분위기(`FetchState<Insight>`)
/// - community: 커뮤니티 기반 분위기(`FetchState<Insight>`)
final class InsightViewModel: ObservableObject {
@AppStorage(AppStorageKey.cacheBriefTodayTimestamp) private var cacheBriefTodayTimestamp: String = ""
@AppStorage(AppStorageKey.cacheBriefCommunityTimestamp) private var cacheBriefCommunityTimestamp: String = ""

@Published var overall: FetchState<Insight> = .loading
@Published var community: FetchState<Insight> = .loading

private let llmService = LLMAPIService()
private let redditAPIService = RedditAPIService()

private var overallTask: Task<Insight, Error>?
private var communityTask: Task<Insight, Error>?

init() {
Expand All @@ -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: {
Expand All @@ -58,8 +49,6 @@ final class InsightViewModel: ObservableObject {
}

Task {
await updateOverallUI()
try? await Task.sleep(for: .milliseconds(350)) // UI가 순차적으로 적용되는 효과를 주기 위한 딜레이
await updateCommunityUI()
}
}
Expand All @@ -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 }
Expand All @@ -100,16 +75,11 @@ final class InsightViewModel: ObservableObject {
}
}

func cancelOverall() {
overallTask?.cancel()
}

func cancelCommunity() {
communityTask?.cancel()
}

func cancelAll() {
overallTask?.cancel()
communityTask?.cancel()
}

Expand All @@ -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,
Expand All @@ -141,15 +102,6 @@ extension InsightViewModel {
extension InsightViewModel {
var sectionDataSource: [ReportSectionData<Insight>] {
[
// 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",
Expand Down
Loading