From 3fcca1365649d29184de3f916209ff62304ae1f6 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sun, 20 Jul 2025 16:09:41 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20asset?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#T3-104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BitnagilIcon에 이미지 추가 --- .../chevron_right_icon.imageset/Contents.json | 23 ++++++++++++++++++ .../chevron_right_icon@1x.png | Bin 0 -> 181 bytes .../chevron_right_icon@2x.png | Bin 0 -> 261 bytes .../chevron_right_icon@3x.png | Bin 0 -> 306 bytes .../setting_icon.imageset/Contents.json | 23 ++++++++++++++++++ .../setting_icon.imageset/setting_icon@1x.png | Bin 0 -> 724 bytes .../setting_icon.imageset/setting_icon@2x.png | Bin 0 -> 1457 bytes .../setting_icon.imageset/setting_icon@3x.png | Bin 0 -> 2094 bytes .../Common/DesignSystem/BitnagilIcon.swift | 4 +++ 9 files changed, 50 insertions(+) create mode 100644 Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/Contents.json create mode 100644 Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@1x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@2x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@3x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/Contents.json create mode 100644 Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@1x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@2x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@3x.png diff --git a/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/Contents.json new file mode 100644 index 00000000..30cca47b --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "chevron_right_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chevron_right_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chevron_right_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..5e32064b2dc654a8beb89427c0a14fc76400320e GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@Nd@oNI$B+ufw^tAHG6)E`T Yr|%Fr+_m7;3ZSVBp00i_>zopr0Bd$Sr2qf` literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/chevron_right_icon.imageset/chevron_right_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4a4d7438d92b24aa5049566613fd7319021040 GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9E$svykh8Km+7D9BhG zV0%>Ixeez|w-=F(EJiYC$*hIBZ7pYYpCLw~E zE0it;xGq`P<>4$ku}y3K#J-sat^O=M_5ACc!``lQ)^%CLiWctDdQ=j6oo1K-6l5$8 za(7}_cTVOdki(Mh=F{Fa=?JY+xCPM+2i-)>znSbYa%iEoqRR2hLvk-?9xpKG`Mj+vJ(R^D?g``f2lBw8kk3a;*ma6O`>6ehT_t0UB9 zjS@sCRZnpBso(d1aeB|TiMuJb5!op~c7v6<%=8tPP4E0N0?Bx~`njxgN@xNApmAn9 literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/Contents.json new file mode 100644 index 00000000..ffcd3ef6 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "setting_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "setting_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "setting_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..aece89bfc252c33471a4572681fa1b12c4ed96a6 GIT binary patch literal 724 zcmV;_0xSKAP)y$|qi!lCQA!Xee14XrAdnV| zN>5SlvOJ&55qzArN4Zn0$thKfBb)_6ONxHIUaoA zBuL{Y)?#Y-1gjsy9(5;%_Sj5Y-iKHmP$&rJe*!U!Er&nPZVdRg26-Z@8<&9Vvu>eg z*_O`&Z{UPu70vgpNQ_=_9@#1E?-QWA&{Mt(`vp6OJ%IWNpVaH&HJVQ0uTZXo--q8v zIRHNrA7n?74`e+i@~wOyaGmV_mgwH(mvi@j2}8e`*PHU>2UkinS1es6Dw5z03V zYWRC_hpfZ@Sr~?QuyAWD`~xBXRZtgA$2>yt|3IB9e+K?ie1Y+L8jQ3H!*>k-%2;RG z^$p)En$Ib@>ce=;zc!Hd`X8V!flpD#qwJ!3r1e>Vb7#JPXoAJq6J2FoqR;2$GEbDw ze`N$+^M~oe?h=oUv6atb%yUd5+Gp%KiT4MZXpWNQEwq@*@>%fuKr8(T3)h0MwiUoU zC(EzIwh^2v-}{tL*qpE!jt?CL4(x=eYp&xfRH#tl9lQXwwg(a5hWuFo0000t&)La|%=0|YD$*plT_awqLHTGTuMvMG12KqzP|*o0`3jGT>pQ-!OhHkY zA3n%S2;+H0mKnayhY(b+0zy4gRQ@F%(+1I5=`|maX3FFZ(rl3Cs;UQupf>?Metgaz zFp~j*KxgmX<#XwQPoxJt!l4fNfvt|~Puc6Dx}L(G^$gb%*Qur{f$;S7bbPKAvb5+O%nd*s+ zCru*zo>BCU4Ak& ztSYX<{Ydl^hb5K!FE!S3Nr@x=1h*>*NU&L~%00mQXRg^u-H#h-W(PUl3RTi?%{=hF zfG%2wvaW*|iZl#y`;i8l8BQPT{yzW75uY;;r{l5I!^%${JsQjV_uDt-2N;H17>J+a z@#`WCW#IEf4ad$CM5i94M&Zxmux@4F^557k*~JB6?|@73!YTuM(I4WT44h>+hE7Tv z1-q3aghS)bAskx3bH+z+8}Y*PS6tSxGv)B*&@3eeOe=0(S2SR2TK6EeWN)lsr+ZLv zc6LsEkr+YV7Sq z%vTYArt#NSSudm|U51$LQe4$Iir<SB1*bLfuY6@ahAsjsP0w|eg+P+h zk53LN?PFu*<@LE74b^1v&B>6Kbg30lhsIN|!`V$_ApYyFM(+$JAn1k6eCK+#Gz4*N z9g@jFbP-T#oU32u!yZzpH0RoU(dF7obOFw!^(tc*0hKCDo5)cxlQ&lU>zepG)E1gr z!D~Yzit zu#D*g9ph?DT&BNn94?EQjrs{I0~fk7P#U^wZ>_9+T^9F4S>V1rk(PvTRx1lepUSpL zkY>T*B`t8d3^irAw7_Lndyk7lXpHZ_S!sSnd^SufN1%)U z-`nBi@%WV^uigF|dd_z&qjVtTf>|Pm`&B;2a;p1bNln(d4y&~z4>m$+nN{7AS*ZQ7 z;QM0GODwRtSM|(QxdL@!>8Ek1Jk>Or>}N^Ne5fXPp#5?I57s1!0IlEcC$ZH>| z!d*!_67vQB-AG{iu%yqd_aI&U-=dF=!`7a23o`R1ZX6y`vcX_GWfGV^_U5TvA9N6B zDk#(3x4)%++(gRzVs}3KMHO_(oenudS<@UGj8emp zzE*0-SCb->fb??0%y9(ffx@kjy&=hpCNG&r3)QGS7K3fkbH)VhqCxw>wZKckw_#Gi9{liNF)-8M7;b1`QnKNul_Cz00000 LNkvXXu0mjfNgB4v literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d22d9cf0d2450de1ee227cacc6e07f6f776549ea GIT binary patch literal 2094 zcmZ{l`9BkmAIB%lG36?`Blq3Ml_N(YY>teLEe*rw{0g)1iQH$dFlTO)SaZw~eMpWi z7L)tPmB_8+s_6UG_xI1^@p`?U@AvB;cqQ3bn_UD6fdBx&MX0?9*NZ>5f*r~>spb<5UEF1! z%bO`&(?C&FalhB;%$>kY{Wa*%c8+%5|H-fEIY$6nwD`X%%!*`Wws!59Fe0;Cz{dt# zEK|Cy9+x)!cN}^SD`wcO>W5A48Ew|orh&v_n6Dr&W#eZat~A@D5p6dSZFNSdHm?4L z#-sX1T&hx5xdAghb#fA2nZ;K)A)nG-%O$obz~>Jv)+254VTwvgqb>4eIOx&ItKdY! z&DgukFnE=cWZbwrlrGx#u+3^16o)o8|CVIH+`4m}g{smD?u>d@->4FRqm_k3@g23C zD3apSm2~G9W*{o7-$RM{UMP~WiDwELL(D0MwMm}%F57hmhogL#51DotZ^beXcrkjY zOwIDsod3@0BYb&HB)vIir_7zum;)<}XH9B}Sbbmw?fE089onFv{C5p z=p!2(e(TV7swc>8cFh)_;(;};224Nr3ib*ePdYas*tlKQ-r0d*Ic(+T!c4{x0-$gt z@OLna=B>#{{VtphSEfiv*n za*i!23-nGjgL!bqX;DKjh9TN12-#)n(#4<`E&L)Te;4lsb`Kisr+4?H?#SYmyMF#BXW6h0P zX=>g?Rc8tPmxYD>()Ls7$LdhRTS(r=`JwBUfi}oGB$S=xuXDV%vU)ffhRzv+ks-|-rxH(4!J4+scfD;d3tfFWyei^JO zCi;?4x%twm%CIZ@LzoTrG+#B5iaEhaqwUxY-XlD#_SlEWuzeX(KK zjh15LTxmd4vbUF4RGI-$JhA&L72lw;@jISONWq(F6?wad%Gbscs)W-abxF0PGHWl9 zQ*x1wveNjLSd3V{w=Am;u9_{l%x)09kfS=W#l@@6LHui_?`I2J7zF>@)VXAlf4Csb zreu_U*E7UlmssCEjK7Ny;j1b*m2b*xQM8=2b+AW_Dp#mTU7b*!A^)|K=a3*9mD{6J zLQ!r)q1&(#EVUKt`a<8@9i+3ny}bn6F<9-a*@SWE40sLM|CTwXOtg|1Z;K#L8v+c2 zMvn;h4Yw}AW*h}1+5lWr_$rA601%p2h7obg`z-KL^-dV0#ywLB^S=5W3Wb$Q*g3y@ z034g-uk^V%-rb~S^}!Vq?ay9E5nhMkrf#0na^@@5tM~$cHp#?EE*=Tel*UUec)@kZNqZUrMmn##BV9nt0nx6m6IUXayB1G>eflWAnW#7b z>}ZLimLR1Wxiq_!>96W#!MwA1a8rhvl@v*F2am~NyHh&>rt!Zhg~RlkY6^oJITU#D6r`D zHJXCxhrnm}(Z^1viw9$Sw$T?186lzF;cCDE`nSRhp^X^;H0d_TWr z9$4fyB|G8@{6Es|EU1i25<|Rhojx@=_3ZQkm#5hVHt!VWD5ILwLrbS!!R$QTNuG5J zl*$U;;AyGCLU8iRZ|IM8^L1h2e#WaG97-ooYBXkIv$n(BiKS6`(yGpH_z%L9)(}K; zTwrlCRn6fLpm?*bqiEr}erazIqPI)U-(B8f&pG1l~oy4B34U6vtBx@p)F|ic)F1jh5DowMraWa`X%%bT$fcmF0O7mMnb~ zs%KS6*=pUp`@|5TI0N&cmQ9FU3~55{i6@-Hk?Q6Zb#U5cfFfIzhgsgMt>P!vFvP literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift index 350f0b41..c4010e88 100644 --- a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift +++ b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift @@ -33,6 +33,10 @@ enum BitnagilIcon { static let mypageFillIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) static let mypageEmptyIcon = UIImage(named: "mypage_empty_icon", in: bundle, with: nil) + + static let settingIcon = UIImage(named: "setting_icon", in: bundle, with: nil) + + static let chevronRightIcon = UIImage(named: "chevron_right_icon", in: bundle, with: nil) } enum Direction { From 154b422a4aea81bb63d243fa01ddd9cf4b71eff4 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sun, 20 Jul 2025 16:10:46 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat:=20NSObject=20extension=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20(#T3-104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - {class이름}.className을 통해 클래스의 이름을 스트링으로 사용할 수 있도록 구현 --- .../Sources/Common/Extension/NSObject+.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Projects/Presentation/Sources/Common/Extension/NSObject+.swift diff --git a/Projects/Presentation/Sources/Common/Extension/NSObject+.swift b/Projects/Presentation/Sources/Common/Extension/NSObject+.swift new file mode 100644 index 00000000..0ee07cd4 --- /dev/null +++ b/Projects/Presentation/Sources/Common/Extension/NSObject+.swift @@ -0,0 +1,18 @@ +// +// NSObject+.swift +// Presentation +// +// Created by 이동현 on 7/20/25. +// + +import Foundation + +public extension NSObject { + var className: String { + return String(describing: type(of: self)) + } + + class var className: String { + return String(describing: self) + } +} From 1021f0c7e7dc098df42aea76627d00236862796f Mon Sep 17 00:00:00 2001 From: taipaise Date: Sun, 20 Jul 2025 23:13:30 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Feat:=20UserDataRepository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=20(#T3-104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유저 정보 (닉네임, accessToken)을 가져올 수 있는 repository 구현 - UserDataRepository 의존성 등록 --- .../DataSourceDependencyAssembler.swift | 4 ++ .../Sources/Common/Error/UserError.swift | 24 ++++++++++++ .../Repository/UserDataRepository.swift | 37 +++++++++++++++++++ .../UserDataRepositoryProtocol.swift | 17 +++++++++ 4 files changed, 82 insertions(+) create mode 100644 Projects/DataSource/Sources/Common/Error/UserError.swift create mode 100644 Projects/DataSource/Sources/Repository/UserDataRepository.swift create mode 100644 Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift diff --git a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift index 8b5b8b6d..95d2c27f 100644 --- a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift +++ b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift @@ -37,5 +37,9 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol { DIContainer.shared.register(type: OnboardingRepositoryProtocol.self) { _ in return OnboardingRepository(networkService: networkService, keychainStorage: keychainStorage) } + + DIContainer.shared.register(type: UserDataRepositoryProtocol.self) { _ in + return UserDataRepository(keychainStorage: keychainStorage, userDefaultsStorage: userDefaultsStorage) + } } } diff --git a/Projects/DataSource/Sources/Common/Error/UserError.swift b/Projects/DataSource/Sources/Common/Error/UserError.swift new file mode 100644 index 00000000..0f6f7ff7 --- /dev/null +++ b/Projects/DataSource/Sources/Common/Error/UserError.swift @@ -0,0 +1,24 @@ +// +// UserError.swift +// DataSource +// +// Created by 이동현 on 7/20/25. +// + +enum UserError: Error, CustomStringConvertible { + case accessTokenLoadFailed + case nicknameLoadFailed + case unknown(error: Error) + + + public var description: String { + switch self { + case .accessTokenLoadFailed: + return "토큰 불러오기에 실패했습니다." + case .nicknameLoadFailed: + return "닉네임 불러오기에 실패했습니다." + case .unknown(let error): + return "알 수 없는 에러가 발생했습니다. \(error.localizedDescription)" + } + } +} diff --git a/Projects/DataSource/Sources/Repository/UserDataRepository.swift b/Projects/DataSource/Sources/Repository/UserDataRepository.swift new file mode 100644 index 00000000..633d22c3 --- /dev/null +++ b/Projects/DataSource/Sources/Repository/UserDataRepository.swift @@ -0,0 +1,37 @@ +// +// UserDataRepository.swift +// DataSource +// +// Created by 이동현 on 7/20/25. +// + +import Foundation +import Domain + +final class UserDataRepository: UserDataRepositoryProtocol { + private let keychainStorage: KeychainStorageProtocol + private let userDefaultsStorage: UserDefaultsStorageProtocol + + init(keychainStorage: KeychainStorageProtocol, userDefaultsStorage: UserDefaultsStorageProtocol) { + self.keychainStorage = keychainStorage + self.userDefaultsStorage = userDefaultsStorage + } + + // TODO: - accessToken fetch 로직 상의 후 결정 + func loadAccessToken() throws -> String { + guard + let token = keychainStorage.load(forKey: TokenType.accessToken.rawValue) + else { throw UserError.accessTokenLoadFailed } + + return token + } + + func loadNickname() throws -> String { + guard + let nickname: String = userDefaultsStorage.load(forKey: UserDefaultsKey.nickname.rawValue) + else { throw UserError.nicknameLoadFailed } + + return nickname + } +} + diff --git a/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift new file mode 100644 index 00000000..99f37db7 --- /dev/null +++ b/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift @@ -0,0 +1,17 @@ +// +// UserDataRepositoryProtocol.swift +// DataSource +// +// Created by 이동현 on 7/20/25. +// + +/// 유저의 이름, 토큰 등 유저 정보와 관련된 데이터를 가져오는 Repository +public protocol UserDataRepositoryProtocol { + /// 저장한 accessToken을 가져옵니다. + /// - Returns: accessToken + func loadAccessToken() throws -> String + + /// 저장한 닉네임을 가져옵니다. + /// - Returns: 유저 닉네임 + func loadNickname() throws -> String +} From a3f03b7b2823fcca79785c06b29e0a1edf3a54f7 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sun, 20 Jul 2025 23:15:21 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=20(#T3-104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이페이지 화면 구현 - 의존성 등록 - TabBar에서 Myapge 클래스 삭제 --- .../PresentationDependencyAssembler.swift | 7 + .../Sources/Common/TabBarView.swift | 15 +- .../View/Component/MypageTableViewCell.swift | 57 ++++++ .../Sources/MyPage/View/MypageView.swift | 177 ++++++++++++++++++ .../MyPage/ViewModel/MypageViewModel.swift | 70 +++++++ 5 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift create mode 100644 Projects/Presentation/Sources/MyPage/View/MypageView.swift create mode 100644 Projects/Presentation/Sources/MyPage/ViewModel/MypageViewModel.swift diff --git a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift index 22c9ae6b..686b5547 100644 --- a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift +++ b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift @@ -39,5 +39,12 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol { DIContainer.shared.register(type: RecommendedRoutineViewModel.self) { _ in return RecommendedRoutineViewModel() } + + DIContainer.shared.register(type: MypageViewModel.self) { container in + guard let userDataRepository = container.resolve(type: UserDataRepositoryProtocol.self) + else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") } + + return MypageViewModel(userDataRepository: userDataRepository) + } } } diff --git a/Projects/Presentation/Sources/Common/TabBarView.swift b/Projects/Presentation/Sources/Common/TabBarView.swift index 03e28180..5a859f38 100644 --- a/Projects/Presentation/Sources/Common/TabBarView.swift +++ b/Projects/Presentation/Sources/Common/TabBarView.swift @@ -38,13 +38,19 @@ final public class TabBarView: UITabBarController { tabBar.tintColor = BitnagilColor.navy600 tabBar.unselectedItemTintColor = BitnagilColor.navy100 + guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else { fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.") } + + guard let mypageViewModel = DIContainer.shared.resolve(type: MypageViewModel.self) else { + fatalError("mypageViewModel 의존성이 등록되지 않았습니다.") + } + let homeView = HomeView() let recommendView = RecommendedRoutineView(viewModel: viewModel) let reportView = ReportView() - let mypageView = MypageView() + let mypageView = MypageView(viewModel: mypageViewModel) homeView.tabBarItem = UITabBarItem( title: "홈", @@ -89,10 +95,3 @@ final class ReportView: UIViewController { view.backgroundColor = .white } } - -final class MypageView: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - } -} diff --git a/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift b/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift new file mode 100644 index 00000000..6b942a02 --- /dev/null +++ b/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift @@ -0,0 +1,57 @@ +// +// MyPageTableViewCell.swift +// Presentation +// +// Created by 이동현 on 7/17/25. +// + +import SnapKit +import UIKit + +final class MypageTableViewCell: UITableViewCell { + private enum Layout { + static let titleLableLeadingSpacing: CGFloat = 20 + static let chevronImageViewTrailingSpacing: CGFloat = 6 + static let chevronImageViewSize: CGFloat = 36 + } + + private let titleLabel = UILabel() + private let chevronImageView = UIImageView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configureAttribute() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(title: String) { + titleLabel.text = title + } + + private func configureAttribute() { + titleLabel.font = BitnagilFont(style: .body1, weight: .regular).font + + chevronImageView.image = BitnagilIcon.chevronRightIcon + } + + private func configureLayout() { + contentView.addSubview(titleLabel) + contentView.addSubview(chevronImageView) + + chevronImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(Layout.chevronImageViewTrailingSpacing) + make.size.equalTo(Layout.chevronImageViewSize) + } + + titleLabel.snp.makeConstraints { make in + make.verticalEdges.equalToSuperview() + make.leading.equalToSuperview().offset(Layout.titleLableLeadingSpacing) + make.trailing.equalTo(chevronImageView.snp.leading).offset(-8) + } + } +} diff --git a/Projects/Presentation/Sources/MyPage/View/MypageView.swift b/Projects/Presentation/Sources/MyPage/View/MypageView.swift new file mode 100644 index 00000000..dd4c523b --- /dev/null +++ b/Projects/Presentation/Sources/MyPage/View/MypageView.swift @@ -0,0 +1,177 @@ +// +// MypageView.swift +// Presentation +// +// Created by 이동현 on 7/17/25. +// +import Combine +import Shared +import SnapKit +import UIKit + +final class MypageView: BaseViewController { + private enum Layout { + static let titleLabelHeight: CGFloat = 54 + static let settingButtonSize: CGFloat = 48 + static let settingButtonTrailingSpacing: CGFloat = 8 + static let profileImageViewSize: CGFloat = 80 + static let profileImageViewCornerRadius: CGFloat = profileImageViewSize / 2 + static let profileImageViewTopSpacing: CGFloat = 32 + static let nicknameLabelHeight: CGFloat = 24 + static let nicknameLabelTopSpacing: CGFloat = 12 + static let divideLineHeight: CGFloat = 6 + static let divideLineTopSpacing: CGFloat = 28 + static let tableViewCellHeight: CGFloat = 48 + } + + private let titleLabel = UILabel() + private let settingButton = UIButton() + private let profileImageView = UIImageView() + private let nicknameLabel = UILabel() + private let dividerView = UIView() + private let tableView = UITableView() + private var cancellables: Set + + override init(viewModel: MypageViewModel) { + cancellables = [] + super.init(viewModel: viewModel) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func configureAttribute() { + view.backgroundColor = .white + + titleLabel.text = "마이페이지" + titleLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font + titleLabel.textAlignment = .center + titleLabel.textColor = .black + + settingButton.setImage(BitnagilIcon.settingIcon, for: .normal) + + profileImageView.layer.cornerRadius = Layout.profileImageViewCornerRadius + profileImageView.layer.masksToBounds = true + profileImageView.backgroundColor = BitnagilColor.gray40 // 임시 + + nicknameLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font + nicknameLabel.textColor = .black + nicknameLabel.textAlignment = .center + + dividerView.backgroundColor = BitnagilColor.gray99 + + tableView.register(MypageTableViewCell.self, forCellReuseIdentifier: MypageTableViewCell.className) + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + } + + override func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + view.addSubview(titleLabel) + view.addSubview(settingButton) + view.addSubview(profileImageView) + view.addSubview(nicknameLabel) + view.addSubview(dividerView) + view.addSubview(tableView) + + titleLabel.snp.makeConstraints { make in + make.horizontalEdges.top.equalTo(safeArea) + make.height.equalTo(Layout.titleLabelHeight) + } + + settingButton.snp.makeConstraints { make in + make.centerY.equalTo(titleLabel) + make.trailing.equalToSuperview().inset(Layout.settingButtonTrailingSpacing) + make.size.equalTo(Layout.settingButtonSize) + } + + profileImageView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(Layout.profileImageViewTopSpacing) + make.centerX.equalToSuperview() + make.size.equalTo(Layout.profileImageViewSize) + } + + nicknameLabel.snp.makeConstraints { make in + make.top.equalTo(profileImageView.snp.bottom).offset(Layout.nicknameLabelTopSpacing) + make.centerX.equalToSuperview() + make.height.equalTo(Layout.nicknameLabelHeight) + } + + dividerView.snp.makeConstraints { make in + make.top.equalTo(nicknameLabel.snp.bottom).offset(Layout.divideLineTopSpacing) + make.horizontalEdges.equalToSuperview() + make.height.equalTo(Layout.divideLineHeight) + } + + tableView.snp.makeConstraints { make in + make.top.equalTo(dividerView.snp.bottom) + make.horizontalEdges.bottom.equalTo(safeArea) + } + + } + + override func bind() { + viewModel.output.nickNamePublisher + .sink { [weak self] nickname in + self?.nicknameLabel.text = nickname + } + .store(in: &cancellables) + + viewModel.output.externalURLPublisher + .sink { url in + UIApplication.shared.open(url) + } + .store(in: &cancellables) + } +} + +extension MypageView: UITableViewDelegate { + +} + +extension MypageView: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return MypageViewModel.MypageMenu.allCases.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return Layout.tableViewCellHeight + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard + let cell = tableView.dequeueReusableCell(withIdentifier: MypageTableViewCell.className) as? MypageTableViewCell + else { return .init() } + + let title = MypageViewModel.MypageMenu + .allCases[indexPath.row] + .rawValue + cell.configure(title: title) + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + defer { tableView.deselectRow(at: indexPath, animated: true) } + + let selectedMenu = MypageViewModel.MypageMenu.allCases[indexPath.row] + + guard selectedMenu == .resetGoal else { + viewModel.action(input: .didSelectMenu(menu: selectedMenu)) + return + } + + guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else { + fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") + } + + let onboardingView = OnboardingView(viewModel: onboardingViewModel, onboarding: .time) + navigationController?.pushViewController(onboardingView, animated: true) + } +} diff --git a/Projects/Presentation/Sources/MyPage/ViewModel/MypageViewModel.swift b/Projects/Presentation/Sources/MyPage/ViewModel/MypageViewModel.swift new file mode 100644 index 00000000..d91e0ca8 --- /dev/null +++ b/Projects/Presentation/Sources/MyPage/ViewModel/MypageViewModel.swift @@ -0,0 +1,70 @@ +// +// MyPageViewModel.swift +// Presentation +// +// Created by 이동현 on 7/17/25. +// + +import Combine +import Domain +import Foundation +import Shared + +final class MypageViewModel: ViewModel { + enum Input { + case didSelectMenu(menu: MypageMenu) + } + + struct Output { + let nickNamePublisher: AnyPublisher + let externalURLPublisher: AnyPublisher + } + + enum MypageMenu: String, CaseIterable { + case resetGoal = "내 목표 재설정" + case notice = "공지사항" + case faq = "자주 묻는 질문" + } + + private(set) var output: Output + private let nicknamePublisher = CurrentValueSubject("") + private let externalURLPublisher = PassthroughSubject() + private let userDataRepository: UserDataRepositoryProtocol + + init(userDataRepository: UserDataRepositoryProtocol) { + self.userDataRepository = userDataRepository + + output = .init( + nickNamePublisher: nicknamePublisher.eraseToAnyPublisher(), + externalURLPublisher: externalURLPublisher.eraseToAnyPublisher()) + + do { + let nickname = try userDataRepository.loadNickname() + nicknamePublisher.send(nickname) + } catch { + BitnagilLogger.log(logType: .debug, message: "\(error.localizedDescription)") + } + } + + func action(input: Input) { + switch input { + case .didSelectMenu(let menu): + handleMenuSelection(menu: menu) + } + } + + private func handleMenuSelection(menu: MypageMenu) { + switch menu { + case .resetGoal: + break + case .notice: // 임시 url + if let url = URL(string: "https://www.google.com") { + externalURLPublisher.send(url) + } + case .faq: // 임시 url + if let url = URL(string: "https://www.naver.com") { + externalURLPublisher.send(url) + } + } + } +} From 1ae7ba7bb20404e5d24617b12209839aec0d4eb1 Mon Sep 17 00:00:00 2001 From: taipaise Date: Mon, 21 Jul 2025 21:31:25 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Common/Error/UserError.swift | 2 +- .../Repository/UserDataRepository.swift | 14 ++--- .../Sources/Common/Extension/NSObject+.swift | 2 +- .../View/Component/MypageTableViewCell.swift | 3 +- .../Sources/MyPage/View/MypageView.swift | 53 +++++++------------ 5 files changed, 31 insertions(+), 43 deletions(-) diff --git a/Projects/DataSource/Sources/Common/Error/UserError.swift b/Projects/DataSource/Sources/Common/Error/UserError.swift index 0f6f7ff7..c8c1e26d 100644 --- a/Projects/DataSource/Sources/Common/Error/UserError.swift +++ b/Projects/DataSource/Sources/Common/Error/UserError.swift @@ -11,7 +11,7 @@ enum UserError: Error, CustomStringConvertible { case unknown(error: Error) - public var description: String { + var description: String { switch self { case .accessTokenLoadFailed: return "토큰 불러오기에 실패했습니다." diff --git a/Projects/DataSource/Sources/Repository/UserDataRepository.swift b/Projects/DataSource/Sources/Repository/UserDataRepository.swift index 633d22c3..1eb3d511 100644 --- a/Projects/DataSource/Sources/Repository/UserDataRepository.swift +++ b/Projects/DataSource/Sources/Repository/UserDataRepository.swift @@ -5,8 +5,8 @@ // Created by 이동현 on 7/20/25. // -import Foundation import Domain +import Foundation final class UserDataRepository: UserDataRepositoryProtocol { private let keychainStorage: KeychainStorageProtocol @@ -19,17 +19,17 @@ final class UserDataRepository: UserDataRepositoryProtocol { // TODO: - accessToken fetch 로직 상의 후 결정 func loadAccessToken() throws -> String { - guard - let token = keychainStorage.load(forKey: TokenType.accessToken.rawValue) - else { throw UserError.accessTokenLoadFailed } + guard let token = keychainStorage.load(forKey: TokenType.accessToken.rawValue) else { + throw UserError.accessTokenLoadFailed + } return token } func loadNickname() throws -> String { - guard - let nickname: String = userDefaultsStorage.load(forKey: UserDefaultsKey.nickname.rawValue) - else { throw UserError.nicknameLoadFailed } + guard let nickname: String = userDefaultsStorage.load(forKey: UserDefaultsKey.nickname.rawValue) else { + throw UserError.nicknameLoadFailed + } return nickname } diff --git a/Projects/Presentation/Sources/Common/Extension/NSObject+.swift b/Projects/Presentation/Sources/Common/Extension/NSObject+.swift index 0ee07cd4..66e8907b 100644 --- a/Projects/Presentation/Sources/Common/Extension/NSObject+.swift +++ b/Projects/Presentation/Sources/Common/Extension/NSObject+.swift @@ -7,7 +7,7 @@ import Foundation -public extension NSObject { +extension NSObject { var className: String { return String(describing: type(of: self)) } diff --git a/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift b/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift index 6b942a02..54046b63 100644 --- a/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift +++ b/Projects/Presentation/Sources/MyPage/View/Component/MypageTableViewCell.swift @@ -11,6 +11,7 @@ import UIKit final class MypageTableViewCell: UITableViewCell { private enum Layout { static let titleLableLeadingSpacing: CGFloat = 20 + static let titleLableTrailingSpacing: CGFloat = 8 static let chevronImageViewTrailingSpacing: CGFloat = 6 static let chevronImageViewSize: CGFloat = 36 } @@ -51,7 +52,7 @@ final class MypageTableViewCell: UITableViewCell { titleLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.leading.equalToSuperview().offset(Layout.titleLableLeadingSpacing) - make.trailing.equalTo(chevronImageView.snp.leading).offset(-8) + make.trailing.equalTo(chevronImageView.snp.leading).offset(-Layout.titleLableTrailingSpacing) } } } diff --git a/Projects/Presentation/Sources/MyPage/View/MypageView.swift b/Projects/Presentation/Sources/MyPage/View/MypageView.swift index dd4c523b..f2749b25 100644 --- a/Projects/Presentation/Sources/MyPage/View/MypageView.swift +++ b/Projects/Presentation/Sources/MyPage/View/MypageView.swift @@ -5,15 +5,13 @@ // Created by 이동현 on 7/17/25. // import Combine +import SafariServices import Shared import SnapKit import UIKit final class MypageView: BaseViewController { private enum Layout { - static let titleLabelHeight: CGFloat = 54 - static let settingButtonSize: CGFloat = 48 - static let settingButtonTrailingSpacing: CGFloat = 8 static let profileImageViewSize: CGFloat = 80 static let profileImageViewCornerRadius: CGFloat = profileImageViewSize / 2 static let profileImageViewTopSpacing: CGFloat = 32 @@ -25,7 +23,7 @@ final class MypageView: BaseViewController { } private let titleLabel = UILabel() - private let settingButton = UIButton() + private let settingButton = UIBarButtonItem() private let profileImageView = UIImageView() private let nicknameLabel = UILabel() private let dividerView = UIView() @@ -41,19 +39,16 @@ final class MypageView: BaseViewController { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { - super.viewDidLoad() - } - override func configureAttribute() { view.backgroundColor = .white + navigationItem.rightBarButtonItem = settingButton + title = "마이페이지" - titleLabel.text = "마이페이지" - titleLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font - titleLabel.textAlignment = .center - titleLabel.textColor = .black - - settingButton.setImage(BitnagilIcon.settingIcon, for: .normal) + settingButton.action = #selector(settingButtonTapped) + settingButton.tintColor = .black + settingButton.image = BitnagilIcon + .settingIcon? + .withRenderingMode(.alwaysTemplate) profileImageView.layer.cornerRadius = Layout.profileImageViewCornerRadius profileImageView.layer.masksToBounds = true @@ -73,26 +68,13 @@ final class MypageView: BaseViewController { override func configureLayout() { let safeArea = view.safeAreaLayoutGuide - view.addSubview(titleLabel) - view.addSubview(settingButton) view.addSubview(profileImageView) view.addSubview(nicknameLabel) view.addSubview(dividerView) view.addSubview(tableView) - titleLabel.snp.makeConstraints { make in - make.horizontalEdges.top.equalTo(safeArea) - make.height.equalTo(Layout.titleLabelHeight) - } - - settingButton.snp.makeConstraints { make in - make.centerY.equalTo(titleLabel) - make.trailing.equalToSuperview().inset(Layout.settingButtonTrailingSpacing) - make.size.equalTo(Layout.settingButtonSize) - } - profileImageView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(Layout.profileImageViewTopSpacing) + make.top.equalTo(safeArea.snp.top).offset(Layout.profileImageViewTopSpacing) make.centerX.equalToSuperview() make.size.equalTo(Layout.profileImageViewSize) } @@ -124,11 +106,16 @@ final class MypageView: BaseViewController { .store(in: &cancellables) viewModel.output.externalURLPublisher - .sink { url in - UIApplication.shared.open(url) + .sink { [weak self] url in + let safariView = SFSafariViewController(url: url) + self?.present(safariView, animated: true) } .store(in: &cancellables) } + + @objc private func settingButtonTapped() { + // TODO: - 추후 설정 페이지 연결 + } } extension MypageView: UITableViewDelegate { @@ -145,9 +132,9 @@ extension MypageView: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard - let cell = tableView.dequeueReusableCell(withIdentifier: MypageTableViewCell.className) as? MypageTableViewCell - else { return .init() } + guard let cell = tableView.dequeueReusableCell(withIdentifier: MypageTableViewCell.className) as? MypageTableViewCell else { + return .init() + } let title = MypageViewModel.MypageMenu .allCases[indexPath.row]