feat(mypage): 최신 공지 배열과 제목 롤링을 적용한다
This commit is contained in:
@@ -56,9 +56,9 @@ struct MyPageView: View {
|
||||
isShowButton: !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
)
|
||||
|
||||
if let notice = viewModel.latestNotice {
|
||||
if !viewModel.latestNotices.isEmpty {
|
||||
// Update Banner
|
||||
UpdateBannerView(item: notice)
|
||||
UpdateBannerView(items: viewModel.latestNotices)
|
||||
}
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
@@ -170,7 +170,7 @@ struct MyPageView: View {
|
||||
viewModel.getMypage()
|
||||
recentContentViewModel.fetchRecentContents()
|
||||
}
|
||||
viewModel.getLatestNotice()
|
||||
viewModel.getLatestNotices()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,34 +217,88 @@ struct MyPageHeaderView: View {
|
||||
|
||||
// MARK: - Update Banner View
|
||||
struct UpdateBannerView: View {
|
||||
|
||||
let item: NoticeItem
|
||||
|
||||
private static let foregroundColor = Color(hex: "B0BEC5")
|
||||
private static let rollInterval: TimeInterval = 5
|
||||
private static let transitionDuration: TimeInterval = 0.35
|
||||
|
||||
let items: [NoticeItem]
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var roller = MyPageNoticeRoller()
|
||||
@State private var rollTimer: Timer?
|
||||
@State private var isVisible = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("\(item.title)")
|
||||
.appFont(size: 16)
|
||||
.foregroundColor(Color(hex: "B0BEC5"))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Text(I18n.MyPage.Main.detail)
|
||||
.appFont(size: 16)
|
||||
.foregroundColor(Color(hex: "B0BEC5"))
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(Color(hex: "B0BEC5"))
|
||||
.frame(width: 24, height: 24)
|
||||
ZStack {
|
||||
if let item = roller.current {
|
||||
Button {
|
||||
AppState.shared.setAppStep(step: .noticeDetail(notice: item))
|
||||
} label: {
|
||||
HStack(spacing: SodaSpacing.s8) {
|
||||
ZStack {
|
||||
Text(item.title)
|
||||
.appFont(.body3)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.id(roller.index)
|
||||
.transition(reduceMotion ? .identity : .asymmetric(
|
||||
insertion: .move(edge: .bottom),
|
||||
removal: .move(edge: .top)
|
||||
))
|
||||
}
|
||||
.clipped()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.frame(width: SodaSpacing.s24, height: SodaSpacing.s24)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundColor(Self.foregroundColor)
|
||||
.padding(.horizontal, SodaSpacing.s24)
|
||||
.padding(.vertical, SodaSpacing.s6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 6)
|
||||
.clipped()
|
||||
.background(Color.black)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .noticeDetail(notice: item))
|
||||
.onAppear {
|
||||
isVisible = true
|
||||
roller.update(items)
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onDisappear {
|
||||
isVisible = false
|
||||
stopRollTimer()
|
||||
}
|
||||
.onChange(of: items) { items in
|
||||
roller.update(items)
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onChange(of: scenePhase) { scenePhase in
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onChange(of: reduceMotion) { reduceMotion in
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
}
|
||||
|
||||
private func restartRollTimer(scenePhase: ScenePhase, reduceMotion: Bool) {
|
||||
stopRollTimer()
|
||||
guard roller.shouldRoll(isVisible: isVisible, isActive: scenePhase == .active) else { return }
|
||||
|
||||
rollTimer = Timer.scheduledTimer(withTimeInterval: Self.rollInterval, repeats: true) { _ in
|
||||
withAnimation(reduceMotion ? nil : .easeOut(duration: Self.transitionDuration)) {
|
||||
roller.advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopRollTimer() {
|
||||
rollTimer?.invalidate()
|
||||
rollTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ final class MyPageViewModel: ObservableObject {
|
||||
private var pendingAuthRequest: AuthVerifyRequest?
|
||||
private var pendingAuthSuccess: (() -> Void)?
|
||||
|
||||
@Published var latestNotice: NoticeItem? = nil
|
||||
@Published var latestNotices: [NoticeItem] = []
|
||||
|
||||
@Published var nickname: String = ""
|
||||
@Published var profileUrl: String = ""
|
||||
@@ -259,8 +259,9 @@ final class MyPageViewModel: ObservableObject {
|
||||
AppState.shared.setAppStep(step: .splash)
|
||||
}
|
||||
|
||||
func getLatestNotice() {
|
||||
noticeRepository.getLatestNotice()
|
||||
func getLatestNotices() {
|
||||
noticeRepository.getLatestNotices()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
@@ -270,14 +271,18 @@ final class MyPageViewModel: ObservableObject {
|
||||
}
|
||||
} receiveValue: {[unowned self] response in
|
||||
self.isLoading = false
|
||||
guard (200..<300).contains(response.statusCode) else {
|
||||
ERROR_LOG("Latest notices HTTP \(response.statusCode)")
|
||||
return
|
||||
}
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<NoticeItem>.self, from: responseData)
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[NoticeItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.latestNotice = data
|
||||
self.latestNotices = data
|
||||
}
|
||||
} catch {
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
|
||||
Reference in New Issue
Block a user