Skip to content

Commit b7776d1

Browse files
committed
Feat: 이용 약관 동의 UI 구현 (#T3-86)
- TermAgreementItemView 컴포넌트 구현 - TermAgreementView UI 구현 - LoginViewModel과 로직 구현
1 parent 187c9b0 commit b7776d1

6 files changed

Lines changed: 468 additions & 14 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// TermsAgreementState.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/8/25.
6+
//
7+
8+
struct TermsAgreementState {
9+
private var agreements: [TermsType: Bool] = [
10+
.service: false,
11+
.privacy: false,
12+
.age: false
13+
]
14+
15+
var isAllAgreed: Bool {
16+
return agreements.filter({ $0.value == false }).isEmpty
17+
}
18+
19+
func isAgreed(termType: TermsType) -> Bool {
20+
guard let agreement = agreements[termType] else { return false }
21+
return agreement
22+
}
23+
24+
mutating func toggleState(termType: TermsType) {
25+
agreements[termType]?.toggle()
26+
}
27+
28+
mutating func togleAllStates() {
29+
let state = !isAllAgreed
30+
TermsType.allCases.forEach { type in
31+
agreements[type] = state
32+
}
33+
}
34+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// TermsType.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/8/25.
6+
//
7+
8+
import Foundation
9+
10+
public enum TermsType: CaseIterable {
11+
case service
12+
case privacy
13+
case age
14+
15+
var title: String {
16+
switch self {
17+
case .service: "(필수) 서비스 이용약관 동의"
18+
case .privacy: "(필수) 개인정보 수집·이용 동의"
19+
case .age: "(필수) 만 14세 이상입니다."
20+
}
21+
}
22+
23+
var link: URL? {
24+
switch self {
25+
case .service: URL(string: "https://yapp-workspace.notion.site/2282106a0e84804cb283e44f24ecc567")
26+
case .privacy: URL(string: "https://yapp-workspace.notion.site/22a2106a0e848090864dc02fba31de34")
27+
case .age: nil
28+
}
29+
}
30+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//
2+
// TermsAgreementItemView.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/7/25.
6+
//
7+
8+
import UIKit
9+
import SnapKit
10+
11+
protocol TermsAgreementItemViewDelegate: AnyObject {
12+
func termsAgreementItemView(_ sender: TermsAgreementItemView, didToggleCheckFor termType: TermsType)
13+
func termsAgreementItemView(_ sender: TermsAgreementItemView, didTapMoreButtonFor termType: TermsType)
14+
}
15+
16+
final class TermsAgreementItemView: UIView {
17+
18+
private enum Layout {
19+
static let checkButtonLeadingSpacing: CGFloat = 20
20+
static let checkButtonSize: CGFloat = 16
21+
static let labelLeadingSpacing: CGFloat = 20
22+
static let moreButtonTrailingSpacing: CGFloat = 24
23+
}
24+
25+
private let checkButton = UIButton()
26+
private let agreementLable = UILabel()
27+
private let moreButton = UIButton()
28+
29+
private var termType: TermsType
30+
private var isAgreed: Bool = false {
31+
didSet {
32+
updateAttribute()
33+
}
34+
}
35+
weak var delegate: TermsAgreementItemViewDelegate?
36+
37+
init(termType: TermsType) {
38+
self.termType = termType
39+
super.init(frame: .zero)
40+
configureAttribute()
41+
configureLayout()
42+
}
43+
44+
required init?(coder: NSCoder) {
45+
fatalError("init(coder:) has not been implemented")
46+
}
47+
48+
private func configureAttribute() {
49+
checkButton.do {
50+
$0.setImage(BitnagilIcon.checkIcon, for: .normal)
51+
$0.tintColor = BitnagilColor.navy100
52+
$0.addAction(UIAction { [weak self] _ in
53+
guard let self else { return }
54+
self.isAgreed.toggle()
55+
self.delegate?.termsAgreementItemView(self, didToggleCheckFor: self.termType)
56+
}, for: .touchUpInside)
57+
}
58+
59+
agreementLable.do {
60+
$0.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: termType.title)
61+
$0.textColor = BitnagilColor.gray50
62+
}
63+
64+
moreButton.do {
65+
let title = BitnagilFont(style: .captionUnderline1, weight: .semiBold).attributedString(text: "더보기")
66+
$0.setAttributedTitle(title, for: .normal)
67+
$0.setTitleColor(BitnagilColor.gray50, for: .normal)
68+
$0.isHidden = termType.link == nil
69+
$0.addAction(UIAction { [weak self] _ in
70+
guard let self else { return }
71+
self.delegate?.termsAgreementItemView(self, didTapMoreButtonFor: self.termType)
72+
}, for: .touchUpInside)
73+
}
74+
}
75+
76+
private func configureLayout() {
77+
addSubview(checkButton)
78+
addSubview(agreementLable)
79+
addSubview(moreButton)
80+
81+
checkButton.snp.makeConstraints { make in
82+
make.leading.equalToSuperview().offset(Layout.checkButtonLeadingSpacing)
83+
make.centerY.equalToSuperview()
84+
make.size.equalTo(Layout.checkButtonSize)
85+
}
86+
87+
agreementLable.snp.makeConstraints { make in
88+
make.leading.equalTo(checkButton.snp.trailing).offset(Layout.labelLeadingSpacing)
89+
make.centerY.equalToSuperview()
90+
}
91+
92+
moreButton.snp.makeConstraints { make in
93+
make.trailing.equalToSuperview().inset(Layout.moreButtonTrailingSpacing)
94+
make.centerY.equalToSuperview()
95+
}
96+
}
97+
98+
private func updateAttribute() {
99+
checkButton.tintColor = isAgreed ? BitnagilColor.navy500 : BitnagilColor.navy100
100+
}
101+
102+
func updateState(isAgreed: Bool) {
103+
self.isAgreed = isAgreed
104+
}
105+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// TotalAgreementButton.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/7/25.
6+
//
7+
8+
import UIKit
9+
import SnapKit
10+
11+
final class TotalAgreementButton: UIButton {
12+
13+
private enum Layout {
14+
static let cornerRadius: CGFloat = 12
15+
static let checkIconSize: CFloat = 24
16+
static let stackViewSpacing: CGFloat = 16
17+
}
18+
19+
private let stackView = UIStackView()
20+
private let checkButton = UIImageView()
21+
private let buttonLabel = UILabel()
22+
23+
private var enableState: Bool {
24+
didSet {
25+
updateButtonAttribute()
26+
}
27+
}
28+
29+
init(enableState: Bool = false) {
30+
self.enableState = enableState
31+
super.init(frame: .zero)
32+
configureAttribute()
33+
configureLayout()
34+
updateButtonAttribute()
35+
}
36+
37+
required init?(coder: NSCoder) {
38+
fatalError("init(coder:) has not been implemented")
39+
}
40+
41+
private func configureAttribute() {
42+
backgroundColor = BitnagilColor.gray99
43+
layer.cornerRadius = Layout.cornerRadius
44+
45+
stackView.do {
46+
$0.axis = .horizontal
47+
$0.alignment = .center
48+
$0.spacing = Layout.stackViewSpacing
49+
$0.isUserInteractionEnabled = false
50+
}
51+
52+
checkButton.do {
53+
$0.image = BitnagilIcon.checkIcon
54+
$0.tintColor = BitnagilColor.navy100
55+
$0.contentMode = .scaleAspectFit
56+
}
57+
58+
buttonLabel.do {
59+
$0.text = "전체동의"
60+
$0.textColor = BitnagilColor.gray50
61+
$0.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font
62+
}
63+
}
64+
65+
private func configureLayout() {
66+
addSubview(stackView)
67+
stackView.addArrangedSubview(checkButton)
68+
stackView.addArrangedSubview(buttonLabel)
69+
70+
stackView.snp.makeConstraints { make in
71+
make.leading.equalToSuperview().inset(Layout.stackViewSpacing)
72+
make.centerY.equalToSuperview()
73+
}
74+
75+
checkButton.snp.makeConstraints { make in
76+
make.size.equalTo(Layout.checkIconSize)
77+
}
78+
}
79+
80+
private func updateButtonAttribute() {
81+
backgroundColor = enableState ? BitnagilColor.lightBlue75 : BitnagilColor.gray99
82+
checkButton.tintColor = enableState ? BitnagilColor.navy500 : BitnagilColor.navy100
83+
buttonLabel.textColor = enableState ? BitnagilColor.navy500 : BitnagilColor.gray50
84+
}
85+
86+
func updateButtonState(enableState: Bool) {
87+
self.enableState = enableState
88+
}
89+
}

0 commit comments

Comments
 (0)