Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GWL-70] Record 만들어진 UI ViewModel 구현 #95

Merged
merged 28 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5f551f7
feat: WorkoutEnviorment ViewModel 생성
MaraMincho Nov 21, 2023
7bd90a6
feat: injectable, WorkOutEnvironmentSetupSyringe 구현
MaraMincho Nov 21, 2023
90e9983
feat: workoutSettingCoordinator 프로토콜 변경
MaraMincho Nov 21, 2023
82819e8
Merge remote-tracking branch 'origin/develop' into feature/iOS/GWL-70
MaraMincho Nov 21, 2023
8835f05
chore: 폴더 이름 변경 workoutSelectScene -> WorkoutEnvironmentScene
MaraMincho Nov 21, 2023
93d7071
chore: 폴더 구조 재확립
MaraMincho Nov 21, 2023
14ef606
feat: DIContainer 구현
MaraMincho Nov 21, 2023
a2f91f0
feat: useCase에서 Task진입으로 코드 변경
MaraMincho Nov 21, 2023
d964850
feat: EnviormentVC연결
MaraMincho Nov 21, 2023
7394c94
chore: 변수 명 수정
MaraMincho Nov 21, 2023
3c6393c
feat: workoutEnvironment 내부 navigationContainerController 페이지 이동 구현
MaraMincho Nov 22, 2023
ad958a1
feat: persistency에서 PeerType 가져오는 로직 구현
MaraMincho Nov 22, 2023
2c84519
chore: Magicnumber 수정
MaraMincho Nov 22, 2023
70d7adf
feat: Workouttpyes TNProvider 에서 Persistency 이용하도록 변경
MaraMincho Nov 22, 2023
8a9fcbb
chore: 주석 수정
MaraMincho Nov 22, 2023
79e5a91
chore: 주석 수정
MaraMincho Nov 22, 2023
fd784f0
feat: Usecase에서 Subscription or cancellables 삭제
MaraMincho Nov 22, 2023
34d271e
Chore: 이름 수정
MaraMincho Nov 22, 2023
f99c91e
chore: 오타 수정
MaraMincho Nov 22, 2023
db3c123
Merge remote-tracking branch 'origin/develop' into feature/iOS/GWL-70
MaraMincho Nov 22, 2023
9e67b63
chore: 피드백 반영
MaraMincho Nov 22, 2023
d90484b
chore: 접근제어자 수정
MaraMincho Nov 22, 2023
c758411
move: repositoryInterface 파일 위치변경
MaraMincho Nov 23, 2023
ac92b6d
feat: 피드백 반영 VC에 publisher타입을 <Result, Never> -> <State, Never>로 수정
MaraMincho Nov 23, 2023
d9e33f8
add: vcstate에서 error case안에 nested되게 수정
MaraMincho Nov 23, 2023
6d4c5be
delete: inject 삭제
MaraMincho Nov 23, 2023
1ee9c01
Merge remote-tracking branch 'origin/develop' into feature/iOS/GWL-70
MaraMincho Nov 23, 2023
db6ac0c
delete: devBranch충돌 회피
MaraMincho Nov 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Coordinator
import RecordFeature
import UIKit

// MARK: - TabBarCoordinator
Expand Down Expand Up @@ -49,11 +50,13 @@ final class TabBarCoordinator: TabBarCoordinating {
childCoordinators.append(homeCoordinator)
homeCoordinator.finishDelegate = self
homeCoordinator.start()

case .record:
let recordCoordinator = BCoordinator(navigationController: pageNavigationViewController)
let recordCoordinator = RecordFeatureCoordinator(navigationController: pageNavigationViewController)
childCoordinators.append(recordCoordinator)
recordCoordinator.finishDelegate = self
recordCoordinator.start()

case .profile:
let profileCoordinator = CCoordinator(navigationController: pageNavigationViewController)
childCoordinators.append(profileCoordinator)
Expand Down
18 changes: 18 additions & 0 deletions iOS/Projects/Features/Record/Resources/Persistency/PeerTypes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"code" : 0,
"errorMessage": null,
"data" : [
{
"description" : "혼자 진행하기",
"icon" : "person.fill",
"title" : "혼자 하기",
"typeCode" : 1
},
{
"description" : "랜덤한 사용자와 같이 진행하세요",
"icon" : "person.3.fill",
"title" : "같이 하기",
"typeCode" : 2
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"code" : 0,
"errorMessage": null,
"data" : [
{
"icon": "figure.run",
"description": "이거 가능?",
"typeCode" : 1
},
{
"icon": "figure.pool.swim",
"description": "수영",
"typeCode" : 2
},
{
"icon": "figure.outdoor.cycle",
"description": "이거 가능?",
"typeCode" : 3
}
]
}
15 changes: 15 additions & 0 deletions iOS/Projects/Features/Record/Sources/Data/DTO/GWResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// GWResponse.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

struct GWResponse<T>: Decodable where T: Decodable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3

이것도 DTO이지 않을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 이름이 애매해서 다음과 같이 사용했습니다. 😂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니면 DTO를 제외하고 Response, Request로 구분지어서 작명하는 것도 좋을 것 같아요.

let code: Int?
let errorMessage: String?
let data: T?
}
16 changes: 16 additions & 0 deletions iOS/Projects/Features/Record/Sources/Data/DTO/PeerTypeDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// PeerTypeDTO.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

struct PeerTypeDTO: Decodable {
let icon: String
let title: String
let description: String
let typeCode: Int
}
15 changes: 15 additions & 0 deletions iOS/Projects/Features/Record/Sources/Data/DTO/WorkoutTypeDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// WorkoutTypeDTO.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

struct WorkoutTypeDTO: Decodable {
let icon: String
let description: String
let typeCode: Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// DataLayerError.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

enum DataLayerError: LocalizedError {
case noData
case repositoryDidDeinit
case curreptedData
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// WorkoutEnvironmentSetupNetworkRepository.swift
// WorkoutEnvironmentSetupNetworkRepository
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation
import Trinet

// MARK: - WorkoutEnvironmentSetupNetworkRepository

struct WorkoutEnvironmentSetupNetworkRepository: WorkoutEnvironmentSetupNetworkRepositoryRepresentable {
private let decoder = JSONDecoder()
private let provider: TNProvider<WorkoutEnvironmentSetupEndPoint>

init(session: URLSessionProtocol) {
provider = .init(session: session)
}

func workoutTypes() -> AnyPublisher<[WorkoutTypeDTO], Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2: 지금은 Mock Data로 테스트해서 정상적으로 받아오지만, 서버에서는 GWResponse형태로 내려받기에 조심할 필요가 있습니다.
차후에 GWResponse<[WorkoutTypeDTO]>로 바꿀 계획이신건지 궁금합니다.

Copy link
Member Author

@MaraMincho MaraMincho Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 예전, MockData를 넣어서 Network에서 받아 올 때는 GWRresponse 포멧의 Json이었는데 현재는 내부 데이터라 [Type]에 대해서 기재했네요. 내부 Json 파일도 GWRresponse로 갖고 있어도 좋을 것 같다는 생각입니다. 따라서 Json도 수정 하겠습니다!

return Future<[WorkoutTypeDTO], Error> { promise in

guard
let bundle = Bundle(identifier: PersistencyProperty.bundleIdentifier),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3: App Target에서 실행하게 된다면 bundle이 App쪽을 따라가기 때문에 이렇게 하신건가요?
영리한 방법이네요.. 잘 써먹겠습니다 ㅎㅎ

let path = bundle.path(forResource: PersistencyProperty.workoutTypesFileName, ofType: PersistencyProperty.peerTypesFileNameOfType),
let data = try? Data(contentsOf: URL(filePath: path))
else {
return promise(.failure(DataLayerError.noData))
}

Comment on lines +28 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3: 지금 보니 session을 안쓰고 Repository에서 Mock Data를 가져오는군요.
Mock Data를 가져오는 코드는 Mock Session에서 할 일이기에 별도로 구현해보는 것도 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두가지 경우를 생각했습니다.

  1. 서버에서 peer를 가져온다 2. localJson에서 가져온다.

두가지를 커버하기 위해서, 한 파일에 넣었습니다. 현재는 API가 없어서, 일단 내부 파일로 구현했고 차후 API가 완성되면 통신 이후 Json을 내부 파일로 넣어서 처리할 예정이었습니다.

"MockSession에서 할 일" 이란 무엇일까에 대해서 고민해봤는데 질문의 의도를 모르겠습니다. 혹시 보충 설명 해주실 수 있을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레포지토리에서는 Session을 이용해서 데이터를 요청하고, 데이터가 어떻게 들어올 것인지는 Session에서 해야하는 역할이라고 생각했습니다.
지금 코드에서는 Session을 주입받되, 사용하지 않는 것 같아보였어요.
즉, 외부에서 MockSession을 주입하고, MockSession에는 Mock JSON의 데이터를 넘겨주는 방법으로 처리하는 게 어떨까 싶어 제시한 의견이었습니다. ㅎㅎ

do {
let workoutTypes = try decoder.decode(GWResponse<[WorkoutTypeDTO]>.self, from: data).data
guard let workoutTypes else {
promise(.failure(DataLayerError.curreptedData))
return
}

promise(.success(workoutTypes))
} catch {
promise(.failure(error))
}
}.eraseToAnyPublisher()
}

func peerType() -> AnyPublisher<[PeerTypeDTO], Error> {
return Future<[PeerTypeDTO], Error> { promise in

guard
let bundle = Bundle(identifier: PersistencyProperty.bundleIdentifier),
let path = bundle.path(forResource: PersistencyProperty.peerTypesFileName, ofType: PersistencyProperty.peerTypesFileNameOfType),
let data = try? Data(contentsOf: URL(filePath: path))
else {
return promise(.failure(DataLayerError.noData))
}

do {
let peerTypes = try decoder.decode(GWResponse<[PeerTypeDTO]>.self, from: data).data
guard let peerTypes else {
promise(.failure(DataLayerError.curreptedData))
return
}

promise(.success(peerTypes))
} catch {
promise(.failure(error))
}
}.eraseToAnyPublisher()
}
}

// MARK: WorkoutEnvironmentSetupNetworkRepository.WorkoutEnvironmentSetupEndPoint

private extension WorkoutEnvironmentSetupNetworkRepository {
enum WorkoutEnvironmentSetupEndPoint: TNEndPoint {
case exerciseTypes
case peer

// TODO: API에 맞게 수정 예정
var baseURL: String {
switch self {
case .exerciseTypes:
return "https://www.naver.com"
case .peer:
return "https://www.naver.com"
}
}

var path: String {
switch self {
case .exerciseTypes:
return ""
case .peer:
return ""
}
}

var method: TNMethod { return .get }
var query: Encodable? { nil }
var body: Encodable? { nil }
var headers: Trinet.TNHeaders { .init(headers: []) }
}

enum PersistencyProperty {
static let bundleIdentifier = "kr.codesquad.boostcamp8.RecordFeature"
static let peerTypesFileName = "PeerTypes"
static let workoutTypesFileName = "WorkoutTypes"
static let peerTypesFileNameOfType = "json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// PeerType.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

struct PeerType: Hashable {
let iconSystemImage: String
let titleText: String
let descriptionText: String
let typeCode: Int

init(icon: String, title: String, description: String, typeCode: Int) {
iconSystemImage = icon
titleText = title
descriptionText = description
self.typeCode = typeCode
}

init(peerTypeDTO dto: PeerTypeDTO) {
iconSystemImage = dto.icon
titleText = dto.title
descriptionText = dto.description
typeCode = dto.typeCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// WorkoutType.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

public struct WorkoutType: Hashable {
let workoutIcon: String
let workoutIconDescription: String
let typeCode: Int

init(workoutIcon: String, workoutIconDescription: String, typeCode: Int) {
self.workoutIcon = workoutIcon
self.workoutIconDescription = workoutIconDescription
self.typeCode = typeCode
}

init(workoutTypesDTO dto: WorkoutTypeDTO) {
workoutIcon = dto.icon
workoutIconDescription = dto.description
typeCode = dto.typeCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// DomainError.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

enum DomainError: LocalizedError {
case didDeinitUseCase
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// WorkoutEnvironmentSetupNetworkRepositoryRepresentable.swift
// RecordFeature
//
// Created by MaraMincho on 11/22/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

// MARK: - WorkoutEnvironmentSetupNetworkRepositoryRepresentable

protocol WorkoutEnvironmentSetupNetworkRepositoryRepresentable {
func workoutTypes() -> AnyPublisher<[WorkoutTypeDTO], Error>
func peerType() -> AnyPublisher<[PeerTypeDTO], Error>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// WorkoutEnvironmentSetupUseCase.swift
// RecordFeature
//
// Created by MaraMincho on 11/21/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

// MARK: - WorkoutEnvironmentSetupUseCaseRepresentable

protocol WorkoutEnvironmentSetupUseCaseRepresentable {
func workoutTypes() -> AnyPublisher<Result<[WorkoutType], Error>, Never>
func paerTypes() -> AnyPublisher<Result<[PeerType], Error>, Never>
}

// MARK: - WorkoutEnvironmentSetupUseCase

final class WorkoutEnvironmentSetupUseCase: WorkoutEnvironmentSetupUseCaseRepresentable {
let repository: WorkoutEnvironmentSetupNetworkRepositoryRepresentable

init(repository: WorkoutEnvironmentSetupNetworkRepositoryRepresentable) {
self.repository = repository
}

func paerTypes() -> AnyPublisher<Result<[PeerType], Error>, Never> {
return repository
.peerType()
.map { dto -> Result<[PeerType], Error> in
let peerTypes = dto.map { PeerType(peerTypeDTO: $0) }
return .success(peerTypes)
}
.catch { error in
Just(.failure(error))
}
.eraseToAnyPublisher()
}

func workoutTypes() -> AnyPublisher<Result<[WorkoutType], Error>, Never> {
return repository
.workoutTypes()
.map { dto -> Result<[WorkoutType], Error> in
let workoutTypes = dto.map { WorkoutType(workoutTypesDTO: $0) }
return .success(workoutTypes)
}
.catch { error in
Just(.failure(error))
}
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation

protocol WorkoutSettingCoordinating: Coordinating {
func pushWorkoutSelectViewController()
func pushWorkoutEnvironmentSetupViewController(workoutSetting: WorkoutSetting)
func pushWorkoutEnvironmentSetupViewController()
func pushOpponentSearchViewController(workoutSetting: WorkoutSetting)
func pushCountdownViewController(workoutSetting: WorkoutSetting)
func finish(workoutSetting: WorkoutSetting)
Expand Down
Loading