From 253f28d750ac9796f2d85e44e8cefed935dfda20 Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Mon, 3 Nov 2025 15:45:43 +0900 Subject: [PATCH 1/6] =?UTF-8?q?rename:=20UpBitApiServiceProtocol=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=86=A0=EC=BD=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=9D=BC=EA=B4=80=EC=84=B1=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20UpBitAPIServiceProtocol=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift | 2 +- ...ApiServiceProtocol.swift => UpBitAPIServiceProtocol.swift} | 3 ++- .../Features/Dashboard/ViewModel/RecommendCoinViewModel.swift | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) rename AIProject/iCo/Domain/Interface/{UpBitApiServiceProtocol.swift => UpBitAPIServiceProtocol.swift} (58%) diff --git a/AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift b/AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift index 5fab03ac..7b400aac 100644 --- a/AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift +++ b/AIProject/iCo/Data/API/Upbit/UpbitAPIService.swift @@ -8,7 +8,7 @@ import Foundation /// 업비트 API 관련 서비스를 제공합니다. -final class UpBitAPIService: UpBitApiServiceProtocol { +final class UpBitAPIService: UpBitAPIServiceProtocol { private let network: NetworkClient init(networkClient: NetworkClient = .init()) { diff --git a/AIProject/iCo/Domain/Interface/UpBitApiServiceProtocol.swift b/AIProject/iCo/Domain/Interface/UpBitAPIServiceProtocol.swift similarity index 58% rename from AIProject/iCo/Domain/Interface/UpBitApiServiceProtocol.swift rename to AIProject/iCo/Domain/Interface/UpBitAPIServiceProtocol.swift index 66766fdb..c881ac94 100644 --- a/AIProject/iCo/Domain/Interface/UpBitApiServiceProtocol.swift +++ b/AIProject/iCo/Domain/Interface/UpBitAPIServiceProtocol.swift @@ -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] } diff --git a/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift b/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift index 49ad5315..67bd68c9 100644 --- a/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift +++ b/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift @@ -16,7 +16,7 @@ final class RecommendCoinViewModel: ObservableObject { @Published var fetchTimestamp: Date? private var llmService: LLMRecommendCoinFetching - private var upbitService: UpBitApiServiceProtocol + private var upbitService: UpBitAPIServiceProtocol var task: Task? let numberOfCoins: Int = 5 @@ -36,7 +36,7 @@ final class RecommendCoinViewModel: ObservableObject { init( llmService: LLMRecommendCoinFetching = LLMAPIService(), - upbitService: UpBitApiServiceProtocol = UpBitAPIService() + upbitService: UpBitAPIServiceProtocol = UpBitAPIService() ) { self.llmService = llmService self.upbitService = upbitService From 507f6571a6b4b2b78ba280d95c9c52b23e5d247c Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Mon, 3 Nov 2025 15:46:43 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20LLM=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EA=B8=80=EC=9E=90=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AIProject/iCo/Domain/Model/Common/Prompt.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIProject/iCo/Domain/Model/Common/Prompt.swift b/AIProject/iCo/Domain/Model/Common/Prompt.swift index 11382535..33fc9a7b 100644 --- a/AIProject/iCo/Domain/Model/Common/Prompt.swift +++ b/AIProject/iCo/Domain/Model/Common/Prompt.swift @@ -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): @@ -97,7 +97,7 @@ enum Prompt { let summary: String } - 커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외) + 커뮤니티 분위기(호재, 악재, 중립)와 그렇게 평가한 이유를 한글로 200자 이상으로 요약해 위 JSON으로 제공 (답변은 한글, 마크다운 금지, 출처 제외) """ case .generateBookmarkBriefing(let importance, let bookmarks): """ From d6bd5c8769c3acb636ea8ded549f5222a360fdfc Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Mon, 3 Nov 2025 15:54:27 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9D=B8=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?RecommendCoin=EC=97=90=20candles=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift b/AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift index b5efec53..47cab158 100644 --- a/AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift +++ b/AIProject/iCo/Domain/Model/Dashboard/RecommendCoin.swift @@ -41,4 +41,5 @@ struct RecommendCoin: Identifiable, Hashable { let tradePrice: Double let changeRate: Double let changeType: TickerChangeType + let candles: [Double] } From df3a1606cd3bba2f920fd3bc377ee820053967d6 Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Mon, 3 Nov 2025 15:56:28 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=BD=94=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레이아웃 변경 - 라인 차트 추가 - 추천 이유를 보여주는 sheet 추가 --- .../iCo/Features/Dashboard/CardConst.swift | 2 +- .../Dashboard/View/CoinCarouselView.swift | 64 ++++++++++- .../Dashboard/View/CoinInfoView.swift | 103 ++++++++++++++++++ .../Dashboard/View/RecommendCardView.swift | 86 +++------------ .../ViewModel/RecommendCoinViewModel.swift | 5 +- 5 files changed, 179 insertions(+), 81 deletions(-) create mode 100644 AIProject/iCo/Features/Dashboard/View/CoinInfoView.swift diff --git a/AIProject/iCo/Features/Dashboard/CardConst.swift b/AIProject/iCo/Features/Dashboard/CardConst.swift index c81f6d46..4e1a312e 100644 --- a/AIProject/iCo/Features/Dashboard/CardConst.swift +++ b/AIProject/iCo/Features/Dashboard/CardConst.swift @@ -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 diff --git a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift index 854dd362..46eaa009 100644 --- a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift +++ b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift @@ -42,9 +42,13 @@ struct CoinCarouselView: View { var tempCoinArray: [RecommendCoin] { wrappedCoins.flatMap { $0.map { $0 }} } - @State private var showNewBadge = false - + @State private var showDetails: Bool = false + @State private var measuredHeight: CGFloat = 0 + private var detent: PresentationDetent { + .height(measuredHeight) + } + var body: some View { /// 화면의 가로 크기에 따라 카드 갯수를 관리하는 computed property var numberOfColumn: Int { hSizeClass == .regular ? 2 : 1 } @@ -59,7 +63,10 @@ struct CoinCarouselView: View { width: nil, height: CardConst.cardHeight ) - .onTapGesture { selectedCoin = coin } + .onTapGesture { + selectedCoin = coin + viewModel.stopTimer() + } .scrollTransition(axis: .horizontal) { content, phase in // 활성화된 코인은 크게 보이게 하기 content.scaleEffect( y: phase.isIdentity ? 1 : CardConst.cardHeightMultiplier, @@ -117,7 +124,53 @@ struct CoinCarouselView: View { handleManualScrolling(cardID: newValue) } .sheet(item: $selectedCoin) { coin in - VStack(spacing: 0) { + VStack(spacing: .spacing) { + CoinInfoView(recommendCoin: coin) { + selectedCoin = nil + viewModel.startTimer() + } + + VStack(spacing: 4) { + HStack(spacing: 8) { + Image(systemName: "sparkles") + .font(.ico14B) + .foregroundStyle(.iCoAccent) + + Text("아이코가 추천하는 이유") + .font(.ico16B) + .foregroundStyle(.iCoLabel) + + Spacer() + } + + 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)) { + showDetails = true + } + } + .padding(20) + .background(.background) + .background( + GeometryReader { geo in + Color.clear + .onAppear { + measuredHeight = geo.size.height + } + } + ) + .presentationDetents([detent]) + .sheet(isPresented: $showDetails) { ZStack(alignment: .center) { HeaderView( heading: coin.name, @@ -129,7 +182,7 @@ struct CoinCarouselView: View { .toolbar(.hidden, for: .navigationBar) RoundedButton(imageName: "xmark") { - selectedCoin = nil + showDetails = false } .frame(maxWidth: .infinity, alignment: .trailing) .padding() @@ -139,7 +192,6 @@ struct CoinCarouselView: View { showNewBadge = isNew } } - .background(.background) } .onAppear { // 무한 스크롤링 효과를 구현하기 위해 추천 코인 배열의 앞 뒤에 안전 코인을 붙이기 diff --git a/AIProject/iCo/Features/Dashboard/View/CoinInfoView.swift b/AIProject/iCo/Features/Dashboard/View/CoinInfoView.swift new file mode 100644 index 00000000..3f157c37 --- /dev/null +++ b/AIProject/iCo/Features/Dashboard/View/CoinInfoView.swift @@ -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()) +} diff --git a/AIProject/iCo/Features/Dashboard/View/RecommendCardView.swift b/AIProject/iCo/Features/Dashboard/View/RecommendCardView.swift index 9c3d5a12..559c0b83 100644 --- a/AIProject/iCo/Features/Dashboard/View/RecommendCardView.swift +++ b/AIProject/iCo/Features/Dashboard/View/RecommendCardView.swift @@ -8,77 +8,17 @@ import SwiftUI struct RecommendCardView: View { - @EnvironmentObject var themeManager: ThemeManager - let recommendCoin: RecommendCoin - 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: 0) { - VStack(alignment: .leading) { - CoinView(symbol: recommendCoin.id, size: 50) - - HStack(spacing: 10) { - Text(recommendCoin.name) - .font(.ico17) - .bold() - .foregroundStyle(.iCoLabel) - - Text(recommendCoin.id) - .font(.ico12) - .fontWeight(.semibold) - .foregroundStyle(.iCoLabelSecondary) - } - .padding(.top, 4) - - HStack(spacing: 4) { - Text("현재가") - .font(.ico14) - .foregroundStyle(.iCoLabel) - - Text(recommendCoin.tradePrice.formatKRW) - .font(.ico14) - .bold() - .foregroundStyle(dynamicStatusColor(for: recommendCoin.changeType)) - } - .padding(.top, 1) - - HStack(spacing: 4) { - Text("전일대비") - .font(.ico14) - .foregroundStyle(.iCoLabel) - - HStack(spacing: 0) { - Group { - Text("\(recommendCoin.changeType.code)\(recommendCoin.changeRate.formatRate)") - } - .font(.ico14) - .bold() - .foregroundStyle(dynamicStatusColor(for: recommendCoin.changeType)) - } - } - .padding(.top, 0.2) - } - .frame(maxWidth: .infinity, alignment: .leading) - - VStack { - Text(recommendCoin.comment.byCharWrapping) - .font(.ico14) - .lineSpacing(6) - .foregroundStyle(.iCoLabel) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .padding(.top, 32) + VStack(alignment: .leading, spacing: 0) { + CoinInfoView(recommendCoin: recommendCoin) + + Text(recommendCoin.comment.byCharWrapping) + .font(.ico14) + .lineSpacing(6) + .foregroundStyle(.iCoLabel) + .padding(.top, 30) } .padding(24) .background( @@ -99,15 +39,15 @@ struct RecommendCardView: View { RecommendCardView( recommendCoin: RecommendCoin( imageURL: nil, - comment: "좋다!", - coinID: "KRW-BTC", - name: "비트코인", + comment: "펏지펭귄은 활발한 커뮤니티와 밈 기반의 인기 덕분에 최근 주목받고 있어요. 소액으로 재미있게 투자하기 좋고, 성장 가능성도 보여 기대돼요. 소액으로 재미있게 투자하기 좋고, 성장 가능성도 보여 기대돼요.", + coinID: "BTC", + name: "월드리버티파이낸셜유에스디", tradePrice: 1600, changeRate: 4.27, - changeType: .rise + 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()) } diff --git a/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift b/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift index 67bd68c9..279134e9 100644 --- a/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift +++ b/AIProject/iCo/Features/Dashboard/ViewModel/RecommendCoinViewModel.swift @@ -108,6 +108,8 @@ final class RecommendCoinViewModel: ObservableObject { guard let data = try await self.upbitService.fetchQuotes(id: "KRW-\(dto.symbol)").first else { return nil } + + let candleData = try await self.upbitService.fetchCandles(id: "KRW-\(dto.symbol)", count: 10, to: nil) return RecommendCoin( imageURL: nil, @@ -116,7 +118,8 @@ final class RecommendCoinViewModel: ObservableObject { name: dto.name, tradePrice: data.tradePrice, changeRate: data.changeRate * 100, - changeType: RecommendCoin.TickerChangeType(rawValue: data.change) + changeType: RecommendCoin.TickerChangeType(rawValue: data.change), + candles: candleData.map { $0.tradePrice }.reversed() ) } catch { return nil From 7b1fe903992854408d70dbcdfd407e80fcc304c2 Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Mon, 3 Nov 2025 16:36:21 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=A1=9C=20sheet=EB=A5=BC=20=EB=8B=AB=EC=95=98=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=EB=8F=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=EA=B0=80=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EB=8F=99=EC=9E=91=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift index 46eaa009..bccb3689 100644 --- a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift +++ b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift @@ -127,7 +127,6 @@ struct CoinCarouselView: View { VStack(spacing: .spacing) { CoinInfoView(recommendCoin: coin) { selectedCoin = nil - viewModel.startTimer() } VStack(spacing: 4) { @@ -170,6 +169,9 @@ struct CoinCarouselView: View { } ) .presentationDetents([detent]) + .onDisappear { + viewModel.startTimer() + } .sheet(isPresented: $showDetails) { ZStack(alignment: .center) { HeaderView( From ef198619450c2093e159367d0077dc3b27893ed3 Mon Sep 17 00:00:00 2001 From: jihyeonjjang Date: Tue, 4 Nov 2025 16:37:50 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20sheet=EA=B0=80=20=EC=A4=91=EC=B2=A9?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=EC=97=B4=EB=A6=AC=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HIG 권장사항에 따라 한 번에 하나의 시트만 표시되도록 변경하여, 모달 전환 시 사용자가 위치를 혼동하지 않도록 함. --- .../Dashboard/View/CoinCarouselView.swift | 116 ++++++++++-------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift index bccb3689..a532d4d3 100644 --- a/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift +++ b/AIProject/iCo/Features/Dashboard/View/CoinCarouselView.swift @@ -43,11 +43,10 @@ struct CoinCarouselView: View { wrappedCoins.flatMap { $0.map { $0 }} } @State private var showNewBadge = false - @State private var showDetails: Bool = false + @State private var showDetailCoin: RecommendCoin? @State private var measuredHeight: CGFloat = 0 - private var detent: PresentationDetent { - .height(measuredHeight) - } + private var detent: PresentationDetent { .height(measuredHeight) } + @State private var isSheetPresented = false var body: some View { /// 화면의 가로 크기에 따라 카드 갯수를 관리하는 computed property @@ -65,7 +64,6 @@ struct CoinCarouselView: View { ) .onTapGesture { selectedCoin = coin - viewModel.stopTimer() } .scrollTransition(axis: .horizontal) { content, phase in // 활성화된 코인은 크게 보이게 하기 content.scaleEffect( @@ -123,13 +121,33 @@ struct CoinCarouselView: View { else { return } handleManualScrolling(cardID: newValue) } + .onAppear { + // 무한 스크롤링 효과를 구현하기 위해 추천 코인 배열의 앞 뒤에 안전 코인을 붙이기 + wrappedCoins = [recommendedCoins, recommendedCoins, recommendedCoins] + cardID = totalCoinCount // 시작점을 중간 배열의 첫 번째 카드로 지정하기 + viewModel.startTimer() + } + .onChange(of: scenePhase) { _, newPhase in + // 백그라운드 상태에서는 타이머 실행 중단하기 + switch newPhase { + case .active: + viewModel.startTimer() + default: + viewModel.stopTimer() + } + } + .onDisappear { + cardID = nil + viewModel.stopTimer() + wrappedCoins.removeAll() + } .sheet(item: $selectedCoin) { coin in VStack(spacing: .spacing) { CoinInfoView(recommendCoin: coin) { selectedCoin = nil } - VStack(spacing: 4) { + VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { Image(systemName: "sparkles") .font(.ico14B) @@ -138,8 +156,6 @@ struct CoinCarouselView: View { Text("아이코가 추천하는 이유") .font(.ico16B) .foregroundStyle(.iCoLabel) - - Spacer() } Text(String.aiGeneratedContentNotice) @@ -155,7 +171,8 @@ struct CoinCarouselView: View { .fixedSize(horizontal: false, vertical: true) RoundedRectangleFillButton(title: "더 자세히 보러가기", imageName: "info.circle", isHighlighted: .constant(true)) { - showDetails = true + selectedCoin = nil + showDetailCoin = coin } } .padding(20) @@ -169,51 +186,34 @@ struct CoinCarouselView: View { } ) .presentationDetents([detent]) - .onDisappear { - viewModel.startTimer() - } - .sheet(isPresented: $showDetails) { - ZStack(alignment: .center) { - HeaderView( - heading: coin.name, - topPadding: 20, - coinSymbol: coin.id, - showBackButton: false, - showNewBadge: showNewBadge - ) - .toolbar(.hidden, for: .navigationBar) - - RoundedButton(imageName: "xmark") { - showDetails = false - } - .frame(maxWidth: .infinity, alignment: .trailing) - .padding() - } + } + .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) - CoinDetailView(coin: Coin(id: "KRW-" + coin.id, koreanName: coin.name, imageURL: coin.imageURL)) { isNew in - showNewBadge = isNew + RoundedButton(imageName: "xmark") { + showDetailCoin = nil } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding() } - } - .onAppear { - // 무한 스크롤링 효과를 구현하기 위해 추천 코인 배열의 앞 뒤에 안전 코인을 붙이기 - wrappedCoins = [recommendedCoins, recommendedCoins, recommendedCoins] - cardID = totalCoinCount // 시작점을 중간 배열의 첫 번째 카드로 지정하기 - viewModel.startTimer() - } - .onChange(of: scenePhase) { _, newPhase in - // 백그라운드 상태에서는 타이머 실행 중단하기 - switch newPhase { - case .active: - viewModel.startTimer() - default: - viewModel.stopTimer() + + CoinDetailView(coin: Coin(id: "KRW-" + detail.id, koreanName: detail.name, imageURL: detail.imageURL)) { isNew in + showNewBadge = isNew } } - .onDisappear { - cardID = nil - viewModel.stopTimer() - wrappedCoins.removeAll() + .onChange(of: showDetailCoin) { _, newValue in + updateTimerState() } } } @@ -284,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 {