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: 1 addition & 1 deletion AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// 업비트 API 관련 서비스를 제공합니다.
final class UpBitAPIService: UpBitApiServiceProtocol {
final class UpBitAPIService: UpBitAPIServiceProtocol {
private let network: NetworkClient

init(networkClient: NetworkClient = .init()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

protocol UpBitApiServiceProtocol {
protocol UpBitAPIServiceProtocol {
func fetchQuotes(id: String) async throws -> [TickerDTO]
func fetchCandles(id: String, count: Int, to: Date?) async throws -> [MinuteCandleDTO]
}
4 changes: 2 additions & 2 deletions AIProject/iCo/Domain/Model/Common/Prompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ enum Prompt {
struct RecommendCoinDTO: Codable {
let name: String
let symbol: String
let comment: String // \(preference)인 투자자에게 추천하는 이유와 최근 동향을 100자 정도의 대화형으로 작성
let comment: String // \(preference)인 투자자에게 추천하는 이유와 최근 동향을 200자 정도의 대화형으로 작성
}
"""
case .generateOverView(let coinKName):
Expand Down Expand Up @@ -97,7 +97,7 @@ enum Prompt {
let summary: String
}

커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외)
커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외)
"""
case .generateBookmarkBriefing(let importance, let bookmarks):
"""
Expand Down
1 change: 1 addition & 0 deletions AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ struct RecommendCoin: Identifiable, Hashable {
let tradePrice: Double
let changeRate: Double
let changeType: TickerChangeType
let candles: [Double]
}
2 changes: 1 addition & 1 deletion AIProject/iCo/Features/Dashboard/CardConst.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum CardConst {
static let headerHeight: CGFloat = 140
static let headerContentSpacing: CGFloat = 30

static let cardHeight: CGFloat = 326
static let cardHeight: CGFloat = 250
static let cardHeightMultiplier: CGFloat = 0.9
static let cardInnerPadding: CGFloat = 16

Expand Down
128 changes: 100 additions & 28 deletions AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ struct CoinCarouselView: View {
var tempCoinArray: [RecommendCoin] {
wrappedCoins.flatMap { $0.map { $0 }}
}

@State private var showNewBadge = false

@State private var showDetailCoin: RecommendCoin?
@State private var measuredHeight: CGFloat = 0
private var detent: PresentationDetent { .height(measuredHeight) }
@State private var isSheetPresented = false

var body: some View {
/// 화면의 가로 크기에 따라 카드 갯수를 관리하는 computed property
var numberOfColumn: Int { hSizeClass == .regular ? 2 : 1 }
Expand All @@ -59,7 +62,9 @@ struct CoinCarouselView: View {
width: nil,
height: CardConst.cardHeight
)
.onTapGesture { selectedCoin = coin }
.onTapGesture {
selectedCoin = coin
}
.scrollTransition(axis: .horizontal) { content, phase in // 활성화된 코인은 크게 보이게 하기
content.scaleEffect(
y: phase.isIdentity ? 1 : CardConst.cardHeightMultiplier,
Expand Down Expand Up @@ -116,31 +121,6 @@ struct CoinCarouselView: View {
else { return }
handleManualScrolling(cardID: newValue)
}
.sheet(item: $selectedCoin) { coin in
VStack(spacing: 0) {
ZStack(alignment: .center) {
HeaderView(
heading: coin.name,
topPadding: 20,
coinSymbol: coin.id,
showBackButton: false,
showNewBadge: showNewBadge
)
.toolbar(.hidden, for: .navigationBar)

RoundedButton(imageName: "xmark") {
selectedCoin = nil
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
}

CoinDetailView(coin: Coin(id: "KRW-" + coin.id, koreanName: coin.name, imageURL: coin.imageURL)) { isNew in
showNewBadge = isNew
}
}
.background(.background)
}
.onAppear {
// 무한 스크롤링 효과를 구현하기 위해 추천 코인 배열의 앞 뒤에 안전 코인을 붙이기
wrappedCoins = [recommendedCoins, recommendedCoins, recommendedCoins]
Expand All @@ -161,6 +141,80 @@ struct CoinCarouselView: View {
viewModel.stopTimer()
wrappedCoins.removeAll()
}
.sheet(item: $selectedCoin) { coin in
VStack(spacing: .spacing) {
CoinInfoView(recommendCoin: coin) {
selectedCoin = nil
}

VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Image(systemName: "sparkles")
.font(.ico14B)
.foregroundStyle(.iCoAccent)

Text("아이코가 추천하는 이유")
.font(.ico16B)
.foregroundStyle(.iCoLabel)
}

Text(String.aiGeneratedContentNotice)
.font(.ico11)
.foregroundStyle(.iCoNeutral)
.lineSpacing(5)

Text(coin.comment.byCharWrapping)
.font(.ico14)
.lineSpacing(6)
.foregroundStyle(.iCoLabel)
}
.fixedSize(horizontal: false, vertical: true)

RoundedRectangleFillButton(title: "더 자세히 보러가기", imageName: "info.circle", isHighlighted: .constant(true)) {
selectedCoin = nil
showDetailCoin = coin
}
}
.padding(20)
.background(.background)
.background(
GeometryReader { geo in
Color.clear
.onAppear {
measuredHeight = geo.size.height
}
}
)
.presentationDetents([detent])
}
.onChange(of: selectedCoin) { _, newValue in
updateTimerState()
}
.sheet(item: $showDetailCoin) { detail in
ZStack(alignment: .center) {
HeaderView(
heading: detail.name,
topPadding: 20,
coinSymbol: detail.id,
showBackButton: false,
showNewBadge: showNewBadge
)
.toolbar(.hidden, for: .navigationBar)

RoundedButton(imageName: "xmark") {
showDetailCoin = nil
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
}

CoinDetailView(coin: Coin(id: "KRW-" + detail.id, koreanName: detail.name, imageURL: detail.imageURL)) { isNew in
showNewBadge = isNew
}
}
.onChange(of: showDetailCoin) { _, newValue in
updateTimerState()
}
}
}

Expand Down Expand Up @@ -230,6 +284,24 @@ extension CoinCarouselView {
viewModel.startTimer()
}
}

/// sheet가 보여지고 있는 경우에는 스크롤 타이머가 멈출 수 있도록 합니다.
private func updateTimerState() {
let isPresented = selectedCoin != nil || showDetailCoin != nil
if isPresented {
if !isSheetPresented {
isSheetPresented = true
viewModel.stopTimer()
print("‼️stop")
}
} else {
if isSheetPresented {
isSheetPresented = false
viewModel.startTimer()
print("‼️start")
}
}
}
}

#Preview {
Expand Down
103 changes: 103 additions & 0 deletions AIProject/iCo/Features/Dashboard/View/CoinInfoView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// CoinInfoView.swift
// iCo
//
// Created by 지현 on 10/27/25.
//

import SwiftUI

struct CoinInfoView: View {
@EnvironmentObject var themeManager: ThemeManager

let recommendCoin: RecommendCoin
var onCloseButtonTap: (() -> Void)? = nil

private func dynamicStatusColor(for type: RecommendCoin.TickerChangeType) -> Color {
switch type {
case .rise:
return themeManager.selectedTheme.positiveColor
case .even:
return themeManager.selectedTheme.neutral
case .fall:
return themeManager.selectedTheme.negativeColor
}
}

var body: some View {
VStack(spacing: 16) {
HStack(alignment: .center, spacing: .spacing) {
CoinView(symbol: recommendCoin.id, size: 50)

VStack(alignment: .leading, spacing: 4) {
Text(recommendCoin.name)
.font(.ico17B)
.bold()
.foregroundStyle(.iCoLabel)

Text(recommendCoin.id)
.font(.ico12Sb)
.fontWeight(.semibold)
.foregroundStyle(.iCoLabelSecondary)
}

Spacer()

if let onCloseButtonTap {
RoundedButton(imageName: "xmark") {
onCloseButtonTap()
}
}
}

HStack {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 4) {
Text("현재가")
.font(.ico14)
.foregroundStyle(.iCoLabel)

Text(recommendCoin.tradePrice.formatKRW)
.font(.ico14B)
.bold()
.foregroundStyle(dynamicStatusColor(for: recommendCoin.changeType))
}

HStack(spacing: 4) {
Text("전일대비")
.font(.ico14)
.foregroundStyle(.iCoLabel)

Group {
Text("\(recommendCoin.changeType.code)\(recommendCoin.changeRate.formatRate)")
}
.font(.ico14B)
.bold()
.foregroundStyle(dynamicStatusColor(for: recommendCoin.changeType))
}
}

Spacer()

LineChartView(values: recommendCoin.candles, lineColor: dynamicStatusColor(for: recommendCoin.changeType))
.frame(width: 130, height: 40)
}
}
}
}

#Preview {
CoinInfoView(
recommendCoin: RecommendCoin(
imageURL: nil,
comment: "펏지펭귄은 활발한 커뮤니티와 밈 기반의 인기 덕분에 최근 주목받고 있어요. 소액으로 재미있게 투자하기 좋고, 성장 가능성도 보여 기대돼요. 소액으로 재미있게 투자하기 좋고, 성장 가능성도 보여 기대돼요.",
coinID: "BTC",
name: "월드리버티파이낸셜유에스디",
tradePrice: 1600,
changeRate: 4.27,
changeType: .rise,
candles: [1.0, 3.0, 2.0, 6.0, 5.0, 3.0, 7.0, 9.0, 6.0, 10.0]
)
)
.environmentObject(ThemeManager())
}
Loading