Skip to content

Commit a3f03b7

Browse files
committed
Feat: 마이페이지 화면 구현 (#T3-104)
- 마이페이지 화면 구현 - 의존성 등록 - TabBar에서 Myapge 클래스 삭제
1 parent 1021f0c commit a3f03b7

5 files changed

Lines changed: 318 additions & 8 deletions

File tree

Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,12 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol {
3939
DIContainer.shared.register(type: RecommendedRoutineViewModel.self) { _ in
4040
return RecommendedRoutineViewModel()
4141
}
42+
43+
DIContainer.shared.register(type: MypageViewModel.self) { container in
44+
guard let userDataRepository = container.resolve(type: UserDataRepositoryProtocol.self)
45+
else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") }
46+
47+
return MypageViewModel(userDataRepository: userDataRepository)
48+
}
4249
}
4350
}

Projects/Presentation/Sources/Common/TabBarView.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,19 @@ final public class TabBarView: UITabBarController {
3838
tabBar.tintColor = BitnagilColor.navy600
3939
tabBar.unselectedItemTintColor = BitnagilColor.navy100
4040

41+
4142
guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
4243
fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
4344
}
45+
46+
guard let mypageViewModel = DIContainer.shared.resolve(type: MypageViewModel.self) else {
47+
fatalError("mypageViewModel 의존성이 등록되지 않았습니다.")
48+
}
49+
4450
let homeView = HomeView()
4551
let recommendView = RecommendedRoutineView(viewModel: viewModel)
4652
let reportView = ReportView()
47-
let mypageView = MypageView()
53+
let mypageView = MypageView(viewModel: mypageViewModel)
4854

4955
homeView.tabBarItem = UITabBarItem(
5056
title: "",
@@ -89,10 +95,3 @@ final class ReportView: UIViewController {
8995
view.backgroundColor = .white
9096
}
9197
}
92-
93-
final class MypageView: UIViewController {
94-
override func viewDidLoad() {
95-
super.viewDidLoad()
96-
view.backgroundColor = .white
97-
}
98-
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// MyPageTableViewCell.swift
3+
// Presentation
4+
//
5+
// Created by 이동현 on 7/17/25.
6+
//
7+
8+
import SnapKit
9+
import UIKit
10+
11+
final class MypageTableViewCell: UITableViewCell {
12+
private enum Layout {
13+
static let titleLableLeadingSpacing: CGFloat = 20
14+
static let chevronImageViewTrailingSpacing: CGFloat = 6
15+
static let chevronImageViewSize: CGFloat = 36
16+
}
17+
18+
private let titleLabel = UILabel()
19+
private let chevronImageView = UIImageView()
20+
21+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
22+
super.init(style: style, reuseIdentifier: reuseIdentifier)
23+
configureAttribute()
24+
configureLayout()
25+
}
26+
27+
required init?(coder: NSCoder) {
28+
fatalError("init(coder:) has not been implemented")
29+
}
30+
31+
func configure(title: String) {
32+
titleLabel.text = title
33+
}
34+
35+
private func configureAttribute() {
36+
titleLabel.font = BitnagilFont(style: .body1, weight: .regular).font
37+
38+
chevronImageView.image = BitnagilIcon.chevronRightIcon
39+
}
40+
41+
private func configureLayout() {
42+
contentView.addSubview(titleLabel)
43+
contentView.addSubview(chevronImageView)
44+
45+
chevronImageView.snp.makeConstraints { make in
46+
make.centerY.equalToSuperview()
47+
make.trailing.equalToSuperview().inset(Layout.chevronImageViewTrailingSpacing)
48+
make.size.equalTo(Layout.chevronImageViewSize)
49+
}
50+
51+
titleLabel.snp.makeConstraints { make in
52+
make.verticalEdges.equalToSuperview()
53+
make.leading.equalToSuperview().offset(Layout.titleLableLeadingSpacing)
54+
make.trailing.equalTo(chevronImageView.snp.leading).offset(-8)
55+
}
56+
}
57+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//
2+
// MypageView.swift
3+
// Presentation
4+
//
5+
// Created by 이동현 on 7/17/25.
6+
//
7+
import Combine
8+
import Shared
9+
import SnapKit
10+
import UIKit
11+
12+
final class MypageView: BaseViewController<MypageViewModel> {
13+
private enum Layout {
14+
static let titleLabelHeight: CGFloat = 54
15+
static let settingButtonSize: CGFloat = 48
16+
static let settingButtonTrailingSpacing: CGFloat = 8
17+
static let profileImageViewSize: CGFloat = 80
18+
static let profileImageViewCornerRadius: CGFloat = profileImageViewSize / 2
19+
static let profileImageViewTopSpacing: CGFloat = 32
20+
static let nicknameLabelHeight: CGFloat = 24
21+
static let nicknameLabelTopSpacing: CGFloat = 12
22+
static let divideLineHeight: CGFloat = 6
23+
static let divideLineTopSpacing: CGFloat = 28
24+
static let tableViewCellHeight: CGFloat = 48
25+
}
26+
27+
private let titleLabel = UILabel()
28+
private let settingButton = UIButton()
29+
private let profileImageView = UIImageView()
30+
private let nicknameLabel = UILabel()
31+
private let dividerView = UIView()
32+
private let tableView = UITableView()
33+
private var cancellables: Set<AnyCancellable>
34+
35+
override init(viewModel: MypageViewModel) {
36+
cancellables = []
37+
super.init(viewModel: viewModel)
38+
}
39+
40+
required init?(coder: NSCoder) {
41+
fatalError("init(coder:) has not been implemented")
42+
}
43+
44+
override func viewDidLoad() {
45+
super.viewDidLoad()
46+
}
47+
48+
override func configureAttribute() {
49+
view.backgroundColor = .white
50+
51+
titleLabel.text = "마이페이지"
52+
titleLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font
53+
titleLabel.textAlignment = .center
54+
titleLabel.textColor = .black
55+
56+
settingButton.setImage(BitnagilIcon.settingIcon, for: .normal)
57+
58+
profileImageView.layer.cornerRadius = Layout.profileImageViewCornerRadius
59+
profileImageView.layer.masksToBounds = true
60+
profileImageView.backgroundColor = BitnagilColor.gray40 // 임시
61+
62+
nicknameLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font
63+
nicknameLabel.textColor = .black
64+
nicknameLabel.textAlignment = .center
65+
66+
dividerView.backgroundColor = BitnagilColor.gray99
67+
68+
tableView.register(MypageTableViewCell.self, forCellReuseIdentifier: MypageTableViewCell.className)
69+
tableView.dataSource = self
70+
tableView.delegate = self
71+
tableView.separatorStyle = .none
72+
}
73+
74+
override func configureLayout() {
75+
let safeArea = view.safeAreaLayoutGuide
76+
view.addSubview(titleLabel)
77+
view.addSubview(settingButton)
78+
view.addSubview(profileImageView)
79+
view.addSubview(nicknameLabel)
80+
view.addSubview(dividerView)
81+
view.addSubview(tableView)
82+
83+
titleLabel.snp.makeConstraints { make in
84+
make.horizontalEdges.top.equalTo(safeArea)
85+
make.height.equalTo(Layout.titleLabelHeight)
86+
}
87+
88+
settingButton.snp.makeConstraints { make in
89+
make.centerY.equalTo(titleLabel)
90+
make.trailing.equalToSuperview().inset(Layout.settingButtonTrailingSpacing)
91+
make.size.equalTo(Layout.settingButtonSize)
92+
}
93+
94+
profileImageView.snp.makeConstraints { make in
95+
make.top.equalTo(titleLabel.snp.bottom).offset(Layout.profileImageViewTopSpacing)
96+
make.centerX.equalToSuperview()
97+
make.size.equalTo(Layout.profileImageViewSize)
98+
}
99+
100+
nicknameLabel.snp.makeConstraints { make in
101+
make.top.equalTo(profileImageView.snp.bottom).offset(Layout.nicknameLabelTopSpacing)
102+
make.centerX.equalToSuperview()
103+
make.height.equalTo(Layout.nicknameLabelHeight)
104+
}
105+
106+
dividerView.snp.makeConstraints { make in
107+
make.top.equalTo(nicknameLabel.snp.bottom).offset(Layout.divideLineTopSpacing)
108+
make.horizontalEdges.equalToSuperview()
109+
make.height.equalTo(Layout.divideLineHeight)
110+
}
111+
112+
tableView.snp.makeConstraints { make in
113+
make.top.equalTo(dividerView.snp.bottom)
114+
make.horizontalEdges.bottom.equalTo(safeArea)
115+
}
116+
117+
}
118+
119+
override func bind() {
120+
viewModel.output.nickNamePublisher
121+
.sink { [weak self] nickname in
122+
self?.nicknameLabel.text = nickname
123+
}
124+
.store(in: &cancellables)
125+
126+
viewModel.output.externalURLPublisher
127+
.sink { url in
128+
UIApplication.shared.open(url)
129+
}
130+
.store(in: &cancellables)
131+
}
132+
}
133+
134+
extension MypageView: UITableViewDelegate {
135+
136+
}
137+
138+
extension MypageView: UITableViewDataSource {
139+
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
140+
return MypageViewModel.MypageMenu.allCases.count
141+
}
142+
143+
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
144+
return Layout.tableViewCellHeight
145+
}
146+
147+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
148+
guard
149+
let cell = tableView.dequeueReusableCell(withIdentifier: MypageTableViewCell.className) as? MypageTableViewCell
150+
else { return .init() }
151+
152+
let title = MypageViewModel.MypageMenu
153+
.allCases[indexPath.row]
154+
.rawValue
155+
cell.configure(title: title)
156+
157+
return cell
158+
}
159+
160+
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
161+
defer { tableView.deselectRow(at: indexPath, animated: true) }
162+
163+
let selectedMenu = MypageViewModel.MypageMenu.allCases[indexPath.row]
164+
165+
guard selectedMenu == .resetGoal else {
166+
viewModel.action(input: .didSelectMenu(menu: selectedMenu))
167+
return
168+
}
169+
170+
guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else {
171+
fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.")
172+
}
173+
174+
let onboardingView = OnboardingView(viewModel: onboardingViewModel, onboarding: .time)
175+
navigationController?.pushViewController(onboardingView, animated: true)
176+
}
177+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// MyPageViewModel.swift
3+
// Presentation
4+
//
5+
// Created by 이동현 on 7/17/25.
6+
//
7+
8+
import Combine
9+
import Domain
10+
import Foundation
11+
import Shared
12+
13+
final class MypageViewModel: ViewModel {
14+
enum Input {
15+
case didSelectMenu(menu: MypageMenu)
16+
}
17+
18+
struct Output {
19+
let nickNamePublisher: AnyPublisher<String, Never>
20+
let externalURLPublisher: AnyPublisher<URL, Never>
21+
}
22+
23+
enum MypageMenu: String, CaseIterable {
24+
case resetGoal = "내 목표 재설정"
25+
case notice = "공지사항"
26+
case faq = "자주 묻는 질문"
27+
}
28+
29+
private(set) var output: Output
30+
private let nicknamePublisher = CurrentValueSubject<String, Never>("")
31+
private let externalURLPublisher = PassthroughSubject<URL, Never>()
32+
private let userDataRepository: UserDataRepositoryProtocol
33+
34+
init(userDataRepository: UserDataRepositoryProtocol) {
35+
self.userDataRepository = userDataRepository
36+
37+
output = .init(
38+
nickNamePublisher: nicknamePublisher.eraseToAnyPublisher(),
39+
externalURLPublisher: externalURLPublisher.eraseToAnyPublisher())
40+
41+
do {
42+
let nickname = try userDataRepository.loadNickname()
43+
nicknamePublisher.send(nickname)
44+
} catch {
45+
BitnagilLogger.log(logType: .debug, message: "\(error.localizedDescription)")
46+
}
47+
}
48+
49+
func action(input: Input) {
50+
switch input {
51+
case .didSelectMenu(let menu):
52+
handleMenuSelection(menu: menu)
53+
}
54+
}
55+
56+
private func handleMenuSelection(menu: MypageMenu) {
57+
switch menu {
58+
case .resetGoal:
59+
break
60+
case .notice: // 임시 url
61+
if let url = URL(string: "https://www.google.com") {
62+
externalURLPublisher.send(url)
63+
}
64+
case .faq: // 임시 url
65+
if let url = URL(string: "https://www.naver.com") {
66+
externalURLPublisher.send(url)
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)