Skip to content

Commit cb2fbf7

Browse files
Merge pull request #180 from YAPP-Github/feat/#177-dynamic-terms
[Feat] #177 - 이용약관 변경 대비, 약관 내용 리모트
2 parents 3765917 + 4a75ab4 commit cb2fbf7

10 files changed

Lines changed: 168 additions & 116 deletions

File tree

Neki-iOS/Core/Sources/Auth/Sources/Data/Sources/AuthEndpoint.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import os
1111
enum AuthEndpoint {
1212
case reissueToken
1313
case login(dto: SocialLoginDTO.Request, provider: ProviderType)
14+
case fetchTerms
1415
case agreeWithTerms(dto: AgreeTermsDTO.Request)
1516

1617
case withdraw
@@ -26,7 +27,7 @@ extension AuthEndpoint: Endpoint {
2627
var authorizationType: AuthorizationType {
2728
switch self {
2829
case .reissueToken: return .reissue
29-
case .login: return .none
30+
case .login, .fetchTerms: return .none
3031
case .agreeWithTerms, .withdraw, .editNickname, .editProfileImage, .fetchUserInfo: return .bearer
3132
}
3233
}
@@ -45,6 +46,7 @@ extension AuthEndpoint: Endpoint {
4546
switch self {
4647
case .reissueToken: return "auth/refresh"
4748
case let .login(_, provider): return "auth/\(provider.name)/login"
49+
case .fetchTerms: return "terms"
4850
case .agreeWithTerms: return "terms/agreements"
4951
case .withdraw: return "users/me"
5052
case .editNickname: return "users/me"
@@ -58,13 +60,13 @@ extension AuthEndpoint: Endpoint {
5860
case .reissueToken, .login, .agreeWithTerms: return .post
5961
case .withdraw: return .delete
6062
case .editNickname, .editProfileImage: return .patch
61-
case .fetchUserInfo: return .get
63+
case .fetchUserInfo, .fetchTerms: return .get
6264
}
6365
}
6466

6567
var body: (any Encodable)? {
6668
switch self {
67-
case .reissueToken, .fetchUserInfo, .withdraw: return nil
69+
case .reissueToken, .fetchUserInfo, .fetchTerms, .withdraw: return nil
6870
case let .login(dto, _): return dto
6971
case let .agreeWithTerms(dto): return dto
7072
case let .editNickname(dto): return dto
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// FetchTermsDTO.swift
3+
// Neki-iOS
4+
//
5+
// Created by SwainYun on 3/9/26.
6+
//
7+
8+
import Foundation
9+
10+
enum FetchTermsDTO {
11+
struct Response: Decodable {
12+
let terms: [TermDTO]
13+
}
14+
}
15+
16+
struct TermDTO: Decodable {
17+
let id: Int
18+
let termType: String?
19+
let title: String
20+
let termInformationURL: URL?
21+
let isRequired: Bool
22+
23+
enum CodingKeys: String, CodingKey {
24+
case id, termType, title, isRequired
25+
case termInformationURL = "url"
26+
}
27+
28+
func toEntity() -> Term { Term(id: id, title: title, isRequired: isRequired, termInformationURL: termInformationURL) }
29+
}

Neki-iOS/Core/Sources/Auth/Sources/Data/Sources/Implementations/DefaultAuthRepository.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,22 @@ public struct DefaultAuthRepository: AuthRepository {
102102
}
103103
}
104104

105-
public func agreeWithTerms(agreements: [TermAgreement]) async throws(AuthRepositoryError) {
106-
let agreements = agreements.map { AgreementsDTO(termID: $0.id, agreed: $0.agreed) }
105+
public func fetchTerms() async throws(AuthRepositoryError) -> [Term] {
106+
let endpoint = AuthEndpoint.fetchTerms
107+
108+
do {
109+
let responseDTO: BaseResponseDTO<FetchTermsDTO.Response> = try await networkProvider.request(endpoint: endpoint)
110+
guard let data = responseDTO.data else { throw AuthRepositoryError.networkError(.responseDecodingError) }
111+
return data.terms.map { $0.toEntity() }
112+
} catch let error as NetworkError {
113+
throw .networkError(error)
114+
} catch {
115+
throw .unknown
116+
}
117+
}
118+
119+
public func agreeWithTerms(agreements: [UserAgreement]) async throws(AuthRepositoryError) {
120+
let agreements = agreements.map { AgreementsDTO(termID: $0.id, agreed: $0.isAgreed) }
107121
let dto = AgreeTermsDTO.Request(agreements: agreements)
108122
let endpoint = AuthEndpoint.agreeWithTerms(dto: dto)
109123

Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Clients/AuthClient.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public struct AuthClient {
2727
public var loginWithApple: @Sendable (_ idToken: Data) async throws -> User
2828
public var loginWithKakao: @Sendable () async throws -> User
2929
public var autoLogin: @Sendable () async throws -> User
30-
public var agreeWithTerms: @Sendable (_ agreements: [TermAgreement]) async throws -> Void
30+
public var fetchTerms: @Sendable () async throws -> [Term]
31+
public var agreeWithTerms: @Sendable (_ agreements: [UserAgreement]) async throws -> Void
3132
public var signOut: @Sendable () async throws -> Void
3233
public var withdraw: @Sendable () async throws -> Void
3334
public var updateProfile: @Sendable (_ nickname: String?, _ updateAction: ProfileImageUpdateAction) async throws -> User
@@ -81,7 +82,15 @@ extension AuthClient: DependencyKey {
8182
}
8283
}
8384

84-
@Sendable func agreeWithTerms(agreements: [TermAgreement]) async throws -> Void {
85+
@Sendable func fetchTerms() async throws -> [Term] {
86+
do {
87+
return try await authRepository.fetchTerms()
88+
} catch {
89+
throw AuthClient.mapError(error)
90+
}
91+
}
92+
93+
@Sendable func agreeWithTerms(agreements: [UserAgreement]) async throws -> Void {
8594
do {
8695
try await authRepository.agreeWithTerms(agreements: agreements)
8796
} catch {
@@ -159,6 +168,7 @@ extension AuthClient: DependencyKey {
159168
loginWithApple: loginWithApple,
160169
loginWithKakao: loginWithKakao,
161170
autoLogin: autoLogin,
171+
fetchTerms: fetchTerms,
162172
agreeWithTerms: agreeWithTerms,
163173
signOut: signOut,
164174
withdraw: withdraw,

Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/TermAgreement.swift

Lines changed: 0 additions & 10 deletions
This file was deleted.

Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/TermsType.swift

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// UserAgreement.swift
3+
// Neki-iOS
4+
//
5+
// Created by SwainYun on 1/25/26.
6+
//
7+
8+
import Foundation
9+
10+
public struct Term: Identifiable, Equatable, Sendable {
11+
public let id: Int
12+
public let title: String
13+
public let isRequired: Bool
14+
public let termInformationURL: URL?
15+
16+
public init(id: Int, title: String, isRequired: Bool = true, termInformationURL: URL? = nil) {
17+
self.id = id
18+
self.title = title
19+
self.isRequired = isRequired
20+
self.termInformationURL = termInformationURL
21+
}
22+
}
23+
24+
public struct UserAgreement: Identifiable, Equatable, Sendable {
25+
public var id: Int { term.id }
26+
public let term: Term
27+
public var isAgreed: Bool
28+
29+
public init(term: Term, isAgreed: Bool = false) {
30+
self.term = term
31+
self.isAgreed = isAgreed
32+
}
33+
}

Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Interfaces/AuthRepository.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public protocol AuthRepository {
3838
func updateProfile(nickname: String?, editAction: ProfileImageEditAction) async throws(AuthRepositoryError) -> Void
3939
/// 자동 로그인 (유저 세션 복구)
4040
func restoreSession() async throws(AuthRepositoryError) -> User
41+
/// 이용약관 목록 조회
42+
func fetchTerms() async throws(AuthRepositoryError) -> [Term]
4143
/// 이용약관 동의
42-
func agreeWithTerms(agreements: [TermAgreement]) async throws(AuthRepositoryError) -> Void
44+
func agreeWithTerms(agreements: [UserAgreement]) async throws(AuthRepositoryError) -> Void
4345
}

Neki-iOS/Core/Sources/Auth/Sources/Presentation/Sources/Feature/TermsAgreementFeature.swift

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,22 @@ import os
1313
public struct TermsAgreementFeature {
1414
@ObservableState
1515
public struct State {
16-
var agreements: IdentifiedArrayOf<UserAgreement>
16+
var agreements: IdentifiedArrayOf<UserAgreement> = []
1717
var isAllAgreed: Bool { agreements.allSatisfy(\.isAgreed) }
18-
var isConfirmButtonEnabled: Bool { agreements.filter(\.isRequired).allSatisfy(\.isAgreed) }
18+
var isConfirmButtonEnabled: Bool { agreements.filter(\.term.isRequired).allSatisfy(\.isAgreed) }
1919
var isLoading: Bool = false
20-
21-
public init() {
22-
self.agreements = [
23-
UserAgreement(type: .serviceUsage),
24-
UserAgreement(type: .privacyPolicy),
25-
UserAgreement(type: .locationService)
26-
]
27-
}
2820
}
2921

3022
public enum Action: BindableAction {
3123
// View Actions
32-
case toggleAgreement(TermsType)
24+
case onAppear
25+
case toggleAgreement(UserAgreement)
3326
case toggleAllAgreements
3427
case confirmButtonTapped
35-
case termPageLinkTapped(TermsType)
28+
case termPageLinkTapped(UserAgreement)
3629

3730
// Internal Actions
31+
case fetchTermsResponse(Result<[Term], Error>)
3832
case agreeTermsResponse(Result<Void, Error>)
3933

4034
// Delegate Actions
@@ -52,24 +46,41 @@ public struct TermsAgreementFeature {
5246

5347
Reduce { (state: inout State, action: Action) -> Effect<Action> in
5448
switch action {
55-
case let .toggleAgreement(type):
56-
state.agreements[id: type.id]?.isAgreed.toggle()
49+
case .onAppear:
50+
state.isLoading = true
51+
return .run { send in
52+
await send(.fetchTermsResponse(Result { try await authClient.fetchTerms() }))
53+
}
54+
55+
case let .toggleAgreement(agreement):
56+
state.agreements[id: agreement.id]?.isAgreed.toggle()
5757
return .none
5858

5959
case .toggleAllAgreements:
6060
let shouldAgreeAll = state.isAllAgreed == false
61-
for type in TermsType.allCases { state.agreements[id: type.id]?.isAgreed = shouldAgreeAll }
61+
for id in state.agreements.ids { state.agreements[id: id]?.isAgreed = shouldAgreeAll }
6262
return .none
6363

6464
case .confirmButtonTapped:
6565
guard state.isConfirmButtonEnabled, state.isLoading == false else { return .none }
6666
state.isLoading = true
67-
let agreementsToSend: [TermAgreement] = state.agreements.map { (id: $0.id, agreed: $0.isAgreed) }
67+
let agreementsToSend = Array(state.agreements)
6868

6969
return .run { send in
7070
await send(.agreeTermsResponse(Result { try await authClient.agreeWithTerms(agreementsToSend) }))
7171
}
7272

73+
case let .fetchTermsResponse(.success(terms)):
74+
let userAgreements = terms.map { UserAgreement(term: $0) }
75+
state.agreements = IdentifiedArray(uniqueElements: userAgreements)
76+
state.isLoading = false
77+
return .none
78+
79+
case let .fetchTermsResponse(.failure(error)):
80+
state.isLoading = false
81+
Logger.presentation.error("Error occured while fetching terms: \(error)")
82+
return .none
83+
7384
case .agreeTermsResponse(.success):
7485
state.isLoading = false
7586
return .send(.didFinishOnboarding)
@@ -79,15 +90,9 @@ public struct TermsAgreementFeature {
7990
Logger.presentation.error("Error occured while agreeing to terms: \(error)")
8091
return .none
8192

82-
case let .termPageLinkTapped(type):
83-
let urlString: String
84-
switch type {
85-
case .serviceUsage: urlString = "https://lydian-tip-26b.notion.site/2ee0d9441db0807c8684ce3e2d4b8aca?source=copy_link"
86-
case .privacyPolicy: urlString = "https://lydian-tip-26b.notion.site/2ee0d9441db0807cb850f78145db6dd3?pvs=74"
87-
case .locationService: urlString = "https://lydian-tip-26b.notion.site/2ee0d9441db080b48223fb0b3263da08?pvs=74"
88-
}
93+
case let .termPageLinkTapped(agreement):
8994
return .run { _ in
90-
guard let url = URL(string: urlString) else { return }
95+
guard let url = agreement.term.termInformationURL else { return }
9196
await openURL(url)
9297
}
9398

0 commit comments

Comments
 (0)