[] 학원 선택 페이지 작성 및 기존 코드 로직 분리 작업

This commit is contained in:
Seonkyu_Kim 2025-02-19 15:59:03 +09:00
parent ca49db680c
commit 71b2c3389f
11 changed files with 341 additions and 283 deletions

View File

@ -7,9 +7,12 @@
import SwiftUI import SwiftUI
// MARK: - ACAMATE // MARK: - ACAMATE
// APPSTORE_URL : https://apps.apple.com/us/app/%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8%EB%A9%94%EC%9D%B4%ED%8A%B8/id6739448113 // APPSTORE_URL : https://apps.apple.com/us/app/%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8%EB%A9%94%EC%9D%B4%ED%8A%B8/id6739448113
///
/// plist http
#if LOCAL #if LOCAL
//public let API_URL: String = "http://0.0.0.0:5144" public let API_URL: String = "http://10.149.217.64:5144"
public let API_URL: String = "https://localhost:7086" //public let API_URL: String = "http://localhost:5144"
//public let API_URL: String = "https://localhost:7086"
public let WS_URL: String = "ws://localhost:5144" public let WS_URL: String = "ws://localhost:5144"
/// URL /// URL

View File

@ -1,117 +1,117 @@
////
//// AccountLoginView.swift
//// AcaMate
////
//// Created by Sean Kim on 12/14/24.
////
// //
// AccountLoginView.swift //import SwiftUI
// AcaMate
// //
// Created by Sean Kim on 12/14/24. //struct AccountLoginView: View {
// // @ObservedObject var loginVM.: LoginViewModel
// @Binding var userId: String
import SwiftUI // @Binding var password: String
// @Binding var isSecure: Bool
struct AccountLoginView: View { // @Binding var isSave: Bool
@ObservedObject var viewModel: LoginViewModel // var body: some View {
@Binding var userId: String // VStack(spacing: 0) {
@Binding var password: String // ZStack(alignment: .leading) {
@Binding var isSecure: Bool // if userId.isEmpty {
@Binding var isSave: Bool // Text(" .")
var body: some View { // .font(.nps(font: .regular, size: 16))
VStack(spacing: 0) { // .foregroundStyle(Color(.Text.border))
ZStack(alignment: .leading) { // .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
if userId.isEmpty { // }
Text("아이디를 입력하세요.") // CustomTextField(placeholder: "", text: $userId)
.font(.nps(font: .regular, size: 16)) // .frame(maxWidth: .infinity,maxHeight: 24)
.foregroundStyle(Color(.Text.border)) // .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) // }
} // .background {
CustomTextField(placeholder: "", text: $userId) // RoundedRectangle(cornerRadius: 24)
.frame(maxWidth: .infinity,maxHeight: 24) // .foregroundStyle(.white)
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) // }
} // .padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
.background { //
RoundedRectangle(cornerRadius: 24) // ZStack(alignment: .leading) {
.foregroundStyle(.white) // if password.isEmpty {
} // Text(" .")
.padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12)) // .font(.nps(font: .regular, size: 16))
// .foregroundStyle(Color(.Text.border))
ZStack(alignment: .leading) { // .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
if password.isEmpty { // }
Text("비밀번호를 입력하세요.") // CustomTextField(placeholder: "", text: $password, isSecure: $isSecure)
.font(.nps(font: .regular, size: 16)) // .frame(maxWidth: .infinity,maxHeight: 24)
.foregroundStyle(Color(.Text.border)) // .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) //
} // HStack {
CustomTextField(placeholder: "", text: $password, isSecure: $isSecure) // Spacer()
.frame(maxWidth: .infinity,maxHeight: 24) // Button {
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) // isSecure.toggle()
// } label: {
HStack { // if password.isEmpty {
Spacer() // Rectangle()
Button { // .frame(width: 16, height: 2)
isSecure.toggle() // .foregroundStyle(Color(.Text.border))
} label: { // .padding(.trailing,24)
if password.isEmpty { // }
Rectangle() // else {
.frame(width: 16, height: 2) // if isSecure {
.foregroundStyle(Color(.Text.border)) // Image(systemName: "eye")
.padding(.trailing,24) // .frame(width: 16, height: 16)
} // .foregroundStyle(Color(.Text.detail))
else { // .padding(.trailing,24)
if isSecure { // }
Image(systemName: "eye") // else {
.frame(width: 16, height: 16) // Image(systemName: "eye.slash")
.foregroundStyle(Color(.Text.detail)) // .frame(width: 16, height: 16)
.padding(.trailing,24) // .foregroundStyle(Color(.Text.detail))
} // .padding(.trailing,24)
else { //
Image(systemName: "eye.slash") // }
.frame(width: 16, height: 16) // }
.foregroundStyle(Color(.Text.detail)) // }
.padding(.trailing,24) // }
// }
} // .background {
} // RoundedRectangle(cornerRadius: 24)
} // .foregroundStyle(.white)
} // }
} // .padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
.background { //
RoundedRectangle(cornerRadius: 24) // Button {
.foregroundStyle(.white) // isSave.toggle()
} // } label: {
.padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12)) // HStack(alignment: .center, spacing: 4) {
// Spacer(minLength: 1)
Button { // if isSave {
isSave.toggle() // Image(systemName: "checkmark.square")
} label: { // .foregroundStyle(Color(.Second.normal))
HStack(alignment: .center, spacing: 4) { // .frame(width: 24, height: 24)
Spacer(minLength: 1) // } else {
if isSave { // Image(systemName: "square")
Image(systemName: "checkmark.square") // .foregroundStyle(Color(.Second.normal))
.foregroundStyle(Color(.Second.normal)) // .frame(width: 24, height: 24)
.frame(width: 24, height: 24) // }
} else { //
Image(systemName: "square") // Text(" ")
.foregroundStyle(Color(.Second.normal)) // .font(.nps(font: .regular, size: 16))
.frame(width: 24, height: 24) // .foregroundStyle(Color(.Text.detail))
} // }
// }
Text("로그인 정보 저장") // .padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12))
.font(.nps(font: .regular, size: 16)) //
.foregroundStyle(Color(.Text.detail)) // Button {
} // loginVM..loginAction.send(true)
} // } label: {
.padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12)) // Text("")
// .font(.nps(font: .bold, size: 24))
Button { // .foregroundStyle(Color(.Text.white))
viewModel.loginAction.send(true) // .padding(EdgeInsets(top: 8, leading: 48, bottom: 8, trailing: 48))
} label: { // .background{
Text("로그인") // RoundedRectangle(cornerRadius: 12)
.font(.nps(font: .bold, size: 24)) // .foregroundStyle(Color(.Second.normal))
.foregroundStyle(Color(.Text.white)) // }
.padding(EdgeInsets(top: 8, leading: 48, bottom: 8, trailing: 48)) // }
.background{ // }
RoundedRectangle(cornerRadius: 12) // }
.foregroundStyle(Color(.Second.normal)) //}
}
}
}
}
}

View File

@ -11,16 +11,6 @@ import Combine
struct LoginView: View { struct LoginView: View {
@EnvironmentObject var appVM: AppViewModel @EnvironmentObject var appVM: AppViewModel
@StateObject private var loginVM = LoginViewModel() @StateObject private var loginVM = LoginViewModel()
@State var cancellables: Set<AnyCancellable> = []
// @Binding var naviState : NaviState
@State var selectIdLogin: Bool = false
@State var userId: String = ""
@State var password: String = ""
@State var isSecure: Bool = true
@State var isSave: Bool = false
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -28,22 +18,22 @@ struct LoginView: View {
Image(.Logo.appIcon) Image(.Logo.appIcon)
.resizable() .resizable()
.frame(width: 200, height: 200) .frame(width: 200, height: 200)
// .padding(.top, 80) // .padding(.top, 80)
.padding(.bottom, 84) .padding(.bottom, 84)
/// ///
VStack(spacing: 16) { VStack(spacing: 16) {
Button { Button {
// MARK: - TODO, // MARK: - TODO,
appVM.isLoading.toggle() loginVM.toggleLoading = true
loginAction(type: .Kakao) loginVM.loginAction(type: .Kakao)
} label: { } label: {
makeButton(image: Image(.Logo.kakaoIcon),color: Color(.Other.yellow), "카카오 계정으로 시작하기") makeButton(image: Image(.Logo.kakaoIcon),color: Color(.Other.yellow), "카카오 계정으로 시작하기")
} }
Button { Button {
// MARK: - TODO, // MARK: - TODO,
// appVM.naviState.set(act: .MOVE, path: .Main)
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: ["AA0000", "AA0001"])) appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: ["AA0000", "AA0001"]))
} label: { } label: {
@ -53,66 +43,18 @@ struct LoginView: View {
.padding([.leading,.trailing], 28) .padding([.leading,.trailing], 28)
Spacer(minLength: 1) Spacer(minLength: 1)
/*
VStack(spacing: 16) {
Button {
// MARK: TO-DO
//
naviState.set(act: .MOVE, path: .Main)
} label: {
HStack(spacing: 24) {
Image("Kakao_Icon")
.resizable()
.frame(width: 32, height: 32)
Text("카카오 계정으로 시작하기")
.font(.nps(font: .regular, size: 16))
.foregroundStyle(Color(.Text.black))
}
.frame(maxWidth: .infinity)
.padding(12)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundStyle(Color(.Other.yellow))
}
}
.frame(maxWidth: .infinity)
/// KAKAO
Button {
// MARK: TO-DO
//
} label: {
HStack(spacing: 24) {
Image(systemName: "apple.logo")
.resizable()
.accentColor(Color(.Text.white))
.frame(width: 32, height: 32)
Text("애플 계정으로 시작하기")
.font(.nps(font: .regular, size: 16))
.foregroundStyle(Color(.Text.white))
}
.frame(maxWidth: .infinity)
.padding(12)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundStyle(Color(.Text.black))
}
}
.frame(maxWidth: .infinity)
/// APPLE
}
*/
}
.onAppear {
subscribeLoginAction()
} }
.frame(maxWidth: .infinity,maxHeight: .infinity) .frame(maxWidth: .infinity,maxHeight: .infinity)
.fullDrawView(.Normal.normal) .fullDrawView(.Normal.normal)
.onChange(of: loginVM.navigateToAcademy, { _, _ in
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: loginVM.bidArray))
})
.onChange(of: loginVM.toggleLoading) { _, new in
appVM.isLoading = new
}
} }
func makeButton(image: Image, color: Color? = nil, _ body: String) -> some View { func makeButton(image: Image, color: Color? = nil, _ body: String) -> some View {
@ -134,58 +76,4 @@ struct LoginView: View {
} }
} }
} }
private func subscribeLoginAction() {
loginVM.loginAction
.sink { isTapped in
if isTapped {
if userId.isEmpty || password.isEmpty {
appVM.alertData = SetAlertData().setErrorLogin()
appVM.showAlert.toggle()
}
else {
}
printLog("로그인")
}
}
.store(in: &cancellables)
}
private func loginAction(type: SNSLoginType) {
LoginController().login(type)
.flatMap{ snsId in
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/user/login",
parameters: [
"sns_id": "\(snsId.snsId)",
"acctype": "\(type == .Apple ? "ST00": "ST01")"
],
decodingType: APIResponse<User_Academy>.self)
}
.sink { completion in
switch completion {
case .failure(let error):
printLog("\(error)")
appVM.isLoading.toggle()
case .finished:
appVM.isLoading.toggle()
}
} receiveValue: { response in
guard let ua = response as? APIResponse<User_Academy> else {return}
if let bids = ua.data.toStringDict()["bid"] {
printLog(bids)
if let bidArray: [String] = jsonToSwift(bids) {
// ID
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: bidArray))
} else {
printLog("JSON 변환 실패")
}
}
}
.store(in: &cancellables)
}
} }

View File

@ -6,56 +6,131 @@
// //
import SwiftUI import SwiftUI
import Combine
struct SelectAcademyView: View { struct SelectAcademyView: View {
@State var cancellables: Set<AnyCancellable> = [] @EnvironmentObject var appVM: AppViewModel
@StateObject var saVM = SelectAcademyViewModel()
@State private var scrollOffset: CGPoint = .zero
var bids: [String] var bids: [String]
@State private var academyCode: String = ""
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
.frame(height: 100) .frame(maxHeight: 100)
Image(.Logo.appIcon) Image(.Logo.appIcon)
.resizable() .resizable()
.frame(width: 200, height: 200) .frame(width: 200, height: 200)
CustomTextField(placeholder: "학원 코드 입력", text: $academyCode)
.frame(maxWidth: .infinity,maxHeight: 48) VStack(spacing: 4) {
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) HStack(spacing: 0){
.background { Text("학원 코드")
RoundedRectangle(cornerRadius: 24) .font(.nps(font: .bold, size: 16))
.foregroundStyle(Color(.Text.detail))
Spacer(minLength: 1)
}
//MARK: TO-DO
//
// 1. txf
// 2.
CustomTextField(placeholder: "학원 코드 입력", text: $saVM.academyCode)
.frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 40, trailing: 24))
VStack(spacing: 4) {
HStack(spacing: 0){
Text("학원 목록")
.font(.nps(font: .bold, size: 16))
.foregroundStyle(Color(.Text.detail))
Spacer(minLength: 1)
}
.padding(EdgeInsets(top: 12, leading: 24, bottom: 0, trailing: 24))
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
VStack(spacing: 12) {
ForEach(Array(saVM.academyList.enumerated()), id: \.offset) { index, academy in
AcademyCell(numbering: index, academy: saVM.academyList[index],selectNum: $saVM.selectNum){
saVM.toggleSelection(for: index)
}
}
}
.padding(EdgeInsets(top: 0, leading: 24, bottom: 12, trailing: 24))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Spacer(minLength: 1)
Button {
appVM.naviState.set(act: .MOVE, path: .Main)
} label: {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color(.Second.normal))
Text("입장하기")
.font(.nps(size: 16))
.foregroundStyle(Color(.Normal.light)) .foregroundStyle(Color(.Normal.light))
} }
// .padding([.leading, .trailing], 24) .frame(height: 56)
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24)) }
.opacity(saVM.selectNum >= 0 ? 1 : 0)
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
} }
.onAppear { .onAppear {
loadAPIData(url: "\(API_URL)", saVM.loadAcademy(bids: bids)
path: "/api/v1/in/member/academy",
method: .post,
parameters: ["bids": bids],
decodingType: APIResponse<[AcademyName]>.self)
.sink { completion in
switch completion {
case .failure(let error):
printLog("\(error)")
// appVM.isLoading.toggle()
case .finished:
break
// appVM.isLoading.toggle()
}
} receiveValue: { response in
guard let ua = response as? APIResponse<[AcademyName]> else {return}
printLog(ua)
}
.store(in: &cancellables)
} }
} }
} }
struct AcademyCell: View {
let numbering: Int
let academy: AcademyName
@Binding var selectNum: Int
let action: VOID_TO_VOID
var body: some View {
HStack(alignment: .center, spacing: 0) {
Image(.Logo.pageIcon)
.resizable()
.frame(width: 32, height: 32, alignment: .center)
.padding(12)
Spacer(minLength: 1)
Text("\(academy.name)")
.font(.nps(size: 18))
.foregroundStyle(Color(.Text.detail))
.multilineStyle(.center)
Spacer(minLength: 1)
Button{
// saVM.toggleSelection(for: numbering)
action()
} label: {
Circle()
.stroke(Color(.Text.detail), lineWidth: 4)
.fill (selectNum == numbering ? Color(.Point.normal) : Color(.Normal.normal))
.frame(width: 18, height: 18)
}
.frame(width: 32, height: 32)
.padding(12)
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(Color(.Second.normal), lineWidth: 2)
}
.onTapGesture {
action()
}
}
}

View File

@ -8,6 +8,49 @@
import SwiftUI import SwiftUI
import Combine import Combine
class LoginViewModel: ObservableObject { class LoginViewModel: ObservableObject {
let loginAction = CurrentValueSubject<Bool, Never>(false) private var cancellables = Set<AnyCancellable>()
@Published var toggleLoading: Bool = false
@Published var navigateToAcademy: Bool = false
@Published var bidArray: [String] = []
func loginAction(type: SNSLoginType) {
LoginController().login(type)
.flatMap{ snsId in
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/user/login",
parameters: [
"acctype": "\(type == .Apple ? "ST00": "ST01")",
"sns_id": "\(snsId.snsId)"
],
decodingType: APIResponse<User_Academy>.self)
}
.sink { [weak self] completion in
switch completion {
case .failure(let error):
printLog("\(error)")
self?.toggleLoading = false
case .finished:
self?.toggleLoading = false
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
guard let ua = response as? APIResponse<User_Academy> else {return}
if let bids = ua.data.toStringDict()["bid"] {
printLog(bids)
if let bidArray: [String] = jsonToSwift(bids) {
// ID
printLog("GOOD")
self.navigateToAcademy = true
self.bidArray = bidArray
} else {
printLog("JSON 변환 실패")
}
}
}
.store(in: &cancellables)
}
} }

View File

@ -0,0 +1,45 @@
//
// SelectAcademyViewModel.swift
// AcaMate
//
// Created by TAnine on 2/19/25.
//
import SwiftUI
import Combine
class SelectAcademyViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
@Published var academyCode: String = ""
@Published var academyList: [AcademyName] = []
@Published var selectNum: Int = -1
func loadAcademy(bids: [String]) {
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/member/academy",
method: .post,
parameters: ["bids": bids],
decodingType: APIResponse<[AcademyName]>.self)
.sink { completion in
switch completion {
case .failure(let error):
printLog("\(error)")
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let self = self else {return}
guard let academyNames = response as? APIResponse<[AcademyName]> else {return}
self.academyList = academyNames.data
}
.store(in: &cancellables)
}
func toggleSelection(for index: Int){
if selectNum == index {
selectNum = -1
} else {
selectNum = index
}}
}

View File

@ -11,7 +11,6 @@ import KakaoSDKCommon
import KakaoSDKAuth import KakaoSDKAuth
import KakaoSDKUser import KakaoSDKUser
import Alamofire
import Foundation import Foundation
class LoginController { class LoginController {
@ -29,7 +28,6 @@ class LoginController {
} }
}) })
.eraseToAnyPublisher() .eraseToAnyPublisher()
//
case .Apple: case .Apple:
return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil)) return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil))

View File

@ -16,12 +16,13 @@ public func loadAPIData<T: Decodable>(url: String, path: String,
parameters: [String: Any], parameters: [String: Any],
headers: HTTPHeaders = [:],//["Accept": "application/json"], headers: HTTPHeaders = [:],//["Accept": "application/json"],
decodingType: T.Type) -> Future<Any, Error> { decodingType: T.Type) -> Future<Any, Error> {
let encoding: ParameterEncoding = (method == .get) ? URLEncoding.default : JSONEncoding.default
return Future { promise in return Future { promise in
printLog(parameters) printLog(parameters)
AF.request("\(url)\(path)", AF.request("\(url)\(path)",
method: method, method: method,
parameters: parameters, parameters: parameters,
encoding: JSONEncoding.default, encoding: encoding,
headers: headers headers: headers
) )
.validate(statusCode: 200 ..< 300) .validate(statusCode: 200 ..< 300)
@ -38,3 +39,5 @@ public func loadAPIData<T: Decodable>(url: String, path: String,
} }
} }
} }

View File

@ -8,10 +8,6 @@
import SwiftUI import SwiftUI
protocol MultilineStyle: View { protocol MultilineStyle: View {
// func lineLimit(_ limit: Int?) -> Self
// func minimumScaleFactor(_ scale: CGFloat) -> Self
// func multilineTextAlignment(_ alignment: TextAlignment) -> Self
// func truncationMode(_ mode: Text.TruncationMode) -> Self
} }
extension Text: MultilineStyle { extension Text: MultilineStyle {

View File

@ -21,6 +21,13 @@
<dict> <dict>
<key>NSExceptionDomains</key> <key>NSExceptionDomains</key>
<dict> <dict>
<key>10.149.217.64</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>ipstein.myds.me</key> <key>ipstein.myds.me</key>
<dict> <dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <key>NSExceptionAllowsInsecureHTTPLoads</key>