Skip to content

pcsoyeon/SSAC-Mogakko

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SeSAC Study Matching App

🌱 지도λ₯Ό 톡해 μ£Όλ³€μ˜ μƒˆμ‹Ήλ“€μ„ ν™•μΈν•˜κ³ , μŠ€ν„°λ””λ₯Ό μš”μ²­ 및 1:1 μ±„νŒ…μ„ ν•˜λŠ” μ„œλΉ„μŠ€

Service Level Project 
- μ‹€μ œ 업무 ν™˜κ²½κ³Ό λ™μΌν•œ ν”„λ‘œμ„ΈμŠ€λ‘œ μ§„ν–‰λ˜λŠ” 개인 ν”„λ‘œμ νŠΈ
- μ„œλΉ„μŠ€ 레벨의 API/기획 λͺ…μ„Έμ„œμ™€ λ””μžμΈ λ¦¬μ†ŒμŠ€λ₯Ό 전달 λ°›μ•„μ„œ μ•± 개발 진행

Screen Shot

Slide 16_9 - 2


Term

  • 2022.11.07 ~ 2022.12.07

Link

πŸ”— Notion Link


Tool

  • 기획 λͺ…μ„Έμ„œ : Conference
  • λ””μžμΈ μ‹œμŠ€ν…œ 및 UI : Figma
  • μ„œλ²„ API : Conference/Swagger
  • νŒ€ μ§ˆμ˜μ‘λ‹΅ : Zep

Stack (+ Library)

  • MVC, MVVM(Input/Output) Pattern
  • Swift, UIKit
  • AutoLayout, Snapkit, Then
  • RxSwift
  • Alamofire (Alamofire+URLRequestConvertible)
  • CLLocation, MapKit
  • Firebase
    • Push Notification
    • Login (μ „ν™”λ²ˆν˜Έ 둜그인/jwt Token)
  • CompositionalLayout, DiffableDataSource

Trouble Shooting

  • 토큰 만료 (jwt token λ§Œλ£Œμ‹œ, 401 error에 λŒ€ν•œ λŒ€μ‘)
    func refreshIdToken(completion: @escaping (Result<String, Error>) -> Void) {
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error = error {
                completion(.failure(error))
            } else {
                guard let idToken = idToken else { return }
                UserData.idtoken = idToken
                completion(.success(idToken))
            }
        }
    }

  • enum으둜 API Error λΆ„κΈ°μ²˜λ¦¬
@frozen
enum APIError: Int, Error {
    case takenUser = 201
    case invalidNickname = 202
    case invalidAuthorization = 401
    case unsubscribedUser = 406
    case serverError = 500
    case emptyParameters = 501
}

extension APIError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .takenUser:
            return "이미 κ°€μž…ν•œ μœ μ €μž…λ‹ˆλ‹€."
        case .invalidNickname:
            return "μœ νš¨ν•˜μ§€ μ•Šμ€ λ‹‰λ„€μž„μž…λ‹ˆλ‹€."
        case .invalidAuthorization:
            return "토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ 둜그인 ν•΄μ£Όμ„Έμš”."
        case .unsubscribedUser:
            return "아직 κ°€μž…ν•˜μ§€ μ•Šμ€ μœ μ €μž…λ‹ˆλ‹€."
        case .serverError:
            return "μ„œλ²„ μ—λŸ¬μž…λ‹ˆλ‹€. μž μ‹œ ν›„ μ΄μš©ν•΄μ£Όμ„Έμš”."
        case .emptyParameters:
            return "request header/bodyλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”."
        }
    }
}
private func checkIdToken() {
        GenericAPI.shared.requestDecodableData(type: Login.self, router: UserRouter.login) { [weak self] response in
            print("κΈ°μ‘΄ - ", UserData.idtoken)
            guard let self = self else { return }
            
            switch response {
            case .success(let data):
                UserData.nickName = data.nick
                
                Helper.convertNavigationRootViewController(view: self.view, controller: TabBarViewController())
                
            case .failure(let error):
                switch error {
                case .takenUser, .invalidNickname:
                    return
                case .invalidAuthorization:
                    UserAPI.shared.refreshIdToken { result in
                        switch result {
                        case .success(let idtoken):
                            print("κ°±μ‹  - ", UserData.idtoken)
                            self.refreshToken(idtoken)
                            
                        case .failure(let error):
                            print(error.localizedDescription)
                            return
                        }
                    }
                case .unsubscribedUser:
                    Helper.convertNavigationRootViewController(view: self.view, controller: NicknameViewController())
                case .serverError:
                    self.showToast(message: "μ„œλ²„ λ‚΄λΆ€ 였λ₯˜μž…λ‹ˆλ‹€. μž μ‹œ ν›„ 재인증 ν•΄μ£Όμ„Έμš”.")
                case .emptyParameters:
                    self.showToast(message: "Client Error")
                }
                
            }
        }
    }

  • λ‚΄ μƒνƒœμ— λ”°λ₯Έ 지도 ν™”λ©΄ Floating Button νƒ€μž… 및 ν™”λ©΄μ „ν™˜ λΆ„κΈ°μ²˜λ¦¬
enum MDSFloatingButtonType {
    case plain
    case matching
    case matched
    
    var image: UIImage {
        switch self {
        case .plain:
            return Constant.Image.search
        case .matching:
            return Constant.Image.antenna
        case .matched:
            return Constant.Image.message
        }
    }
}
            floatingButton.rx.tap
            .withUnretained(self)
            .bind { vc, _ in
                if vc.floatingButton.type == .plain {
                    let viewController = StudyViewController()
                    viewController.viewModel.mapLatitude.accept(vc.mapLatitude)
                    viewController.viewModel.mapLongitude.accept(vc.mapLongitude)
                    vc.navigationController?.pushViewController(viewController, animated: true)
                } else if vc.floatingButton.type == .matching {
                    // 맀칭쀑
                    let studyViewController = StudyViewController()
                    let searchViewController = SearchSesacViewController()
                    searchViewController.mapLatitude = vc.mapLatitude
                    searchViewController.mapLongitude = vc.mapLongitude
                    searchViewController.stateType = .matching
                    vc.navigationController?.pushViewControllers([studyViewController, searchViewController], animated: false)
                } else {
                    // 맀칭된 > μ±„νŒ…ν™”λ©΄μœΌλ‘œ 이동
                    let viewController = ChatViewController()
                    vc.navigationController?.pushViewController(viewController, animated: true)
                }
            }
            .disposed(by: disposeBag)

πŸ€” λ„€λΉ„κ²Œμ΄μ…˜μœΌλ‘œ 화면이 μ—°κ²°λ˜μ–΄ μžˆμ„ λ•Œ, ν•œλ²ˆμ— μ—¬λŸ¬ 화면을 pushν•˜κ³  μ‹Άλ‹€λ©΄?

extension UINavigationController {
    func pushViewControllers(_ inViewControllers: [UIViewController], animated: Bool) {
        var stack = self.viewControllers
        stack.append(contentsOf: inViewControllers)
        self.setViewControllers(stack, animated: animated)
    }
}

  • 동적 λ ˆμ΄μ•„μ›ƒ κ΅¬ν˜„ Card Type에 λ”°λΌμ„œ λ³΄μ—¬μ§€λŠ” μš”μ†Œκ°€ λ‹€λ₯΄λ―€λ‘œ, enum으둜 caseλ₯Ό λ‚˜λˆ μ„œ 관리
@frozen
enum CardViewType {
    case plain
    case info
}


Retrospect

SLP ν”„λ‘œμ νŠΈλ₯Ό 톡해 μ‹€μ œ μ„œλΉ„μŠ€(νšŒμ‚¬) 규λͺ¨μ˜ 기획, λ””μžμΈ, μ„œλ²„λ₯Ό ν™•μΈν•˜κ³  그에 맞게 기술 μŠ€νƒμ„ μ •ν•œ λ’€ 일정에 맞게 ν”„λ‘œμ νŠΈλ₯Ό κ΅¬ν˜„ν•˜λŠ” κ²½ν—˜μ„ ν•  수 μžˆμ—ˆλ‹€. λͺ¨λ“  기획과 λ””μžμΈ, μ„œλ²„κ°€ ν™•μ •λœ μƒνƒœλ‘œ κ°œλ°œμ„ μ‹œμž‘ν•œ 것이 μ•„λ‹ˆλΌ, κ°œλ°œμ„ μ§„ν–‰ν•˜λŠ” 도쀑에 μ„œλ²„κ°€ λ³€κ²½λ˜κΈ°λ„ ν•˜κ³  기획이 더 μΆ”κ°€λ˜κΈ°λ„ ν•˜λŠ” 상황 μ†μ—μ„œ μœ μ—°ν•˜κ²Œ λŒ€μ²˜ν•˜λ©° κ°œλ°œμ„ μ§„ν–‰ν–ˆλ‹€. 이 κ³Όμ • μ†μ—μ„œ μ½”λ“œλ₯Ό μž¬μ‚¬μš©ν•  수 있게 λΆ„λ¦¬ν•˜λŠ” 것과 λ‘œμš°ν•œ 값을 μ‚¬μš©ν•˜μ§€ μ•Šμ•„μ•Ό ν•œλ‹€λŠ” 것을 κΉ¨λ‹¬μ•˜λ‹€.


Figma와 기획λͺ…μ„Έμ„œλ₯Ό λ³΄λ©΄μ„œ μž¬μ‚¬μš©ν•  수 μžˆλŠ” 뢀뢄듀이 μžˆλ‹€λ©΄ λ”°λ‘œ λ””μžμΈμ‹œμŠ€ν…œμœΌλ‘œ κ΄€λ¦¬ν•˜κ±°λ‚˜ λͺ¨λΈλ‘œ κ΄€λ¦¬ν•˜λŠ” μœ μ—°ν•œ 사고λ₯Ό λ°°μ› λ‹€. 특히, λΉ„μŠ·ν•œ UI둜 λ™μž‘λ˜λŠ” 화면듀이 λ§Žμ•˜κΈ° λ•Œλ¬Έμ— μ΅œλŒ€ν•œ λΆ„λ¦¬ν•΄μ„œ enum으둜 caseλ₯Ό λ‚˜λˆ  κ΄€λ¦¬ν•˜μ˜€λ‹€.


Firebase μ „ν™”λ²ˆν˜Έ 둜그인, WebSocket을 μ΄μš©ν•œ 1:1 μ±„νŒ…, IAP λ“±μ˜ μƒˆλ‘œμš΄ κΈ°λŠ₯/κΈ°μˆ μ„ κ΅¬ν˜„ν•˜λ©΄μ„œ λ‹€μ–‘ν•œ 곡뢀λ₯Ό ν•  수 μžˆμ—ˆλ‹€.
μ±„νŒ…μ˜ 경우 μ†ŒμΌ“κ³Ό μ–Έμ œ 연결을 ν•˜κ³  ν•΄μ œν•˜λ©°, Realm을 기반으둜 μ±„νŒ… 내역을 μ €μž₯ 및 뢈러였고 λ‹€μ‹œ μƒˆλ‘œμš΄ μ±„νŒ…μ„ μΆ”κ°€ν•˜λŠ” λ“±μ˜ μ „λ°˜μ μΈ ν”„λ‘œμ„ΈμŠ€λ₯Ό 배울 수 μžˆμ—ˆλ‹€.


μ½”λ“œλ₯Ό κΉ”λ”ν•˜κ²Œ μž‘μ„±ν•˜λŠ” 것도 μ€‘μš”ν•˜μ§€λ§Œ, μ–΄λŠ ν”„λ‘œμ νŠΈμ™€ λ§ˆμ°¬κ°€μ§€λ‘œ κΈ°ν•œμ΄ 있기 λ•Œλ¬Έμ— λ™μž‘μ΄ 되고 완성도가 μžˆλŠ” 결과물을 λ‚΄μ•Ό ν•œλ‹€λŠ” 점도 μžŠμ§€ μ•ŠμœΌλ©΄μ„œ μž‘μ—…μ„ μ§„ν–‰ν–ˆλ‹€. 그렇기에, λ™μž‘μ΄ λ˜λ„λ‘ κ΅¬ν˜„ν•˜κ³  κ°œμ„ ν•˜λŠ” 과정을 λ°˜λ³΅ν•˜λ©΄μ„œ 진도λ₯Ό λ‚˜κ°”λ‹€. κ·ΈλŸΌμ—λ„ μ½”λ“œκ°€ 덜 μ •λ¦¬λœ 뢀뢄듀이 μžˆμ–΄ 아쉬움이 λ‚¨λŠ”λ‹€.

About

πŸ€ Service Level Project

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages