diff --git a/AIProject/iCOWidget/CoinWidget.swift b/AIProject/iCOWidget/CoinWidget.swift index 27c0eb43..364d8d7e 100644 --- a/AIProject/iCOWidget/CoinWidget.swift +++ b/AIProject/iCOWidget/CoinWidget.swift @@ -237,8 +237,8 @@ struct CoinCardView: View { .frame(maxWidth: .infinity, alignment: .trailing) } - - SparklineView(prices: coin.history) + + LineChartView(values: coin.history) .padding(8) HStack { @@ -255,54 +255,3 @@ struct CoinCardView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - -struct SparklineView: View { - let prices: [Double] - - private var normalized: [CGFloat] { - guard prices.count > 1 else { return [] } - guard let min = prices.min(), let max = prices.max(), min != max else { - return Array(repeating: 0.5, count: prices.count) - } - return prices.map { CGFloat(($0 - min) / (max - min)) } - } - - var body: some View { - GeometryReader { geo in - if normalized.isEmpty { - Path { path in - let midY = geo.size.height / 2 - path.move(to: CGPoint(x: 0, y: midY)) - path.addLine(to: CGPoint(x: geo.size.width, y: midY)) - } - .stroke(Color.gray.opacity(0.4), lineWidth: 1) - } else { - ZStack { - Path { path in - for (index, value) in normalized.enumerated() { - let x = geo.size.width * CGFloat(index) / CGFloat(normalized.count - 1) - let y = geo.size.height * (1 - value) - if index == 0 { - path.move(to: CGPoint(x: x, y: y)) - } else { - path.addLine(to: CGPoint(x: x, y: y)) - } - } - } - .stroke(Color.iCoAccent, style: StrokeStyle(lineWidth: 2, lineJoin: .round)) - - if let last = normalized.last { - let x = geo.size.width - let y = geo.size.height * (1 - last) - - Circle() - .fill(Color.blue) - .frame(width: 6, height: 6) - .position(x: x, y: y) - } - } - } - } - //.frame(height: 36) - } -} diff --git a/AIProject/iCo.xcodeproj/project.pbxproj b/AIProject/iCo.xcodeproj/project.pbxproj index 70d478a7..0fe7f714 100644 --- a/AIProject/iCo.xcodeproj/project.pbxproj +++ b/AIProject/iCo.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ App/Resource/Assets.xcassets, Core/Local/UserDefaults/AppStorageKey.swift, "Core/Util/Double+Util.swift", + Features/Base/LineChartView.swift, ); target = B7C71D282E7BD9D6000E1191 /* iCOWidgetExtension */; }; diff --git a/AIProject/iCo/Features/Base/LineChartView.swift b/AIProject/iCo/Features/Base/LineChartView.swift new file mode 100644 index 00000000..71898151 --- /dev/null +++ b/AIProject/iCo/Features/Base/LineChartView.swift @@ -0,0 +1,80 @@ +// +// LineChartView.swift +// iCo +// +// Created by 백현진 on 10/24/25. +// + +import SwiftUI + +/// 공통으로 사용할 수 있는 라인 차트(미니 차트) 컴포넌트 +/// +/// - Parameters: +/// - values: 차트에 표시할 값 목록 (예: 가격 데이터) +/// - lineColor: 차트 선 색상 (기본값은 `Color.iCoAccent`) +/// - showsLastDot: 마지막 지점(최근 데이터)에 점을 표시할지 여부 (`true` 시 표시) +/// - lineWidth: 선의 두께 (기본값: `2`) +struct LineChartView: View { + /// 차트에 표시할 값 목록 (예: 가격 데이터) + let values: [Double] + + /// 차트 선 색상 (기본값: aiCoAccent) + var lineColor: Color = .iCoAccent + + /// 마지막 지점 표시 여부 + var showsLastDot: Bool = true + + /// 선 두께 + var lineWidth: CGFloat = 2 + + /// 내부 계산용 정규화된 값 + private var normalizedValues: [CGFloat] { + guard values.count > 1 else { return [] } + guard let min = values.min(), let max = values.max(), min != max else { + return Array(repeating: 0.5, count: values.count) + } + return values.map { CGFloat(($0 - min) / (max - min)) } + } + + var body: some View { + GeometryReader { geo in + if normalizedValues.isEmpty { + // 값이 없을 때 — 가운데 회색 선 표시 + Path { path in + let midY = geo.size.height / 2 + path.move(to: CGPoint(x: 0, y: midY)) + path.addLine(to: CGPoint(x: geo.size.width, y: midY)) + } + .stroke(Color.gray.opacity(0.4), lineWidth: 1) + } else { + ZStack { + // 스파크라인 + Path { path in + for (index, value) in normalizedValues.enumerated() { + let x = geo.size.width * CGFloat(index) / CGFloat(normalizedValues.count - 1) + let y = geo.size.height * (1 - value) + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + } + .stroke(lineColor, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) + + // 마지막 점 + if showsLastDot, let last = normalizedValues.last { + let x = geo.size.width + let y = geo.size.height * (1 - last) + + Circle() + .fill(lineColor) + .frame(width: 6, height: 6) + .position(x: x, y: y) + } + } + } + } + } +} +