[] 대시보드 학습관리 & 공지사항 뷰 생성, 메인화면 전환 로직 수정

This commit is contained in:
Seonkyu_Kim 2025-02-11 17:55:19 +09:00
parent ad85ccea7a
commit f6a371923a
25 changed files with 469 additions and 164 deletions

View File

@ -39,7 +39,9 @@ struct SimpleBtnView: View {
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()
.renderingMode(.template)
.frame(width: state.width, height: state.height) .frame(width: state.width, height: state.height)
.foregroundStyle(state.isUsable ? Color(.Second.normal) : Color(.Normal.normal))
} }
} }
.disabled(!state.isUsable) .disabled(!state.isUsable)

View File

@ -12,22 +12,23 @@ import Combine
/// ///
struct NavigationView: View { struct NavigationView: View {
@EnvironmentObject var appVM: AppViewModel @EnvironmentObject var appVM: AppViewModel
@State private var naviState : NaviState = .init(act: .NONE, path: .Intro) @State private var naviState : NaviState = .init(act: .NONE, path: .Intro)
@State private var history: [PathName] = [.Intro] @State private var history: [PathName] = [.Intro]
var body: some View { var body: some View {
ZStack { VStack(spacing: 0) {
switch naviState.path {
case .NONE: ZStack {
EmptyView() switch naviState.path {
case .Intro: case .NONE:
IntroView(naviState: $naviState) EmptyView()
case .Login : case .Intro:
LoginView(naviState: $naviState) IntroView(naviState: $naviState)
case .Main: case .Login :
MainView(naviState: $naviState) LoginView(naviState: $naviState)
case .Main:
MainView(naviState: $naviState, menuName: .Home)
}
} }
} }
.onChange(of: naviState) { old, new in .onChange(of: naviState) { old, new in
@ -43,7 +44,6 @@ struct NavigationView: View {
case .MOVE: case .MOVE:
moveHistory(path: new.path) moveHistory(path: new.path)
} }
// LOG
printLog("\(old.path) => \(new.path)") printLog("\(old.path) => \(new.path)")
showHistory() showHistory()
} }

View File

@ -37,9 +37,6 @@ struct OffsetObservableScrollView<Content: View>: View {
Color.clear Color.clear
.frame(height: 1) .frame(height: 1)
.id("TOP") .id("TOP")
// . content scrollViewProxy ,
// .
content(scrollViewProxy) content(scrollViewProxy)
.overlay { .overlay {
GeometryReader { geometryProxy in GeometryReader { geometryProxy in
@ -58,7 +55,7 @@ struct OffsetObservableScrollView<Content: View>: View {
} }
.coordinateSpace(name: coordinateSpaceName) .coordinateSpace(name: coordinateSpaceName)
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
printLog("\(scrollOffset) -> \(value)") // printLog("\(scrollOffset) -> \(value)")
scrollOffset = value scrollOffset = value
} }

View File

@ -12,10 +12,26 @@ struct CalendarBoxView: View {
var body: some View { var body: some View {
DashBoardView(image: Image(.Icon.calendar), title: "최근 일정") { DashBoardView(image: Image(.Icon.calendar), title: "최근 일정") {
VStack(spacing: 12) { if !summaryCalDataList.isEmpty {
ForEach(Array(summaryCalDataList.enumerated()), id: \.offset) { index, data in VStack(spacing: 12) {
CalCellView(summaryCalData: data) ForEach(Array(summaryCalDataList.enumerated()), id: \.offset) { index, data in
CalCellView(summaryCalData: data)
}
} }
} else {
Text("최근 일정이 없습니다.")
.font(.nps(size: 20))
.foregroundStyle(Color(.Text.detail))
.lineLimit(1)
.minimumScaleFactor(0.5)
.truncationMode(.tail)
.frame(maxWidth: .infinity)
.padding([.top,.bottom],12)
.background {
RoundedRectangle(cornerRadius: 4)
.stroke(Color(.Second.normal), lineWidth: 2)
.fill(Color(.Second.light))
}
} }
} moreAction: { } moreAction: {

View File

@ -0,0 +1,72 @@
//
// HomeView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct HomeView: View {
@State private var scrollOffset: CGPoint = .zero
@State private var topViewState: Bool = false
var body: some View {
VStack(spacing: 0) {
ZStack {
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
VStack(spacing: 0) {
TopProfileView(userType: .Student)
// .padding(EdgeInsets(top: 0, leading: 0, bottom: 12, trailing: 0))s
.padding(.bottom,12)
Group {
AttendanceBoxView()
CalendarBoxView(summaryCalDataList: [])
CalendarBoxView(summaryCalDataList: [
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다."),
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다.")])
ManagementBoxView(managementList: [])
ManagementBoxView(managementList: [
SummaryManagement(id: "01", title: "과목 명1", teacher: "A", ratio: 27, homework: 3),
SummaryManagement(id: "02", title: "과목 명2", teacher: "B", ratio: 80, homework: 10),
SummaryManagement(id: "03", title: "과목 명3", teacher: "C", ratio: 13, homework: 2),
SummaryManagement(id: "04", title: "과목 명4", teacher: "D", ratio: 72, homework: 0),
])
NoticeBoxView(noticeList: [
SummaryNotice(id: "00", title: "공지사항1", date: "2025-02-11", new: true),
SummaryNotice(id: "01", title: "공지사항2", date: "2025-01-11", new: false),
SummaryNotice(id: "02", title: "공지사항3", date: "2025-01-01", new: false)
])
}
.background {
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(Color(.Other.cell))
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
}
}
if topViewState {
VStack(spacing: 0) {
TopView(titleName: "Name")
.transition(.move(edge: .top))
.animation(.easeInOut, value: scrollOffset)
Spacer(minLength: 1)
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onChange(of: scrollOffset.y) { oldValue, newValue in
if newValue > 200 && topViewState == false {
topViewState = true
} else if newValue < 20 && topViewState == true{
topViewState = false
}
}
}
}

View File

@ -0,0 +1,173 @@
//
// ManagementBoxView.swift
// AcaMate
//
// Created by TAnine on 2/10/25.
//
import SwiftUI
struct ManagementBoxView: View {
@StateObject var btnVM = ButtonViewModel()
@State private var leftBtnID = UUID()
@State private var rightBtnID = UUID()
@State var managementList: [SummaryManagement]
@State private var countNum: Int = 0
var body: some View {
DashBoardView(image: Image(.Icon.edu), title: "학습 관리") {
if managementList.count >= 1 {
HStack {
SimpleBtnView(vm: btnVM, id: leftBtnID)
Spacer(minLength: 1)
SimpleBtnView(vm: btnVM, id: rightBtnID)
}
.padding([.bottom],12)
MangCellView(summaryMang: $managementList[countNum])
} else {
Text("학습하고 있는 강의가 없습니다.")
.font(.nps(size: 20))
.foregroundStyle(Color(.Text.detail))
.lineLimit(1)
.minimumScaleFactor(0.5)
.truncationMode(.tail)
.frame(maxWidth: .infinity)
.padding([.top,.bottom],12)
.background {
RoundedRectangle(cornerRadius: 4)
.stroke(Color(.Second.normal), lineWidth: 2)
.fill(Color(.Second.light))
}
}
} moreAction: {
// MARK: TO-DO
//
printLog("학습 관리의 더보기")
}
.onAppear {
btnVM.setImage(for: leftBtnID, newImage: Image(.Icon.left))
btnVM.setSize(for: leftBtnID, newWidth: 24, newHeight: 24)
btnVM.setAction(for: leftBtnID) { countNum -= 1 }
btnVM.setIsUsable(for: leftBtnID, newValue: false)
btnVM.setImage(for: rightBtnID, newImage: Image(.Icon.right))
btnVM.setSize(for: rightBtnID, newWidth: 24, newHeight: 24)
btnVM.setAction(for: rightBtnID) { countNum += 1 }
if managementList.count == 0 {
btnVM.setIsUsable(for: rightBtnID, newValue: false)
}
}
.onChange(of: countNum) { oldValue, newValue in
if countNum == 0 {
btnVM.setIsUsable(for: leftBtnID, newValue: false)
} else if 0 < countNum && countNum < managementList.count-1 {
btnVM.setIsUsable(for: leftBtnID, newValue: true)
btnVM.setIsUsable(for: rightBtnID, newValue: true)
}
else {
btnVM.setIsUsable(for: rightBtnID, newValue: false)
}
}
}
}
struct MangCellView: View {
@StateObject var btnVM = ButtonViewModel()
@Binding var summaryMang: SummaryManagement
@State var ratioBtn = UUID()
@State var flagBtn = UUID()
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 8) {
HStack(spacing: 12) {
Image(.Icon.management)
.resizable()
.frame(width: 24, height: 24, alignment: .center)
Text("\(summaryMang.title)")
.font(.nps(size: 20))
.foregroundStyle(Color(.Text.title))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("\(summaryMang.teacher)")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
Text("선생님")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
}
}
//
HStack(spacing: 6) {
HStack(spacing: 10) {
CircleBtnView(vm: btnVM, id: ratioBtn)
VStack(spacing: 10) {
HStack(spacing: 4) {
Text("학습")
.font(.nps(size: 16))
.foregroundStyle(Color(.Text.detail))
Text("진도")
.font(.nps(font: .bold, size: 16))
.foregroundStyle(Color(.Text.detail))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("\(summaryMang.ratio)")
.font(.nps(font: .bold, size: 24))
.foregroundStyle(summaryMang.ratio > 70 ? Color(.Other.blue):Color(.Other.red))
Text("%")
.font(.nps(font: .bold, size: 20))
.foregroundStyle(Color(.Text.detail))
}
}
VStack(spacing: 10) {
HStack(spacing: 4) {
Text("숙제")
.font(.nps(font: .bold, size: 16))
.foregroundStyle(Color(.Text.detail))
Text("개수")
.font(.nps(size: 16))
.foregroundStyle(Color(.Text.detail))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("\(summaryMang.homework)")
.font(.nps(font: .bold, size: 24))
.foregroundStyle(Color(.Second.normal))
Text("")
.font(.nps(font: .bold, size: 20))
.foregroundStyle(Color(.Text.detail))
}
}
}
}
}
.padding(12)
.background {
RoundedRectangle(cornerRadius: 4)
.stroke(Color(.Second.normal), lineWidth: 2)
.fill(Color(.Second.light))
}
.onTapGesture {
// MARK: TO-DO
//
printLog(summaryMang)
}
}
}

View File

@ -0,0 +1,58 @@
//
// NoticeBoxView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct NoticeBoxView: View {
@State var noticeList: [SummaryNotice]
var body: some View {
DashBoardView(image: Image(.Icon.notice), title: "공지사항") {
ForEach(Array(noticeList.enumerated()), id: \.offset) { index, notice in
if index < 3 {
NotiCellView(summaryNoti: notice)
}
}
//
} moreAction: {
// MARK: TO-DO
//
printLog("공지사항의 더보기")
}
}
}
struct NotiCellView: View {
var summaryNoti: SummaryNotice
var body: some View {
VStack(spacing: 8) {
HStack(spacing: 12) {
Image(summaryNoti.new ? .Icon.noticeNew : .Icon.noticeOld)
.resizable()
.frame(width: 24, height: 24, alignment: .center)
Text("\(summaryNoti.title)")
.font(.nps(size: 20))
.foregroundStyle(Color(.Text.title))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("날짜 :")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
Text("\(summaryNoti.date)")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
}
}
}
}

View File

@ -10,7 +10,7 @@ import SwiftUI
struct TopProfileView: View { struct TopProfileView: View {
@StateObject var btnVM = ButtonViewModel() @StateObject var btnVM = ButtonViewModel()
var userType: UserType = .Parent var userType: UserType
// MARK: TO-DO // MARK: TO-DO
// //
@ -81,6 +81,8 @@ struct TopProfileView: View {
} }
} }
.fullDrawView(.Other.cell) .fullDrawView(.Other.cell)
.onAppear { .onAppear {
let topBtnIDList = [shopID,notifyID] let topBtnIDList = [shopID,notifyID]
let iconList = [Image(.Icon.market), Image(.Icon.notificationSET)] let iconList = [Image(.Icon.market), Image(.Icon.notificationSET)]
@ -132,6 +134,3 @@ struct TopProfileView: View {
} }
} }
#Preview {
TopProfileView()
}

View File

@ -1,81 +0,0 @@
//
// ManagementBoxView.swift
// AcaMate
//
// Created by TAnine on 2/10/25.
//
import SwiftUI
struct ManagementBoxView: View {
@StateObject var btnVM = ButtonViewModel()
@State private var leftBtnID = UUID()
@State private var rightBtnID = UUID()
var body: some View {
DashBoardView(image: Image(.Icon.edu), title: "학습 관리") {
} moreAction: {
printLog("학습 관리의 더보기")
}
}
}
struct MangCellView: View {
@StateObject var btnVM = ButtonViewModel()
let summaryMang: SummaryManagement
@State var ratioBtn = UUID()
@State var flagBtn = UUID()
var body: some View {
VStack(spacing: 8) {
HStack(spacing: 12) {
Image(.Icon.management)
.resizable()
.frame(width: 24, height: 24, alignment: .center)
Text("\(summaryMang.title)")
.font(.nps(size: 20))
.foregroundStyle(Color(.Text.title))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("\(summaryMang.teacher)")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
Text("이름")
.font(.nps(size: 12))
.foregroundStyle(Color(.Text.detail))
}
}
//
HStack(spacing: 6) {
HStack(spacing: 10) {
CircleBtnView(vm: btnVM, id: ratioBtn)
VStack(spacing: 10) {
HStack(spacing: 4) {
Text("학습")
.font(.nps(size: 16))
.foregroundStyle(Color(.Text.detail))
Text("진도")
.font(.nps(font: .bold, size: 16))
.foregroundStyle(Color(.Text.detail))
Spacer(minLength: 1)
}
HStack(spacing: 4) {
Spacer(minLength: 1)
Text("\(summaryMang.ratio)")
.font(.nps(font: .bold, size: 24))
.foregroundStyle(summaryMang.ratio > 70 ? Color(.Other.blue):Color(.Other.red))
Text("%")
.font(.nps(font: .bold, size: 20))
.foregroundStyle(Color(.Text.detail))
}
}
}
}
}
}

View File

@ -0,0 +1,14 @@
//
// ManagementView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct ManagementView: View {
var body: some View {
Text("학습 관리")
}
}

View File

@ -0,0 +1,14 @@
//
// ChattingView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct ChattingView: View {
var body: some View {
Text("채팅")
}
}

View File

@ -0,0 +1,14 @@
//
// CalendarView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct CalendarView: View {
var body: some View {
Text("일정")
}
}

View File

@ -0,0 +1,15 @@
//
// EtcView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct EtcView: View {
var body: some View {
Text("더보기")
}
}

View File

@ -17,6 +17,7 @@ struct BottomView: View {
@State private var calendarID = UUID() @State private var calendarID = UUID()
@State private var etcID = UUID() @State private var etcID = UUID()
@Binding var menuName: MenuName
var body: some View { var body: some View {
let idList: [UUID] = [homeID,managementID,chattingID,calendarID,etcID] let idList: [UUID] = [homeID,managementID,chattingID,calendarID,etcID]
@ -39,13 +40,14 @@ struct BottomView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onAppear { .onAppear {
let idList: [UUID] = [homeID,managementID,chattingID,calendarID,etcID] let idList: [UUID] = [homeID,managementID,chattingID,calendarID,etcID]
let btnText: [String] = ["", "학습 관리", "채팅", "일정", "더보기"] //["", " ", "", "", ""]
let btnType: [MenuName] = [.Home, .Management, .Chatting, .Calendar, .Etc]
let btnImage: [Image] = [Image(.Icon.home),Image(.Icon.management),Image(.Icon.chatting),Image(.Icon.calendar),Image(.Icon.etc)] let btnImage: [Image] = [Image(.Icon.home),Image(.Icon.management),Image(.Icon.chatting),Image(.Icon.calendar),Image(.Icon.etc)]
idList.enumerated().forEach { (index, id) in idList.enumerated().forEach { (index, id) in
btnVM.btnStates[id] = ButtonState() btnVM.btnStates[id] = ButtonState()
btnVM.setSize(for: id, newWidth: 52, newHeight: 52) btnVM.setSize(for: id, newWidth: 52, newHeight: 52)
btnVM.setText(for: id, newText: btnText[index], btnVM.setText(for: id, newText: btnType[index].rawValue,
newFont: .nps(font: .bold, size: 6)) newFont: .nps(font: .bold, size: 6))
btnVM.setImage(for: id, newImage: btnImage[index]) btnVM.setImage(for: id, newImage: btnImage[index])
@ -56,6 +58,7 @@ struct BottomView: View {
btnVM.setIsSelected(for: $0, newValue: false) btnVM.setIsSelected(for: $0, newValue: false)
} }
} }
menuName = btnType[index]
} }
} }

View File

@ -13,69 +13,44 @@ struct MainView: View {
@State var cancellables: Set<AnyCancellable> = [] @State var cancellables: Set<AnyCancellable> = []
@Binding var naviState : NaviState @Binding var naviState : NaviState
@State private var scrollOffset: CGPoint = .zero @State var menuName: MenuName
@State private var topViewState: Bool = false
// @State private var scrollOffset: CGPoint = .zero
// @State private var topViewState: Bool = false
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ZStack { Group {
OffsetObservableScrollView(scrollOffset: $scrollOffset) { proxy in switch menuName {
VStack(spacing: 0) { case .Home:
TopProfileView() HomeView()
.padding(EdgeInsets(top: 0, leading: 0, bottom: 12, trailing: 0)) case .Management:
AttendanceBoxView() ManagementView()
.background { case .Chatting:
RoundedRectangle(cornerRadius: 8) ChattingView()
.foregroundStyle(Color(.Other.cell)) case .Calendar:
} CalendarView()
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) case .Etc:
AttendanceBoxView() EtcView()
.background {
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(Color(.Other.cell))
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
AttendanceBoxView()
.background {
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(Color(.Other.cell))
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
CalendarBoxView(summaryCalDataList: [
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다."),
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다.")])
.background {
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(Color(.Other.cell))
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
}
}
if topViewState {
VStack(spacing: 0) {
TopView(titleName: "Name")
.transition(.move(edge: .top))
.animation(.easeInOut, value: scrollOffset)
Spacer(minLength: 1)
}
} }
} }
BottomView() Spacer(minLength: 1)
BottomView(menuName: $menuName)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.onChange(of: scrollOffset.y) { oldValue, newValue in //
if newValue > 200 && topViewState == false { // .onChange(of: scrollOffset.y) { oldValue, newValue in
topViewState = true // if newValue > 200 && topViewState == false {
} else if newValue < 20 && topViewState == true{ // topViewState = true
topViewState = false // } else if newValue < 20 && topViewState == true{
} // topViewState = false
} // }
// }
} }
} }

View File

@ -21,6 +21,8 @@ struct ButtonState {
var textColor: Color = .Text.detail var textColor: Color = .Text.detail
var isUsable: Bool = true var isUsable: Bool = true
// -- CircleBtn -- // // -- CircleBtn -- //
var isSelected: Bool = false var isSelected: Bool = false

View File

@ -13,6 +13,8 @@ struct NaviState: Equatable {
var act: NaviAction var act: NaviAction
var path: PathName var path: PathName
static func == (lhs: NaviState, rhs: NaviState) -> Bool { static func == (lhs: NaviState, rhs: NaviState) -> Bool {
return lhs.act == rhs.act && lhs.path == rhs.path return lhs.act == rhs.act && lhs.path == rhs.path
} }
@ -21,6 +23,7 @@ struct NaviState: Equatable {
self.act = act self.act = act
self.path = path self.path = path
} }
} }
enum NaviAction: Hashable { enum NaviAction: Hashable {
@ -45,3 +48,11 @@ enum PathName: Hashable {
case NONE case NONE
} }
enum MenuName: String, Hashable {
case Home = ""
case Management = "학습 관리"
case Chatting = "채팅"
case Calendar = "일정"
case Etc = "더보기"
}

View File

@ -0,0 +1,15 @@
//
// Notice Data.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import Foundation
struct SummaryNotice {
var id: String
var title: String
var date: String
var new: Bool
}

View File

@ -10,7 +10,6 @@ import Combine
class AppViewModel: ObservableObject { class AppViewModel: ObservableObject {
@Published var isLoading: Bool = false @Published var isLoading: Bool = false
@Published var showAlert: Bool = false @Published var showAlert: Bool = false
var alertData: AlertData = .init(body: "") var alertData: AlertData = .init(body: "")

View File

@ -60,6 +60,13 @@ class ButtonViewModel: ObservableObject {
btnStates[id] = state btnStates[id] = state
objectWillChange.send() objectWillChange.send()
} }
//
// func setDisable(for id: UUID, _ newValue: Bool) {
// var state = btnStates[id] ?? ButtonState()
// state.isUsable = newValue
// btnStates[id] = state
// objectWillChange.send()
// }
// -- CircleBtn -- // // -- CircleBtn -- //