From b928577171469f5bcfaed7608d988eb113849cb3 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 16:44:22 +0900 Subject: [PATCH 01/47] =?UTF-8?q?add:=20#18=20-=20icon=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ic_empty_search.imageset/Contents.json | 23 ++++++++++++++++++ .../ic_empty_search.png | Bin 0 -> 4490 bytes .../ic_empty_search@2x.png | Bin 0 -> 8693 bytes .../ic_empty_search@3x.png | Bin 0 -> 12965 bytes .../ic_empty_trip.imageset/Contents.json | 23 ++++++++++++++++++ .../ic_empty_trip.imageset/ic_empty_trip.png | Bin 0 -> 6087 bytes .../ic_empty_trip@2x.png | Bin 0 -> 11927 bytes .../ic_empty_trip@3x.png | Bin 0 -> 18486 bytes .../ic_server_error.imageset/Contents.json | 23 ++++++++++++++++++ .../ic_server_error.png | Bin 0 -> 3627 bytes .../ic_server_error@2x.png | Bin 0 -> 7135 bytes .../ic_server_error@3x.png | Bin 0 -> 10566 bytes 12 files changed, 69 insertions(+) create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/Contents.json create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@2x.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@3x.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/Contents.json create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@2x.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@3x.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/Contents.json create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error@2x.png create mode 100644 Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error@3x.png diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/Contents.json b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/Contents.json new file mode 100644 index 0000000..a3ad2a6 --- /dev/null +++ b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ic_empty_search.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_empty_search@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_empty_search@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search.png new file mode 100644 index 0000000000000000000000000000000000000000..1400bbb4223eab85da0cb5014ccb454652218fa1 GIT binary patch literal 4490 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 E00%*UegFUf literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@2x.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0164b29308e9c69c21f6766f50ee706d8c60d4b4 GIT binary patch literal 8693 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 JiAVpY|&j&OuxXj-70*Q6dq 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 KDOD&Sg8vs!j>G`~ literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@3x.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_search.imageset/ic_empty_search@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..81a1e3c6f4da5478b46890e7deefffdb3e97164c GIT binary patch literal 12965 zcmc(GXH=6<^kyhhe*pv$1%V)h-lP|S01BaFsD>s;3tf5uhK zZE&&5inHEpl|3Tp==EOp^=^z0=0%Yd_>S~maKul2?K-_XJlZycS_*0D?Y9e2DqSn& z03;w}BD^g(;2_tk)~)*4yMHeKm$d{#Jk0-v*S&X3q3*}YCb)`SQ{l~C~|KI&40S6?l6m} ziX!I%tk;E(iX=Ux4KLEV-}`xY3j2Ko8@$p(2mq5IRjjSc^La8R#KHkJ&q3l}`Q=V1 z0r@C0S(j4-;(W`GvxabUxsw{X9_I^u zhi6e;Y%kC})c>Q3%zf(aB~h*mvxm%tn>wKyg1~%dzNNLO_$9IT+hdz2>;5+WK`NgU zElc>`4bVUhh!w5Z?9w?F#K8o}W|iX&|HuC!KXUsH zpq2fxG3=;-NEg&E3YPAzSvjQ$P%T@6>QRt-< z+vI5JfusE5X`xteLA7Z*f7z zA8#9u`hjcq*3Jh#ch)_#b--L&&kH9<>$xlv3_nGet+yG!)uxG->83S(1OD_*E^*r$oNckFpwTVThW`}~z;yUERsDQ<@W zGd-PkgZq}DOdCzS&zdUu(WZWujr>K6J|)BH%XbI(4mPZVco~l31-=YEtK?OaJ$7Ne zteGebfW3I{ep}+Cyd3NlYE|-w1DDY52XgPP`$Ya(V>#;AofqTLz<*<8-?1U2Y5qAs zKz34Q&$}wfWDtxr-Wcpe=c5!3F< zS)VG(%X)V`v$PFrasU~ae4wOL2@XDg5&d%sZ81Ud3EhznXMRl#Twx(BWqT;}A*_DR zO&7tc*7Ik~sMBM%uLnPRbc|HN9!~bYzUK@Q1j>C6&=t+i=@jJs$S8|tq5ae9jWYHS z?TmM5&Tw@NuO>^R#g)yziI$c(*-mVrYSeM=I|2tLCMNa-nm9k|{pU}3OnZ8YvW6vM za1Ip7j>r~gz@ch zA#18ZxuF`}Q#EPA(w@NY;bRAx&ISryH5SJ}ztcYi(LLWg3kf~TT!XLX52J$5qrCEp zM_P`CW!Hc4Ro@efgE&Aq+4ilE-|fZ{^%y9yiU~cNoHP4-28jr#Iu*J-6gpH5K2{By z(J!<_j$HHwU#t3Etf!;MbCHh%punHdsa-aD4sFS*MV|fTb8P0w#fP&>XtkHVc8@*w zx&n~1tzmJQ^hhDd6x*`N5-g4Fyhiq^ zEF?P6Yu6w5N@zFEj3d=R+&bivuB zOm?y6rz72f?b!<2>VS0#d-4UE^^QrDKonF&bKbZlo}mQ}@*9u6Wzky?bg zw7rv%czZdXm!`_%_q{r$!2#`|VIMKoUUrcOVEY+EWC*l-D%nR2$i(9;o;~Mb zeO1WOr{j*V(U1>E)E;ZIg|#})@98G*j?y*+Qqsf5kGE@=d@NGL&_z`EPW&`4%^~S# zl#l$KG+VFyt#Pp?NLM%17tL}-IhqQF71<;-hy)%BfkQxzKf9|H_Lf_Xj(?U35TQF3 z7N`Xld@@XE^lz$%JjA|!-W}#E?iP`hyW05OJAfdziw@G7M_H+ys38*j+|Rh72FADY^GxJelyT zbNJZfOhy36Jol?Ahyc=Fl5k)8E;K?(k-7&DQJIU3`E&1|XOkg`NU7{WI6MODu@=eD zBg_-k@ZO+Ktz&@nS=MZ4na(LcP5^j5qU!C8E+PTEe<6_tPPl&;3ZP-vWAh_q4Iu_R zvFgWa35tvVlWH3jlna4Bi@=vMva+5L%M+3V)|9>ULm`Ck&V55QvdKAQ3;>=WUL>tP zUIyL9jKFiea+;rBH>YT5c&Bs%WGFm#)7&`|w$@2U5x(pl_tJ$)t)?Wuz(~BEQm~yY zx3$40&9GL(WB#{GPpho`4;Q}SqCJwjgm;#&n)@T>AMPVTvvso-TwlT=gpZMy^{bHr zbq+i4DkLO3T+agpNDLak{*NoLi_cCG-6)C|uSs~M57n^w&W1lvo)gd_=k=v`To)b_ z%}ZV?-qV+RGX_Dw%AE0;=*p8-;Cny0wRT5^O&(c80JuBmV82r-YrWO%^S$%YbPW2JT9_IiXIW9x5wS*%HK`cH;%k&94F+w> zMfw5^U&UyZ%T<0ND{A^ntU&{XkTE{HRAS!k{-Sx?WqyWPKDfP&3&tUWtI6%TiV4+( z&_?|CqloQmUm>(Dvd2}z5XW&Q*Xq%7MQMk`8oF!*UjGS@ypT=bXtfpD;7rPTdG6(6 zYgrx87sp)ZKe-Ja$;s{F7Txf6KW(@@|FeL&o!%=lC|D9pp^{T6cCUZHVf(rT{+B>qsjKePX+`2t;LZ@v!Ed{?_>aPmo8NVtd2$z-?YB)n z1KX43jzJc{qTl0mH$z!(4a_s8I7D%K2$_>~{-LGW@!$*MNf@2fy?~Z%DQh>k1ELMV zKu2DC9<$tlD^-_-)!S3Yx{5BH%*3IO{CPirLPr`8`=*06U1kr;>pX@eW?QY2EB2Wa zIu}H4{fx}KQg3jn?5c5Frav?+g8!~Pm-ao#|Kl#*a`=m{{ASV-MtA?_h!y@XV?fsc9>kJVf?H8ph|(%g6xrm8J&E!h%A ziJ3P&#`6|=cdC~8W6?Blzo&dSg^~WbbPu#-Myf}(WMxR9F}^Z5~j0eZ8r0 z7RH@VfKn8fmE|*OcA;ltbQ?$$NLJei_UG{aq@dF+Hy`>{C;GDg-r(7;fO_qn!qi44 zowHBcv0Z`@Eq;pJc+Y!CoGwKVWs-23vBA3B&nLKy3qBq;x%Rn@HI_(CbInkj+@J!phjfg^yXwCy#eB@4aGxD zXTo11_mi;&)UriTi`aAJ>t9*br^WJH2}Z^N=ij60QqsYusdt~1-FK6-<#D}u zHg$_*7;BgQ2X*pjwc4d*RoQ zP@egmqqYOF1kZ0$4goh8n_VE|Z;jx#0;}3{O9Fm-hQ;bk+~=BD%5HQvwtwrRAG?z> z&kQA64ynHU+T9(!Y_z>ymIs<7tV%J|8X_5wWNIGW)y=a@+hMuSzX}J%(m-npuAWb4 z*|a*v;}i&-4apekzv(}lw9lUriY0-l!+itw zFtu_{vtQ${C!jq^59-r*Zw}khE{UA4!=g*;i5pAf!XFj4< zz_qZ1PVMq;Y(Je4k}5tf>Kn>RC??jx?}?qM${nr*T|uV@C)p-sg$qu{3`Y~fwa%~- z2^VODGf+t3naM|O|IrSVu!ip(Z71GqxL@}xYkJcBATju@55g$4D-|3R6Y`^7T7`lC zwQ_j%e%zZEs=NCrsF57y@H>*QHQcUV0p-B_x$^wFG-laZs$*C%<@ms9n0Wk;&W;du z5#V(gFaC%)o~WNyZ8ZYCl(IusE;fn9ko@qtd7twof*j?q`1wk&$-t;XxnVu@^Z;{m zr+n5IH&23TH~Bh~PmxW*@f@UxA&&G}vQ$Xdw$N`BQBSHV(C34Og4@>z(x)FCFxE&v zV;V(iIY&{`sUwzktekXh1WC|)8&wtwa7HB=DyVXQB=#YOET2PPWj$0E4VIMqsIOBA z8VlsanK$A0B1NShz%ewXE4YK>gB|tyjY*23Yp31u|D- z*|WDXjGZJK9^;H`4%(6_-*k8YjPq)={!h0J;Z<1B;JpV|;Mgw33%XOeDv@gRNx7l5 z*m^#&Si`6K?LJD(dolJ2sz8K+4;lZdjZJZTs|}k-bA*}dpxKiyw7pZ%U^~QE9+(gR z9hnj|$j|&zU^PPeS^1)mze?$nSDGhA5M}1W5~82+F$7t^+e8W5oW9*wo=R>r3ZN97 zlSwq+(kZ{6)#4>Fsr6~KVx85)xqnVBp7-ZyP({&2tuY8k*JvI4*B zLw|=3d@*L*qb1}KKTeu$HMLVdi<$Br#-&m-tIN1GE-kbQSFSb=%g+ifqAJw=!)p~& zTfRggtF@acU~Ge8By!i=-(Y6oCZjyy%wQ$uIHS4aXu06x&?y6Wna>Mj!)t06B!8x* zt*uQi_}diZyzu3XtWm5MX=U88t#aFTnoeuIzESkvq#coj$IiEg$Z5vovI6~Tb+}6z zQY>3TQ~G?hb;)sd^wsslcxcQII+t%r|IpNCBPIRVAQ5#>KxHG6*A@+!m6 z;>r*lDf%66ltW&vI9B2}dP_uHXUz5P8lQOAa?HynkH2s=yoYcgL}=*tm3Nush)$qqP#`>r~Wyi~RL|XT6S}W`KYsW+mv#3;MD~~B$g*i|$yJ5EPE(;K zp)?>AD%-8SDT1rc+w;1j35L$KZCh5w!qsZUcmB2bKHGp8NBy!n+89c* zuwAk==`^_+k-Scu6>fG#{K#Sif>D$4THL8OuE#cxT~G3r)LD(r@JCnp=&P44?kHA% z#h>;o#LQNgwP*lE+K9Sbz~LRg<5aHg8&`}7jpanPt#W@6k&ylS2V13GeH*aioQsUZ zR{f~t5M=Pd$yDRmC-l3qEMDLPoH1gw;akB`HC%3go8CM9K*f0|Jjt0f7oJ>hL4bKs zfKVlo_*6-(qf4g+j2L96Qh!ewh5THAH%-_(EI%jVv(Lc%7pp0OMUhFlI`iH&w z<^h4`g|(=U^4?GEopQ1pcTy(TS+jf=$g0uOpM}fBWh?g7Zn{?ut<;qRwMk&EuKN=U z3)!A!Bf&REhgCiu8n3!ceZZ41=EdpMQ!|-uH-ywb_=dJ-KZ;(!>^}>|ioe%1>9nsk zWr(#+TmH2OavN`~CZO z5{c-MOFO9Lm#x08cq2(U$;NQo-_4N}5?TIP@aW&)eU3pl8Fsme%eT1Y>Z_*=nj<5| za`E7{b9-)ak+OlJQZ|J>70?q@cU*nQpF>V7x7B7*yX~$X;?J*SyQ)&OdOq-l8DyYe z^{s>3;&7C@LiV9=6#C+%z2&q#8}KN`l_Cnu+EQoT_kVhvt+#UwlDDU_nh8%DB96nT*%9{B8NZQ8kZBo07afiT zBE^Vd7FE;Remt@#dA=t~{kHbl40f*GySZN}G`z{J_1G}J1!7|2#~ZsK#Jq^>M_s_`Yhb(;=I~lNM#8;I?#gf3=#GiAT&)YWe~xK|xhPpcibY`1+8P zkxe;_t^|FO_Z+G~iE-=Y>yxKxyGi9wQ>e0p>k#&CsL~J~;OPVA6o^ECGMo)#p1f1g z&@?ZYP*jpYt3|}~#ediG3%fG*BC+rlCs1sBd;q$@i5I!zRxxyF&c#%d{nafA zqTar;T|bg{^Kx-B_vv}-mr%Ue>~=^VWrKQ|*F%bqqzd$b`-MAw6MKj`<(pEEhUwmq z3_=F`_4V~P9XD{+E^luob0&F%vjD04Q2yDM?s5rFJ(!ZHJP%g7GZpn@D!bRq2VAi8 zuTGviJB(_}3cg;FXB1PM+0tSqEfAHK#yjH1?2cb+qZPBVGQV5u|IJr`K9t;GpS6i3 zNpQYJzLITlbmF%t*PlwhD%x;8cz=qK*yRi-evj0Xaio{Azy9z~Wh#zEO|kXE1W!=b zfJTLB3;M#(JFsYNOt-E@pCIJ8NS&>)`*XZT`uOqd7cDauDQ{hI8U~N9R@vUX=c?K8 zkNC3-=1yUW^Xe#7G^@Az_*C{vX>qf|_q68v2Yun%XY_g<$T(D8rI%jBGJDy+*>n>g z!BMl}3GzB5M`S5iGeQxc&O2IMJD!>ZvznTV(!;p+*i()0z;Zlpy-sQ<>(@`tgh!O@ z>*N(Joz{|*gW$B^NX0@SsA7N*xHUc{($Blcv8n%eUzgFDpTCsEuua>FrZJ`{7?JO( zL_aRbFg1&l^>k{{wg`(OtH7_JS8(v;cD}~IQ(3u>)@|{~0q;r*B>DjKD$@g7rY|@> zcE}84%w$CBAzmw2LMV#Fk=?_SuF3dB+GOBTXDP#aGl^@b)2tv_GF-zN~J&e|Q`+0lQwPpSO#ozc~enibU|NJa5esXX-IycqU z@k3^YV;~m0$lbES=s?w8aee6Om!*jnHPUfN8J84jzBCkd3;kvY{D45_$eh^AR z2~0pIjG16I)s?woF1P}0@WHKZd7Y8AE$3N%6&lw0VVU(c_nhrN8JTa~WD+(uH;dg{ zvOVich)UtTFdpO3P0Y&@?o7TV-_sHU5KkbRsl?=L@M#XV!Nv=j<5gwq@HvKytQP~> z-rLK`%*smEK5eWH)RMpX8$a!ID@aeQg7BgZe~uCP#0yMCS8MT_P4#ElFlX7w0+Hh8 zhVG*JYH#vo4*f!P@vAY1P5kLo{v1uB-%}>8sm)!uJq>w7cp`F@ANKVM`>}5COr;_> zz+t|6p0R=2_|A;R zhcIXMFp|Wc$;3nA0=Hgc&y*I*`k++no#TW>S+9mj!O7X1LGc*V%D8*!g481ykhQ~Z z>=-YP>+ItSdh~2L8LDJPa1ue&BEeab?YH-)_1ZB?Odr~=@kVooS1mnZBS6;2O0~Gg ztV~bmcAaibhN?Gm^DU^hcB@F(0M@;ma^o&*0FySi{YwQQposKHu-TLwAXyZD{Xt1m&e`V6Q_VFn&7|7WW7nz7PjVw#iOmf3O_}$O|G5| zlBdi-FCg=x4%+899Cj4c(gCXH+Y5@L8)4BJf-OGgMUo&3O1tT#_AXR!hFZH7F|z)F zb{5v}m3y>Z^3pM0r&lVZT`W~ed!8F!o6?H*!=$vomlvmhQs#En=RN4BQwNR;*X8IH zyWaC$=6B~R5Wfwxggk@}@Wvho8(?%KC@LYfasH@l8qODWlVm%LtQQ*%_QCb7iE ze11s=K26ykBZ;iBL;|_|`xC^h%JZKfN)x#A^dN0mPd{$fx=N|8nbxPxNxEem_Ip%x zDswM3ya}KejsYvbx`!IYH-2ojx^Kb3h@}&)C->sEY?Mb z2F^8_va z`)YDz-<<-~DL`5H_MkKl{jkW|s}sR9N`1 zlnfaXLLgA+L-}e;_T;z1Q;vblE9;Xf0>quYALC_e<cNgecsMhfpdC`G=GJg%c}4R6MuJ;?HYhtd?1 z@#EGA^?XN8_JN}NF0Gpri$#H43hxXB2RFtClm;tPM(y6uIUfZyUjV6MbE$V|vi+R! zK_8D5Y%a^;6!H$1yB!;m1e4k~re9K% z=G16p`W5-cMK?Awt!jPYqcvh?)X^|TaZHFXsns9ip@=&tyr`YTvxE3XnSHRtWrx48BaOfRLy#gfM1Vd2&Ns+u;bDb;qC>}B3h)@Jy` zyKy(YLBQ+Ah3CK!qR>||hu=Tmd~A-yBZUxqab(xozk}q)rk8IheSFjOB-dF>tJXeT zLv$bS$o8|cv#q;Y6%h_sQ(?6M#P)ZA9M1I9sHzh9kDgJ6i)cHBvNq{~fmYWx=kNPs zF_|Pc@i%?VI9!Fc_sZ9hiT0uJ6IAyTeGE!Zxl}UDF`+r>yfH$KRJ{B6hH{@tk!iSX60UZb;NE_e#l{4l( z5#){QcAX_x9K)Kf)6pjz(r3^fYwYz}cxYIX@f_97PVhxR+VFWjD!`brGshbuoi*eR zL~^2RVLj*PU$_7j_8@x`UsKO^-QpbFFKdz9d>#ZNGt$0~m>&as?S@(`x$`?h1~Zuc zvhIjji!A}5Prqp%hpk0$dr{Cq+dIX04BD;->xsVor6$O7K;1fr(UNxk?a^1u5jKBM zc_c1mK%eAC>hTN^z83%B!{4vlQzkw?M@hY*-(D~`LdppHon#KvNyvDaVNs5$gt4u6|@ZO`y1=t&B+a(jKIk^jm;AsGqNZxqIk$G!S4QFbW z5;E%cN`Q04($i{qLnwRY457V#Efg}l|B8~h0Pu~N(u6f^tKJ*4s?h)4Tx4B*-}c@8 zalCO{p`>f}?)gd%%)A@`fy8i>EWKB94<0&{B)~bm{^w3aFzW6vi23QWw%@7;2S4|; zPZM@1n7D(#3=6G`k8-}T7!=CaqLReTR=yCEKJZ5?l~N*m=_aqigmWNrh9k;nPx`x^ z2%d7;XNYM#=n~V8a;m{wO@e#w&w9_lZ=fXIGvP}O`EKzzzay#6v)EJQi}!M)vP9Mi zu|V=Kh!vb@)Er-5Z@;}>F1fqq2iy_WsASboWna%Bu!u}&ZN|qLCW^0!_7^tvBLs=4nY51A zs|`KMwE5+;gOW(#ZVuT3BQVeygU(K|Y(!)NDs_hhtm$GgQGROC( z*A!*j*4j#jl=xS8pZvWAUNk@TP@I7I7M(!ah6nT)9iSG!T5$oLTxwQtP%q00CGZ@| zTB`rP?++#>2cc3p{86)vS85704bSKJIEEh+kh^WwK9P0|mwET0^dWhbv)8qpphx0uBEIL~U+DYQnLMER%0 zfG%I$BEAjpWC&6-^_bE_k^}MqA(f(CU3LGyj0o44wL1B(|2%p?0gLky!*-aPA zFPm1+_0JZx9C|5da>D*+=8E$)(4HQPCq8-iKz{8jiU>%F16%F6Y4vfIKIMmuj0~QU zBU-=QjbYr`{;;)mJjZ~(Pp#vztIGZXvGW*s^alwc`J^wZtK`J0>->TpoM!@4Oi}21 z3n$B3cfDNOTBKeSKmEFY5A{VOsW4bD`L?dbPN7r_X2Dh)p8_?%Al<&xz2*@&amo0) zzb-{yuq#`xAmLH8S?j=0WopylGv?4VjX)4jy^~K{%azsy|esf9)}U#U4KoQ^31Xt4!jvPH;G z7bzSgRkC?)w=9YScqT$V`&;i@BI=BxEQQ2yONCM|{CVdY?$^KQXnrH7_Q{I#XdTZ> zR@3KtRs4@VLX5ZQ-bZrNRz9knp+C1TN5$|9xbGnS$A^q4u%Gv0%dM*qWu_8{V2fJ~ zU)8$q>>6_;ryLrI=b6ZSz~>qmD7*Oj$MYorT~x&5AcUiDSQv2&#_j9FRj(CW$k6?M6+I$A@$S;C4||KIZP$0+-*Qs!)03gP z_z{uD()bS8*@cv zJeyhjusp?z^Bd)*&C|5)Ol1&(+f-PgPat2Jp&R}k&+Ku7->Y0sQ4|6 z-ax7U?KRamZW%Fgz`MV*xU=Bi_Qyf)t40rX=6f31b!Z;qc|-T{>*8k7cD6)8Xp`07 p{&?wqHH!HE`$rJ}F9q&j2d7qBnknOi6aH0Id#?SgO6gVT{{q3w%%T7Q literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/Contents.json b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/Contents.json new file mode 100644 index 0000000..ea63b28 --- /dev/null +++ b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ic_empty_trip.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_empty_trip@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_empty_trip@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip.png new file mode 100644 index 0000000000000000000000000000000000000000..307f1f2690d519b584152b2173c8f1f327ee2c73 GIT binary patch literal 6087 zcmZ{oRa6x2)5n*kOKFw{k?sb`r6gB!T{q#J=n0VO0< zT0|ONf9JjaU(EA8^PD*|b2W43lVk#UNKL^?0RRA~_4TyP{%Oj8Mn>}Qt?LD={S$J3 zJ*#H`044o@MgVw|5Bm2>@XYL?CZKMT{pUZ2$Q^791^^nVxn`w@9z_F8(>O>4hyVAs5eBNWyTR-DExRt9=V6@U zXA|w^G!&km5EtDqW891K)btGL_tlSo*g|!{y1t15)X|WMzP8)jJGbAnDpY-N+8Tba zZ;(GV9#$59Pub27aZ-}A_Xg^tH~r2S##c@s<{ zTX?6^XhvMf5z28nxW$4 zhA1tPjK>d@mD#?`iVM^6(Nn#I3PIOoja}euW&ZDN0<+3;Ys=rYf!vsQI;pH^<`}hH zYmpqnI12-&gsiI#&ac?dnsVQ{N>!3fX@!y;p|U+#MbFZIHc;mIyRwG(l+?>qnX#hGMT-?f%}7skRpmvGe@BP_z{ zc1zzv+u$Au+TfL){-g>`egyh7`TB_3U)^#YK5L{r*{Wzoo=Ciqh;ZZz+lqXPG^%e| zX`h{#0JO8_XLST^+)Ise25#z8IUqt*z8UBgJw@2(q(LjwWZ(lRO0RJxHxP0FhM>MF zEvnTnwmo0Xcc&eOrx;j3T(LxOVIW_VWOhc+nCGuu`Dmc&GIs9M1Ao^#uoIUDZ!p>(c`nBEC7a{YJZ{G?W2bnoeU4ni7dyFMiF^I7zD z!rZLJDy1P#hV!P2BLTlx9)WY$)p;j~pamL>lq)t~Z>Jl>t}c}p@_z5=o@135r{ycc&)Kc<4(QU-S}ZC5f>Pv}@jx_3M{tyxpQ06i;rieQ&P# zgnd){)peJX)k}?;MIIpi_}S6-?dEo85gWj6%MfsOX*bBey-W$2nMm}AgdktT_ z_;bPv-iF}PJuFtt)aR!!V}xNr!a#7FLI@M3Nm8%4d`E4fi)@Fh0aY=5p5qmaxroAT zZ_B2xnZc5Oz|7S64p!|pLda?j@N9mg1Nd)j5iBdCvjTH!k`2CLYlBsF8W6@8hlAQ^(1k&I2KZ2P0k!XTsJu>(J09xCQ0KV|3$$X_2->;p!p(qv^ zx9Pnl%m|?r1(7F|JEyO9>gNbCBUANpw;eaqc%@6 z@gLpX{!cbx`yJag?uj~EZ-c)TpX@BSM{}t~wAaFVKR3GkJY4&@5nI*3)~8Bdei`QU z433`pa%T3-9Ix|lL_rg^7%{!0F*DzMXcQO?p%C5Eu82W?x@Y|K;vWvoP;{(=i=68RioT+c~7U%h#G@GUil@ znvqOmB$Sl>VG2E7`}+jR*WPjW=paVM zeNxeW%7s-T?(2WqVO%Z3SlThwjiM?Faj*GC>XId!#p(;_X-PzkWC*)Uhn!nw$ZE>j z_UPrdN)Iy$vI}@)JV<);E&O1n&aOkmz#-yjz!y0bo2QNA-ftkxAvyR+`t7GRV+e;f z$xIbUvNyaL_L+LE0BJ9kW}0u4Qx5|W$vhnu^tOuc22M{=$u0M?`E@&m+cTxSg~ja3 zFW1Bf<~6%dy>d8YO|*FNHpAk-Mmzl7GguZ8e*IF^WA4mN*w2hq@r8HovCwfZi^TPh zt(?toM1!O#YUZ(#m&2q|6tq)J{5p(B9t$}|Iw;1?GpmBNzY{sl)Ku}p$piuTPk8)R z$cJ6|e9?G=2tVw(+uxP=9%9^ipbv{P!@UAx-hQc63+=1+`x4Mz@3!Lg>0&L}=$nX5 zB_+woz19U$<|N!UFlJ0s*VR3e;v7TXFQ}c3z@^QVx8PZ9{7RGC;nWpwnsw+Z*L@Lp z3C3a{dYlx=fVh0@3)-k|p0#>JlrpZ>^7# zdjiLJFYc5rMrl=kXhzmOx2-^*UY!TVXcjVa=j}X{uwF4!#tp;Coe6ze+KFQTs}Kt~ z;bhO%?=g%0xDJum0cBrC&*IS*8a1C3E?6!`hb2Ds<2nLSykY`B)ay9L4HY38q6yg6 zr7+#)hCuJfBZ)%*kOz!Dk(;~Ofq~nflkGgo9VX^Ba`b%&V@f7y_sZ#0un`gq4wa7X zuc?78D&MAGND;&e$p!v7j4^H5ygDVMPZeYWfam5nd)#d+W2R}3-wTg8j=bV47~ph6 zBh4g5c^BEsCUvrh@Q(A5BvyTZ@I1LM$7p2E0BSUC!bP*nX}f2E|=i9dZQZeQ*er=rcj>c1RE>u#|_?` z<6wy|1=ahrPlRX}HrPHoiqSLmcHky@yxdR=VE?C61*&Z;_T34hAOQ?d#on265TO`X zaI>TudF=#`2fd$WrIIciG*w= za(D(A8?qneFGZvq7)6!SFKfqSqi)%jf!3G;7M7Y6W8D;%wE-d{J0cC)d5;jQ=eLxTYnhMc z6`M)ns}|4SjBJ_`zQ(Oy3hKH_^T#k;M&=0dxKIFH9TQOgobGtto7L-8S1Aj{0Y2$7 zOz*WZT3$>UmFN!RB-3DGAfjh*IvKEV4O)pxB<}R46v8XPTd#HH2kyK=vz(IP4)3lU zP#yf7t%xMoO))&{zgK_1MBlJH&YWC|BKGNYSEJL3wS0O@sulVgY=hSPB?d97EEop< z{PIWa!tHO~iW)=2k|{lHEZK(R8>BTQ=Vn@F$ED%)M(OJ@v>!JkO5}F^-1TelIxHg2KLQE;mxm_VNg7=Zp z8E*^MeZfS&F_iH`cT}P>aNc8{ny(bfkAe3bK9^VB@1ijf<9{ewjYj`M!GcIOU)2TK zVw%dT_Z9@nYFe&(Bxc#Td?p9)2=f%pROV+SC)$qNv^^a;$GE7C_&x#J^F9)2r_~kX z7s-1)Brk4^uAE@t)~m7pBb5MeM@$W4(JQKj z;;PxzlXX@;3}fm&Up|7Kz+y=+DZ%C9E^%Fg=KJz1bd(zYVKSj2?!OypNYd&WePKsq z#@NoEV*LRsq{ykD%T*(g#Zl*4jbI-BH6{Vd1sC(qA5(>{Q&Yhwr!mNa$&Yr|Lvx$370pwh4p z_W5zo=?R><)0V>cdt?@!=A4CLxP3sl8aJmczPqgoaj{U5^y|2GD5B6et zoypbCUxB{n(VLqNPyA&u(oegGN7k)Za3`4i@4NQ%uu$k1A8yI_@47T<2R%*Mi3Ul( z`P|WC;=M>4C=*qo?7u}%5&LlK61dF5F-*{{Uc(9Fr2A#R>8M8kqHngt8Hi}cHfx7G zk0u}c)YD#ft?27ub~eF*Xb8Xi6vapY^_E!m82`TI@Jf0~U^|L*zD9|wqgRaR0$_`!I9)~G*GRGP5n zj7f9zfV!MlRJG7d#x&G^k~=?0>yQ^PANqVRsgXQxRkpU*&OnGPEcI2bqd#xJRvK$% zO31-4wXnRhAI`_w93a2(Yv5j77jQLKtZ?sE%%g$-%_ASo3h@_EQq!0fjXjR3R-@Zy;kBVJh zU;DDGyT-Br5(B%cQ3^0P-u%@->de4>{#9Z`)^B7j)sjzs;&EC}U_<5p7nZht18i9X zS(`gC4FOXXScN8jbkT7^xGS}!`A=#?xAbZJFSm~XgIl;9ONDyzaJrw{)JIK6$Mdbx z^>3Ytu?3-9D0U1%O{&*-56Jejk6(ys--P_Zc>EgH zhjNsPEkf$crNg^b<`y1I5fc)89-waE2l>NI(MvH&^Y%S_krLI^SEWC}`^;Gj6GC~0 zFSP{o5*A@SUQo9%o@B2DLo!4?OyzpA*f%SFx%0g}DR{LwK%M4OVRU3>YgSvHE}Q$J zm8KO~V0-!9nggq$`8K3tFY;#gfx`PHbsuy`!Rr$9FFRkuj5m&b5wtGU5=>hCK9CVV zw3aL@wP@?)Sl?4GSgNkblV(Qrp@D{ElSt;}V{T5^-4>zM&cDsI_=J?Z>CxQIyz&@% z5lsCa-ey*QvdFVX8R^D_dP~j8dud&nX{@p@m;MQp-lMake9PPUTmSL)N_E!b>6SHW zCEt(6fT}i90qxk$W_rdZwT>f$UFkns8R4h#K~oROfX)q3Zg}k{9Nd6sQ@5X%8J~HM zDirQPGd_~aMqd2cJ&=nenazEuisR>q?phkuwy}yV4}ik@Ob zf$N}U!(Pv$tM_lLy%|d=La(I~{EXuMpl#R^x5I9eHwNn{)yo7PUenG;1$ltcQiSTt zPwoY7A=X}KSG<)**6@>BbyJ?yHQBdq_%A6h__EZrO3^mUr{6)1*Nj07B|WNMHafmp zaK97%*=VW$dhQ~0c{QO@EOensHd!qssjV5L`&`Bs9`EHN!`etAN{9vDvzAka9A>&gh@$JB$~K@Bov9! z%N&V`)vV`KEe6jsxuTW>Q@^5D)SVm2szR7UZh_XotdR|4^?Gp9+XL~D@1!b-T@^~o zs4fn8v`UN`>-oMAU-{a`NEY#e^FNa4G)uTSQ=TiZKJj3{n9h`qHrXG7Su(A47v=-| z{AJHE6uz)~w7g2K-~B3uvlew2(e#NVVk6-tjiXv&+$7pbL)bO2pt7bPP zi0FkUV*~nri-=m^X!fr(erYh{L0^j`wpW|YnubOr;1q{a4)&caQoH^$TcBVR3{473CdO_^~O22lL#mTEbUE;pNAM#f_GQ z%r`szsl=N}O|Xa~<-(2lQ3}PtspJLaj{7h|rCFT3b3~|t#3J|1y+kiRL4=deJ7~^n zIQE;L&ce^I&@AmJzk7|$1C=kSV*l`GCKAn5Sut6?S|QN9yeH9}r0}c7TTl3jgn<%m ze?f3DS-e$lT;WdW0Hf#o&q;2Wsi?cZw1gB!XZxN91+`sx!U5I*Rb+P#s(l9Iw_*a- z^`hWS{26OPl=bCEFS(qV&TXx3*RAHjoDS9TAw>A~qAoGr8%ODZE|6zmA04p3^we41 zq4!yNDeXgcmbu@s%xR+U!D>IF1YC%%WgJ?xw>}eJr4_=du0L^baMUJgzjNJJGoqYj z-T0$%H_iqSafb#c7IFE6V0Bwk6by^=bfTN1q`{tlB`NHR8E=HP$C}y1G>{uh?jnuO zP@H6J^S$nA4#O$}(nB`k>k2)vIQ~3WX_ix)?qfaREiEbRW-yEMY?>?(J^qi`y2<-1 zRmz>NWwTgZ6@mQeCRCypkaho?SnHM?_1+Q|?aad)>Oc$sxH~{!2cli4>4^A07E^xZ literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@2x.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b5bd905bb08d38835ceca9e3e60bff86ccd8796c GIT binary patch literal 11927 zcmch71yfv2&@L?Qi@U?(u(*cc4nc#%;=$bm1b250E{nUnLjnX18nU#CUxQcFWcc$o2=A^bF9laOLvi%hL9~%nm4t(9OvHFHLxO|jWLK1t)b@ct%MVD? z-pfA-YVV>2`r7)*QbenxG2s6ANf{h%>G9>1?)+Ro>ytpXKB32~S^|jNKzs2}81{qI zRAMkM&dGWfTg{_slJwUf>h$Okkz$k|PJ*ai#Y93t4dgW^pp@=i{(*m0MXzyyX@UP& zKWKsB?W50>9n^WVF0k|0zuwmh6w4@|g@9JS^q!(mbBy9xbk z-xaN>!pn`h<7#oR_L*DPXzS!J7yhfmV0S&6Q?HY0Y{v-=h?^a(GK^q$`3L}V{V%Mq$nPrD6d3Cb0WrUH5i}N<4 zNM3?rBjjX0B6;y?*Z6FOK!I>2AedsRn&LQVT~5XPXNXzNKA2Dx$b{>g-}V~@*yy?c zB{p8vDqC@n$S9H5%r@BQYmPSz7+%Bz7M+M*XteR=A#$b^A}teeM?-?mYQ~C;XeTd+ z;&Uqe9NzOgmLqyBa1&*PH4K0=7V1yO;Be#K$>09;GI zCKesqh6`@;fj(y{ z+ueNjIs9NVlNXJnfsc?pv4i~k@Z`ka*3K@Z(x=^RgnJKX&%KkPz;p#L%!z05PH}#6 z8})WiCvns36|~2V`7S)jnC^UeK6-52%A+_4?W2;x3JcD3_Foq18(WAnXF%plj*9bP3ycP?hu~u(_lU| z3!2I`aXhMuu}!%igf9sp#%M(!h9lJ#dWU_hiEG@Av~t$}+V_|Ic_51a)}KPZIXa15-6WqNQ)nlS@4g-#F*-^7mlB^J!05I6Bnj_@1qCQ@gReXz&6M+r6nbh#Gx2>GdgDv zZ!W5rJrEw34*qUG4-B7-XlW>>U5g3!Gv`s>Ib@3LHQcyPDbP(Oddk}hd^`gz(PD9c zS3K(q4n#ccxdw5(43H~3^u@$XZy#Wq%$4M@$@vO>I7w~XgFo}NQVdrxT0I(t^)^~h zwq`a~03J$B94vWfu?F}_b!Te(DV0gB!ayBeintQnRwy|6>^0%Uqa$>)Gn6NJ!btXO zoHD}Hc&48|4_S0uJDI+yS6$+uIqfaQc53t9f!AI}x zIQpj{f>hgVUQhfF5J8p~o(qyU{ge8}bxx05J1PK&_Jf_FM~hFGpJ8e-5hGN}bfX?dhRdFMCtEt_{ez(sy`89xhN#kti{_mI!jcqKrQxkiuOBxT z*n$e)@0&xt-fhVuS52#E4#(LGaZ_p3A~QOvGH;OT(b;?r_}ip9O>Ha{6IZDkQGEWV zhD7=_&~Lnu+~a1z?1Nt0)+OfIXCJoPXt-v{W1iGSNVu1p_n+g~A{rfzL(Wl|eMRqx z&)K9DD9XD(`=ROC2>6>F@e^Cb-lJnmes`trHW1q0xl87yFJeoy@gFBK$OWmr=MwtY zn@n1ls4FwFa}UQHWoQ81R42tp#5h3<@_D7`%Mob*eF_MmKaoi|J#i8~YPoD2Ps{_0 zXoz%XSnbE7!)U2vYj9)?mq#f!E|U+IEeXF}mx)X>ETs=j7$j@TY`NP$dD2mf-z69b z-%n0d<>Ot0BEgck?4&YFvP#EO1JCBkW*$}T<>L{C zl+`^5Ojd=NW_(rE67`;d(1!*~pbndbObmpbRgqK<1ve;9!ZYk`Lb%ZqxTK<)g=Nejjz-bKn{!aD#Bu0gU4)IU@mN3=k8)r(xU z414u3j7V3naDERq(0N&98EE`BxuyDjsXeZ2QMIt0)|erpG22f`r+EMzXSJvW%|VJV zJEeE-VyZWIv%-*AP3#TD@FC=X`_IqM|M`3^t|~jPWRfwdV@93tfMKwgCULL{5lKX2 zlAq%LXbN9c{=b?SK6rld1n}A|GSAR)V=j%uxtt(^?68qD^11#+S_Lc5rt&<59P#Zm z3HPwaH)i-L=;+q0y6POPWejUO7%l`pTl2snn*H|O2q3;kQK3qJ>+GuJ{jqatnR{)29Molp6vFwnL ztk489W51&f+b?Q#THvob?bR|G8HB9n2Co=<7t2Nk!0Be7^m` zucbiM;1qZbveQv!VkEQ(*;do$FKru7+BFi0(XpkTV^u9dfDVt~CLW&{>}AKsy$VOn z!yZ>3^n9|_OF%$a5cJ|ZTgDA<5{nir3H^80{)RhKM!S#B>;_M=5XH?Fq zIm;8DKR;D%%7!@tnaL9_2oJ^p64%E$rx{W{{yi&E=Imt9{NynryZ3OvX4{Vp7y?b~ zxpknbqJn$p7sdudgOGWQa>dnDK-u!k zw^V3dv)sZ+JfqkbQoxzER}Tv;Sg8NXt7cSL6>cNmbHrlc-tF10_WIxX0f5Bz9Zy%& z_|dV5oGJtjZXTSw9S2g7dAfz4=?Ryp!P!cr<;4Je>PVJx_iPbek1cwm@^qX@`EeDe z%O?3HOQR48t{O^IyaFvBKY0)bbrdo?GAW-9Jn`X@uBoc7kt8f_#fbNYt9;EUFf4}T2vt&>hKNYpfEam1U#Cl-=1JT`mK6^B z3VYBKH!@oujisz9Gx@-p5w9`^mmpHo3#~Y%|IfF3xFO7D^SwSh>?DiRhaG13npy-iSa;l<3HFj8)7s@U6+)FsUC~3bGM)Z0SOdl1}K@HoT zRBw3&6>WzlKuG1V9WtfO#q1|D2}sgb0lg8QWeKEHCK`h|(7MiR?ALWl7+>87JeUbF zdsd5#zPN~_o(}YqKP6?a5le1=P-xduz?quBA{xNrs#V&nu8N_O$5?1#E7Qp&GE|06 zM!aBtd~4auy$UlwOJFDs=&gT%BD9haPiXe!O05MpuJTRe`I z&5v+NoXEa1lod5N2S@nTp?JS_5iZMtYtC0*2FcOL-P8bvSdKS152lrjIBJ^aEa9s} z|I5+70UK@HJ;|&`x|36g3SyTuvJ) z!e@`48r#0)10X0%FFhwCg|{XBw<*N2;W8{blIFByid4>&glgQC&zZ~50M;Q(Rz39K zOGEj4aTm6B_T6zGN}U!ymE=`HI}1Q+$;LiBS*3q4_YvJB29v6i@Jz^HcgrLMToX?2 z+><;HWDLCD7z)Rz8k{)&iEIH_kRKNmB;`wTD4b_G#w|Z7q;*#+;u&LUC;Tph$`(2pEEr}w>pR9?sFoUiOd*$*@oD0&WajwB13%=L!n8ZY`L$J;!YGcLDQEi zP>ELD^iC0B0?4XI?(w$$0ux9!kWZ*6c@8QJkbhr( zhaIGFLHUr>^zA&Q11=wfPYL2;Qrd`HEg6SG6#riD7$>D^RKveXq)k#0l9Ff!9uG_2 zIYg;2m01|e6F+sgiGR-x`@MZhwdUm_+Uxa6lv1Ri7Q^BqwmX77BsSJ6EcgI|>_|{~ z2jP(};E5{n+x9fJ7wF_acFT}3s>}I{VAfb^r9;C<{(enrJ#5YrKIn85=U8#>o;h0S z`&0|NA%%4^J@3;FkLBDNFuZ-s?_@fSdl9MJ{Z`Dusgx{P+vrKcQsDfTlt5@aYB$xa z1BUJ<{BH>&Y|c`nKB5+)vb??NOnGdg6-m^%t6MvaXcHHX+AQk4z9{R~0{&Ci5tD@I z^~!RtxlgVb7Q9wS6xhCILTiaYA@E?jj>F#cOm_#i0dO4FDvgC-okC%)Ph3(`uVj# z2vWmz2Mc!*3}C!lNBIy77RvZ>tWStZiU(+<@tP{C7&Y#hUgwF zD`f$f_qUO=bInpdAsbcUsr&QNNW55I6j`W*)tqYr3Is_@5l17lSn@o+S5IaS(VUe^ z$nxkqWD2Pom+pJgm4tnV*Lk#Niq&qRMs%JY9mV01$LYJXH@Iw_4qxZ9`*T_NO^ZVU zy-O=fdGCohv#aWI03~qM9zIA45GOj z{pKEAJSZ^@;(#_gytD(@doYKc+2H zMw3}-psh^K{liPDh)dRksN+Zr8=Q76<=*01Mpps}+!g&fDb@2IyG@^XS5eXLfnZg# zypc_pm?L!ah^f zTr6#amV>U5@#Q>c1x#aPYx73|^GyC~Vhuf2A-J>A-SNezuA^n;?83z^i!LmW7zF)xIhp|g@!+{ff6b+?leAyI^4F{d^q zF@w`bY^oJpl2prDB9J;ntp!PF`mwTOq^hYE4;kt*`}JORHs)a}i=}A1sQ3<@mNQlU zE6=X$Dx5wCI1bu=vLksXU2ZYWWjDezmqv1%5Gi4sQ=~;RML9<*f}}!}9^B#NLRp+K zJ}5HZIO7pir(x+BJ_!tFxh3B`?v%{HiVY#WRyRZ75O);s^*XE_umBJJe$?VbKh=eL zap57Ww*KmyYwntP|Bx$v>^c>k>$c^45FTi9u*vrpNb~|RiPJ~>yf(ihnK==aA0KtR zA)~6z0>oqZSw%zUTv@knzg}PWlE#7do!h(ay??(nm@q^s^Veq5nm(3r+~nM^5v%2p z{~UtT{`;;1=|Dh7&)NV2n_D#k-mYoT?_8LC0EErD6bgMHEq+*=>CQxT^$p>PQi|js zK_d3$klc)SFhj-9sl3eS3bV5C5X6O|#;J)2pZk`}(O1rF54zsJ>_%aVp}@@NLtH7C zv_dO#D5qMjJl&j5qArVIfJ}5)vF5EIN9o+a=;OW?0`0*6*Z=rP`gd$6qUzoL-Je!$C&)p5R2+Y3{j+PXt=GMb8?3>v1^vj(s@c9 zUxUGml2ONAEmB)Z$DHBUJ~qG#MqEa{JW{kLFUa+<$15F!A_KV#`b@`)F!0jK?Ah?y zF{!VwPt3}4cmP6VtNM6bEh2L9xpI}Xv2VGATZhFtWTYdaCMp)O_#PJQctePOVX@FF zRLoAYKx~pD6ipD(kv#-Q7dCSuN0&Lh$6^ma0AI*%d^i2!JIgaZxoJ>qJ*MQ!X91(@ zbeV6reeFL*d+wPOil`eHdDs7Wsk-#@Wlm6e!(eBdr(}km835z#+}`vdslmD-+f^sy zukyM;Ehn}fWLil!_-p(-0>8Y;mI}4OoZ|@j8ozYFFq`_#q$Y>Tr)r7G9DR^EFHUDd zvpB}`w=Ih$XG;}~mL~25|k9*_=0b zbThLVh$vnvG**-$bLJT%#gtFMr2gx1>#nDu?5bY3k0`t`lznN4IniDMs2BBZ-=K~& zdf1DYC3;B{dmgIzaboHonLS=e5EXDUfPe!`CA~_+O+!MaC-=)QZY9FBQ+7aF$DWfe zxz9vl!d^Ye(ukS@| zooK^U+qqrCEb-!|=dS65fExKbS9PN&TYWuBAYl+Sh6RzWFI>6`5qruTW+dP#U2$}V zq+b<|Mo5C0d;EsRhj)CVR}MC0xTA1BP~+o%Yp_y=O>SIVkcf2Mdel^D~%r*(1VdfUIhu?;kapPfsr-z~EIH6xs4LWfxcV#(NK z;d-r8oRL2oyn+V;i!|cp-TjWF#(wUZQJ7&(iZ;mF$A_g*TDQ-{J&7HwQHec~H>Ll; zADBidT31z^epZGEiRQlh{)}^utIo+H-jHldAo7o!(KEOGooGzqMYC| zq^Xjflevahj{-(QB;E8m-T`E1=0^85FkAup>msRHf26$j!Z9kg-m<8~V7~9>v)|cw z$CiLcuZuRN5|Fm%YN+wvPN;T-uM<9KWAHHo_nl7EF2)>hG6!^pBu6_iIIvSe z-L~E|N)(=N2lNO3yCh|lWN4c>75JO1!AhUzUqOel_j@~(`FAb+sS7+3i=e*7_oH=o zE9)}bANgD3`59{I$9_EE&r<&FG~m?JvI_Az%YsB=+keK{DqQ-er05 zBe8dLsa5SmNfP$aftG<N#8C#dTtD)2i0`^Qy+vj7O)Ho= zIUC}+NnMnOXE<^{x`r}_`!6Ev*s@pNdfFREZfU^!#aT5uzLc5gwrcuVN3LMQ3EKP~ zb=S%tz9Cx$d-R*+F+$vU0RzY(eEe*cp3wl;G}%}ptZ42Bn?H+LJ!h?81Au$pzx`+u zEN&(V(Sf(BN;0QLE{W%?eT)!>+}&fe==(QI2EPUPG^`B*yCeNVkoYV&4$sD3A~|J< zo0?PAv}$r_p(e4cM@xDOYOLU47x&9hx6)V9nW4E_mgw~>RQLhZ2sdS=F0R|I?*o4g zvK9e5{PLQvRNT@MxcNJOQ?VnuU8H|(YEkN7(98RR8jGLY!*@V0@aYf3o>5D#Z|jH8 zHH74^RZSN5^mi(>$Y9G8Ip~NN8=VWEk{$~D&q>>QB{)+v&tuOh3|6rQ+@yarAr)vJB}wjF1*bLd|xpa3D$bxIighR>Djx2WPi@G9;d3 z%(w1-ZBJYA&y16u{yQFSjcSdlmEL6xW!!6v=cAe!flb344yer zGXQB~r`=l6tpW1T7cgYVAg^pNc>WmSvf~(9-DBRzV1*c!X{J;-`Ac6*E48)iSR1f# z(RH00m64oEe$jF66zLWY@7DcTRc~+ns2W~)V6@)3kL0$Yf`*A4ON|dxM*}9VcDxei zs4^+dJXh&$s2fk&u1?W2Jr==zAo@MYp4T%zOPM~PF&fcq{avYryyvMCgapc*{@vB& zhGxhb%)&43#o(JjkH~EAv=md%QFe!OQDk%HTqHeyt7Sn zAaNId?snYTK5v=$4lr~IoI0FyUQd2ZhBzwRF`i4s8Qeid|3JyU=O7zzi|M0dsGv$> z%x|v^{QEK?;V=&f9uoEvuURF`mfUU<`}@o{0l)&%ksBHS)2JE2$A?W&uXL8@ue?p zOv&z*%vb*7%y%&gj?|jDJ3FR5poOLZYQFSsI-|bNzLrWK3z)FVS9-_dCkGDZ(N#(~ z&6?F9J){G5{_8n^PXu;;{ZH+v1jVnnHp=$?{`MW8my*3X?ZC(@GppJ6?M`6R$cd%= zp4g8oz1+;ly0KOHClmx$u#gYvGtzsSDEy?E{PR|i_{^`z-eol#N*MMdll2nU z*dF;Z%j>cWZ`49*85MDObzZu&0tf4%Wg{~zcGS_fc-+wR9j(HiN2DPb6k67WWMft* z$GDN$kv$5Ej6E?w*{))>P7YaE)EENDg!O!R+?+iv>dN1K!(Y)2scKbp z3{6qq>P403eSP8=7Pg}_%eY}Z4LSpjhB8LTuKBdCsi6VGMAJJsA2RZ%G4S`VHobpQ zKFDt_bbkF<_ekAxS;EnBrv4N|*g{`;{SRvUD{+&y#S-?p>ooB-FYw>LfW?Ko&h?w= zI{`$h*K6lZI+^{n!JwT-be=o5 z7huE$h*1nj%QDM+#>w$cd02;-vt*Um+TVh3O1D!Emw4_bcca;0wzxhnyV&x3Ab zt=%d!>iDm(K}-R)_cVUt53z_W0Ib16{^OU$L=I7!`~L z@cjdo-9drF&0bFwI2z9P0RhH`2maxIp6-{{ zkH&4Dke)w{1(AOigL0OT5$7Kt&bsYBZfk~W}CH%E@Lkz7gnL6RIZrGq@|bU+)l-S09cZO*m8*PZHL}fsuBD&?h}Wnsge@@qPQo&%W=P29iIPr|MNsEXO2Rs4L*Z-xd*w-{COQ? z_nifb0MWV?3$nC6!x`t1t%X`eFS!@;7=@)^EA`07kog}x@y1lugX1%@wP}w_;{=HH zzTX*qn=soThhb%A(EPsn?$XT%4kmUS>dX(Ouuhr4DUNt&$5*^}flmQ$>mTS2e!~4= z>rGo9I#5y5=Nujwz0Q??**v4r&8Vo8`y-6&ZO&-{HH9ZpM zUOm_ILT=!la!Fc|QxPbpr4QxCWUVji@n&R(%X#d6=4+66qTK7JakGCeh;8Ny))VS? z-jLkE`rI=1jnrcY*THUju{|%4lX+y z3Kx1|?EB-Ei%j|A@)ke%* zY>AitsASobnC3bZ5f)&^;xSIJ^%tKB{`war2Vh#|zv6HV;--N}h+(5K2vJTC6X{ij zWK7(W)x?0pMRW)1p<8EEMF7WzvhXs;{~dF@&AgjkA1o~=0A4ElJoVM+mX)|U9U-{$ z9u`YKo89&uz=*^S9{ee=nt&7$jkpW|-7#_yg-zoNvN1XOeyPeS^BZX6Z=wjT;(e+% z*#pUtGGH0pk`eoN-OMX%>C%mVpHd{3k6!yT@l)q}E@?1Y4%%=Y+=&71v_jXNK6>&w z`#QMTdJm2=c;J{&?HOfAzcfux6km>*bUKLxDKJ?=)>)gDX6t*<-{!9x1}f+(Z~5WD zNeogbcW=qQwzRfjvMoqxs~JXZak1tWt7HvgqJo z_%=w2W_*zrFiG^h!$M*D3oB}i91^7`dpNVV#2p~k-&iDnpY=RfjRN^SF%>|Z!nIb9 z9;~ye>L_}BFnhO=%;y^2ad3z#xj5v#seundlG{6|Y(8Hq_0P0&fbM^f8_1$sh4TQE zyGqLEWAJmznKe<3+{}9k*U@2>mjkUSbD_S?4B(FyRjPFc>ETvphkEj@1YB_N%66mM z{DYMIz3SrCKefN-W|PDnC1Z7sWS?|d2wSzWqUJZ|xoXo##q`k~sZB~6xuNP(*8B0J zq4=iq{I5H0B22wypn=lNl#-X=(`y7c(bxO)%M`HF^1iWCN(2%eqVP$Kaic`~6HrHR z+rv0Mjk4kFLEp7$Ut8C%HcAlHS;M)$;S?9b$4??vCWODcucp{gjKQz01T|56A_St1 zWjUGMRRM>6J9319Rp<8D^sFRowJ1vx%`HN^&=sNGMisAyH;hROVBs&%&#F6&ICg{d zZ#xvnZ!|FFN}>ug3EILBUX?du^KJT4qYswTTIzC&i*{GG-$|CIiWbApCMmcYQ*f{) zo=6+WI zAW8i`)?31Muhk4>6hmP%*MY=_*)qIQ<~oh)4DT~6Qw*It!-=>|#CExlhVCy&$K0+g z;VE`G5QrubaKt1qQ(bGK1}*K3LK{rbN5FCD>|jO7VR~-NRMIycZ7NDOS(pPvDoQ_N z%(8s+AeuvOyqHTp*L+Jgh`NJ3=J1FH4aS6#VCzX&ceEKvCtHo-hIe|5`Y)MUJyX+P z)?24pc)3Ik+;NiV_lDyEiZWiAHQofdLWZY#m}M-csYp$K6-Lj%$qvzHV^BKdKw(PF%-dAoQq=Nnoa zD#UZOr!TA#<*PEDy;%b!i`E@r@%RsKjCsE65If4uXZ?NbXz?AlZoqo5G^yqOx9rG? z;yq0j)`%!1Dv5H$kH|753^bb##+JD%PK3-ze(O zmgH0z-5iwp-PU&})DoqSNDvZovunQNjYkfrD61w@Cj|-lKXIL}h5!Hn literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@3x.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_empty_trip.imageset/ic_empty_trip@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..59bbddb7750f015e9c8a893dc90ab77387e81721 GIT binary patch literal 18486 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 literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/Contents.json b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/Contents.json new file mode 100644 index 0000000..08ca3e4 --- /dev/null +++ b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ic_server_error.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_server_error@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_server_error@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error.png new file mode 100644 index 0000000000000000000000000000000000000000..1be051a7e81cad8746f9cc4752cd1b3646b357e7 GIT binary patch literal 3627 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 literal 0 HcmV?d00001 diff --git a/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error@3x.png b/Projects/Modules/DSKit/Resources/Assets.xcassets/ic_server_error.imageset/ic_server_error@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..56ca071c504bed3efd6e5abb61cbd6f32927b7b9 GIT binary patch literal 10566 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 JN_oq${{her*82be literal 0 HcmV?d00001 From 6d87048f43cc1991e718c999f3ad8f420cee244a Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:06:55 +0900 Subject: [PATCH 02/47] =?UTF-8?q?chore:=20#18=20-=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Target+Extension.swift | 5 +++-- Projects/App/Project.swift | 3 +-- .../Features/BaseFeatureDependency/Project.swift | 1 - .../Sources/Views/Cells/PlaceCell.swift | 1 - .../FollowFeature/Sources/Views/MediaInfoView.swift | 1 - Projects/Features/HomeFeature/Project.swift | 4 ++-- Projects/Features/TabBarFeature/Project.swift | 3 ++- .../Sources/Views/UpcomingTripCell.swift | 1 - .../Sources/Component/BottomPlacedButton.swift | 1 - .../Component/BottomSheetViewController.swift | 1 - Projects/Modules/Networks/Project.swift | 3 +-- Projects/Modules/ThirdPartyLibs/Project.swift | 1 - Tuist/Package.swift | 13 ++++++++++++- 13 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift index 19f7d80..e472031 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift @@ -48,7 +48,8 @@ public extension Target { dependencies: [TargetDependency], scripts: [TargetScript], isStatic: Bool = false, - hasResources: Bool = true + hasResources: Bool = true, + settings: Settings? = nil ) -> Target { return .target( name: name, @@ -61,7 +62,7 @@ public extension Target { resources: hasResources ? ["Resources/**"] : nil, scripts: scripts, dependencies: dependencies, - settings: .frameworkSettings + settings: settings ?? .frameworkSettings ) } } diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 582ffcb..eb201ca 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -21,8 +21,7 @@ let project = Project.makeModule( scripts: [.swiftLint], dependencies: [ .data, - .Modules.networks, - .Features.rootFeature, + .Features.rootFeature ], settings: .appSettings() ) diff --git a/Projects/Features/BaseFeatureDependency/Project.swift b/Projects/Features/BaseFeatureDependency/Project.swift index a0b7d45..584bfc9 100644 --- a/Projects/Features/BaseFeatureDependency/Project.swift +++ b/Projects/Features/BaseFeatureDependency/Project.swift @@ -15,7 +15,6 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "BaseFeatureDependency", dependencies: [ - .core, .domain, .Modules.dsKit ], diff --git a/Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift b/Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift index 526a4d0..2f32516 100644 --- a/Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift +++ b/Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift @@ -9,7 +9,6 @@ import Core import Domain import DSKit -import Kingfisher import SnapKit import Then import UIKit diff --git a/Projects/Features/FollowFeature/Sources/Views/MediaInfoView.swift b/Projects/Features/FollowFeature/Sources/Views/MediaInfoView.swift index a0d1032..e88e556 100644 --- a/Projects/Features/FollowFeature/Sources/Views/MediaInfoView.swift +++ b/Projects/Features/FollowFeature/Sources/Views/MediaInfoView.swift @@ -9,7 +9,6 @@ import Core import Domain import DSKit -import Kingfisher import SnapKit import Then import UIKit diff --git a/Projects/Features/HomeFeature/Project.swift b/Projects/Features/HomeFeature/Project.swift index 7bd4377..a5cc848 100644 --- a/Projects/Features/HomeFeature/Project.swift +++ b/Projects/Features/HomeFeature/Project.swift @@ -16,8 +16,8 @@ let project = Project.makeModule( name: "HomeFeature", dependencies: [ .Features.baseFeatureDependency, - .Features.Follow.feature, - .data + .Features.Search.feature, + .Features.Setting.feature ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/TabBarFeature/Project.swift b/Projects/Features/TabBarFeature/Project.swift index 7f7d9af..7b002e3 100644 --- a/Projects/Features/TabBarFeature/Project.swift +++ b/Projects/Features/TabBarFeature/Project.swift @@ -16,7 +16,8 @@ let project = Project.makeModule( name: "TabBarFeature", dependencies: [ .Features.Home.feature, - .Features.Travel.feature + .Features.Travel.feature, + .Features.Search.feature ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/TravelFeature/Sources/Views/UpcomingTripCell.swift b/Projects/Features/TravelFeature/Sources/Views/UpcomingTripCell.swift index 272d553..58785d5 100644 --- a/Projects/Features/TravelFeature/Sources/Views/UpcomingTripCell.swift +++ b/Projects/Features/TravelFeature/Sources/Views/UpcomingTripCell.swift @@ -7,7 +7,6 @@ // import DSKit -import Kingfisher import SnapKit import Then import UIKit diff --git a/Projects/Modules/DSKit/Sources/Component/BottomPlacedButton.swift b/Projects/Modules/DSKit/Sources/Component/BottomPlacedButton.swift index b10b5f1..85270e5 100644 --- a/Projects/Modules/DSKit/Sources/Component/BottomPlacedButton.swift +++ b/Projects/Modules/DSKit/Sources/Component/BottomPlacedButton.swift @@ -7,7 +7,6 @@ // import Core -import SnapKit import UIKit public final class BottomPlacedButton: UIButton { diff --git a/Projects/Modules/DSKit/Sources/Component/BottomSheetViewController.swift b/Projects/Modules/DSKit/Sources/Component/BottomSheetViewController.swift index 9aa566b..4f45026 100644 --- a/Projects/Modules/DSKit/Sources/Component/BottomSheetViewController.swift +++ b/Projects/Modules/DSKit/Sources/Component/BottomSheetViewController.swift @@ -6,7 +6,6 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import SnapKit import UIKit // MARK: - BottomSheetConfiguration diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift index aab2aec..0d7fe6a 100644 --- a/Projects/Modules/Networks/Project.swift +++ b/Projects/Modules/Networks/Project.swift @@ -16,8 +16,7 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "Networks", dependencies: [ - .core, - .domain + .core ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Modules/ThirdPartyLibs/Project.swift b/Projects/Modules/ThirdPartyLibs/Project.swift index fcd9a4f..62b1d2a 100644 --- a/Projects/Modules/ThirdPartyLibs/Project.swift +++ b/Projects/Modules/ThirdPartyLibs/Project.swift @@ -18,7 +18,6 @@ let project = Project.makeModule( .SPM.Kingfisher, .SPM.Moya, .SPM.RIBs, - .SPM.RxSwift, .SPM.RxCocoa, .SPM.SnapKit, .SPM.Then diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 69ab405..44ac7d5 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -8,7 +8,18 @@ import PackageDescription // Customize the product types for specific package product // Default is .staticFramework // productTypes: ["Alamofire": .framework,] - productTypes: [:] + productTypes: [ + "RxSwift": .framework, + "RxCocoa": .framework, + "RxRelay": .framework, + "RxCocoaRuntime": .framework, + "Moya": .framework, + "Alamofire": .framework, + "SnapKit": .framework, + "Then": .framework, + "Kingfisher": .framework, + "RIBs": .framework + ] ) #endif From f3f34fabac032fd4eb4e17fe08d72a6a4f4cb145 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:27:33 +0900 Subject: [PATCH 03/47] =?UTF-8?q?feat:=20#18=20-=20=EA=B5=AD=EA=B0=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/Foundation+/String+.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Projects/Core/Sources/Extensions/Foundation+/String+.swift diff --git a/Projects/Core/Sources/Extensions/Foundation+/String+.swift b/Projects/Core/Sources/Extensions/Foundation+/String+.swift new file mode 100644 index 0000000..72ac364 --- /dev/null +++ b/Projects/Core/Sources/Extensions/Foundation+/String+.swift @@ -0,0 +1,34 @@ +// +// String+.swift +// Core +// +// Created by 최안용 on 1/31/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public extension String { + func toFlag() -> String { + guard self.count == 2 else { return "🏳️" } + + let base: UInt32 = 127397 + var flagString = "" + + for uni in self.uppercased().unicodeScalars { + if let scalar = UnicodeScalar(base + uni.value) { + flagString.append(String(scalar)) + } else { + return "🏳️" + } + } + return flagString + } + + func toKoreanCountryName() -> String { + guard self.count == 2 else { return "알 수 없음"} + + let locale = Locale(identifier: "ko_KR") + return locale.localizedString(forRegionCode: self) ?? "알 수 없음" + } +} From ffe834f909df25e9f028b539dfb72e21b6235d38 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:28:02 +0900 Subject: [PATCH 04/47] =?UTF-8?q?feat:=20#18=20-=20reusableViewIdentifier?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UIKit+/UICollectionReusableView+.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Projects/Core/Sources/Extensions/UIKit+/UICollectionReusableView+.swift diff --git a/Projects/Core/Sources/Extensions/UIKit+/UICollectionReusableView+.swift b/Projects/Core/Sources/Extensions/UIKit+/UICollectionReusableView+.swift new file mode 100644 index 0000000..db26157 --- /dev/null +++ b/Projects/Core/Sources/Extensions/UIKit+/UICollectionReusableView+.swift @@ -0,0 +1,15 @@ +// +// UICollectionReusableView+.swift +// Core +// +// Created by 최안용 on 2/3/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +public extension UICollectionReusableView { + static var reusableViewIdentifier : String { + return String(describing: self) + } +} From cb4270a5b6dbe81e661b6ae6b52e1bffd8a7aa4e Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:28:16 +0900 Subject: [PATCH 05/47] =?UTF-8?q?feat:=20#18=20-=20cellIdentifier=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/UIKit+/UICollectionViewCell+.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Projects/Core/Sources/Extensions/UIKit+/UICollectionViewCell+.swift diff --git a/Projects/Core/Sources/Extensions/UIKit+/UICollectionViewCell+.swift b/Projects/Core/Sources/Extensions/UIKit+/UICollectionViewCell+.swift new file mode 100644 index 0000000..db86ea8 --- /dev/null +++ b/Projects/Core/Sources/Extensions/UIKit+/UICollectionViewCell+.swift @@ -0,0 +1,15 @@ +// +// UICollectionViewCell+.swift +// Core +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +public extension UICollectionViewCell { + static var cellIdentifier : String { + return String(describing: self) + } +} From 6484a03a2b8ace63ad4cba7ddfeda15385785880 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:29:46 +0900 Subject: [PATCH 06/47] =?UTF-8?q?feat:=20#18=20-=20Home=EA=B3=BC=ED=98=84?= =?UTF-8?q?=20Network=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/Home/ProgramResponse.swift | 15 ++++ .../Sources/DTO/Home/TripResponse.swift | 26 +++++++ .../Sources/Service/HomeService.swift | 45 ++++++++++++ .../Networks/Sources/TargetType/HomeAPI.swift | 73 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 Projects/Modules/Networks/Sources/DTO/Home/ProgramResponse.swift create mode 100644 Projects/Modules/Networks/Sources/DTO/Home/TripResponse.swift create mode 100644 Projects/Modules/Networks/Sources/Service/HomeService.swift create mode 100644 Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift diff --git a/Projects/Modules/Networks/Sources/DTO/Home/ProgramResponse.swift b/Projects/Modules/Networks/Sources/DTO/Home/ProgramResponse.swift new file mode 100644 index 0000000..a0d8818 --- /dev/null +++ b/Projects/Modules/Networks/Sources/DTO/Home/ProgramResponse.swift @@ -0,0 +1,15 @@ +// +// ProgramResponse.swift +// Networks +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct ProgramResponse: Decodable { + public let id: Int + public let name: String + public let type: String +} diff --git a/Projects/Modules/Networks/Sources/DTO/Home/TripResponse.swift b/Projects/Modules/Networks/Sources/DTO/Home/TripResponse.swift new file mode 100644 index 0000000..e5535df --- /dev/null +++ b/Projects/Modules/Networks/Sources/DTO/Home/TripResponse.swift @@ -0,0 +1,26 @@ +// +// TripResponse.swift +// Networks +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct TripResponse: Decodable { + public let content: [TripContentResponse] + public let hasNext: Bool +} + +public struct TripContentResponse: Decodable { + public let travelId: String + public let title: String + public let thumbnail: String? + public let programName: String + public let traveler: String + public let country: String + public let city: String + public let nights: Int + public let days: Int +} diff --git a/Projects/Modules/Networks/Sources/Service/HomeService.swift b/Projects/Modules/Networks/Sources/Service/HomeService.swift new file mode 100644 index 0000000..6a886f0 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Service/HomeService.swift @@ -0,0 +1,45 @@ +// +// HomeService.swift +// Networks +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Moya + +// MARK: - API 나오기 전 임시 +public protocol HomeServiceProtocol { + //임시 + func getMyTrip() async throws -> Int + + func getCategoryList() async throws -> [ProgramResponse] + func getPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> TripResponse + func getRecommendTripList(page: Int?, size: Int?) async throws -> TripResponse +} + +public final class HomeService: HomeServiceProtocol { + private let provider: MoyaProvider + + public init(provider: MoyaProvider = MoyaProvider()) { + self.provider = provider + } + + public func getMyTrip() async throws -> Int { + try await provider.asyncThowsRequest(.getMyTrip) + } + + public func getCategoryList() async throws -> [ProgramResponse] { + try await provider.asyncThowsRequest(.getCategoryList) + } + + public func getPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> TripResponse { + try await provider.asyncThowsRequest(.getPopularTripList(id: id, page: page, size: size)) + } + + public func getRecommendTripList(page: Int?, size: Int?) async throws -> TripResponse { + try await provider.asyncThowsRequest(.getRecommendTripList(page: page, size: size)) + } +} diff --git a/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift b/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift new file mode 100644 index 0000000..33b37cf --- /dev/null +++ b/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift @@ -0,0 +1,73 @@ +// +// HomeAPI.swift +// Networks +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Moya + +// MARK: - API 나오기 전 임시 +public enum HomeAPI { + case getMyTrip + case getCategoryList + case getPopularTripList(id: Int?, page: Int?, size: Int?) + case getRecommendTripList(page: Int?, size: Int?) +} + +extension HomeAPI: TargetType { + public var baseURL: URL { + NetworkConfiguration.baseURL + } + + public var path: String { + switch self { + case .getMyTrip: + return "" + case .getCategoryList: + return "/api/v1/travel-programs" + case .getPopularTripList: + return "/api/v1/travel-templates/popular" + case .getRecommendTripList: + return "/api/v1/travel-templates/recommend" + } + } + + public var method: Moya.Method { + switch self { + case .getMyTrip, .getCategoryList, .getPopularTripList, .getRecommendTripList: + return .get + } + } + + public var task: Moya.Task { + switch self { + case .getMyTrip, .getCategoryList: + return .requestPlain + case .getPopularTripList(let id, let page, let size): + var params: [String: Any] = [:] + + if let id { params["travelProgramId"] = id } + if let page { params["page"] = page } + if let size { params["size"] = size } + + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case .getRecommendTripList(let page, let size): + var params: [String: Any] = [:] + + if let page { params["page"] = page } + if let size { params["size"] = size } + + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + } + } + + public var headers: [String: String]? { + ["Content-Type": "application/json"] + } +} + From 90fbc9003759ff000fc6e432bf1cc21de2d3dfa4 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:34:12 +0900 Subject: [PATCH 07/47] =?UTF-8?q?del:=20#18=20-=20=EB=A0=88=EA=B1=B0?= =?UTF-8?q?=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Mock/MockHomeService.swift | 135 ---------------- .../Sources/Views/Cells/CategoryCell.swift | 100 ------------ .../Views/Cells/RecommendContentCell.swift | 141 ---------------- .../Views/Cells/YoutuberContentCell.swift | 132 --------------- .../CategoryCollectionView.swift | 99 ------------ .../RecommendContentCollectionView.swift | 82 ---------- .../YoutuberContentCollectionView.swift | 150 ------------------ .../Sources/Views/MyTravelView.swift | 61 ------- .../Networks/Sources/Base/BaseResponse.swift | 2 +- .../Foundation/NetworkConfiguration.swift | 2 - 10 files changed, 1 insertion(+), 903 deletions(-) delete mode 100644 Projects/Features/HomeFeature/Sources/Mock/MockHomeService.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/CategoryCell.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/RecommendContentCell.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/YoutuberContentCell.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionViews/CategoryCollectionView.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionViews/RecommendContentCollectionView.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionViews/YoutuberContentCollectionView.swift delete mode 100644 Projects/Features/HomeFeature/Sources/Views/MyTravelView.swift diff --git a/Projects/Features/HomeFeature/Sources/Mock/MockHomeService.swift b/Projects/Features/HomeFeature/Sources/Mock/MockHomeService.swift deleted file mode 100644 index 7da54b2..0000000 --- a/Projects/Features/HomeFeature/Sources/Mock/MockHomeService.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// MockHomeService.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-21. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import Foundation - -final class MockHomeService: HomeServiceProtocol { - - private let mockImageURLs = [ - "https://picsum.photos/400/300?random=1", - "https://picsum.photos/400/300?random=2", - "https://picsum.photos/400/300?random=3", - "https://picsum.photos/400/300?random=4", - "https://picsum.photos/400/300?random=5", - "https://picsum.photos/400/300?random=6", - "https://picsum.photos/400/300?random=7", - "https://picsum.photos/400/300?random=8", - "https://picsum.photos/400/300?random=9", - "https://picsum.photos/400/300?random=10", - "https://picsum.photos/400/300?random=11", - "https://picsum.photos/400/300?random=12", - "https://picsum.photos/400/300?random=13", - "https://picsum.photos/400/300?random=14", - "https://picsum.photos/400/300?random=15" - ] - - func fetchMyTrips() async -> Result<[MyTrip], HomeError> { - try? await Task.sleep(nanoseconds: 300_000_000) - - return .success([ - MyTrip( - id: 1, - title: "도쿄 여행", - destination: "일본 도쿄", - startDate: Date(), - endDate: Calendar.current.date(byAdding: .day, value: 5, to: Date()) ?? Date(), - thumbnailURL: mockImageURLs[0] - ), - MyTrip( - id: 2, - title: "파리 여행", - destination: "프랑스 파리", - startDate: Calendar.current.date(byAdding: .day, value: 30, to: Date()) ?? Date(), - endDate: Calendar.current.date(byAdding: .day, value: 37, to: Date()) ?? Date(), - thumbnailURL: mockImageURLs[1] - ) - ]) - } - - func fetchPopularTrips(category: TripCategory) async -> Result<[PopularTrip], HomeError> { - let allTripsResult = await fetchAllPopularTrips() - guard case .success(let allTrips) = allTripsResult else { - return .success([]) - } - - if category == .all { - return .success(allTrips.values.flatMap { $0 }) - } else { - return .success(allTrips[category] ?? []) - } - } - - func fetchAllPopularTrips() async -> Result<[TripCategory: [PopularTrip]], HomeError> { - try? await Task.sleep(nanoseconds: 300_000_000) - - return .success([ - .all: [ - PopularTrip(id: 100, title: "곽준빈의 신혼여행", authorName: "곽튜브", destination: "파리", duration: "2박3일", thumbnailURL: mockImageURLs[0], category: .all), - PopularTrip(id: 101, title: "6박7일 스위스 여행", authorName: "찰스엔터", destination: "스위스", duration: "5박6일", thumbnailURL: mockImageURLs[1], category: .all), - PopularTrip(id: 102, title: "충격적인 북유럽 물가", authorName: "곽튜브", destination: "북유럽", duration: "6박7일", thumbnailURL: mockImageURLs[2], category: .all) - ], - .vietnam: [ - PopularTrip(id: 1, title: "다낭 힐링 여행", authorName: "빠니보틀", destination: "다낭", duration: "3박4일", thumbnailURL: mockImageURLs[3], category: .vietnam), - PopularTrip(id: 2, title: "호치민 먹방 투어", authorName: "먹방작가", destination: "호치민", duration: "4박5일", thumbnailURL: mockImageURLs[4], category: .vietnam), - PopularTrip(id: 3, title: "하노이 역사 탐방", authorName: "역사탐방가", destination: "하노이", duration: "3박4일", thumbnailURL: mockImageURLs[5], category: .vietnam) - ], - .europe: [ - PopularTrip(id: 4, title: "파리 로맨틱 여행", authorName: "곽튜브", destination: "파리", duration: "5박6일", thumbnailURL: mockImageURLs[6], category: .europe), - PopularTrip(id: 5, title: "스위스 알프스 투어", authorName: "콩콩팡팡", destination: "스위스", duration: "6박7일", thumbnailURL: mockImageURLs[7], category: .europe), - PopularTrip(id: 6, title: "이탈리아 미식 여행", authorName: "신서유기", destination: "이탈리아", duration: "7박8일", thumbnailURL: mockImageURLs[8], category: .europe) - ], - .hongkong: [ - PopularTrip(id: 7, title: "홍콩 야경 투어", authorName: "여행작가", destination: "홍콩", duration: "2박3일", thumbnailURL: mockImageURLs[9], category: .hongkong), - PopularTrip(id: 8, title: "마카오 카지노 여행", authorName: "럭셔리트래블", destination: "마카오", duration: "2박3일", thumbnailURL: mockImageURLs[10], category: .hongkong), - PopularTrip(id: 9, title: "홍콩 맛집 탐방", authorName: "맛집헌터", destination: "홍콩", duration: "3박4일", thumbnailURL: mockImageURLs[11], category: .hongkong) - ], - .singapore: [ - PopularTrip(id: 10, title: "싱가포르 가족여행", authorName: "가족여행전문", destination: "싱가포르", duration: "4박5일", thumbnailURL: mockImageURLs[12], category: .singapore), - PopularTrip(id: 11, title: "마리나베이 야경", authorName: "야경전문가", destination: "싱가포르", duration: "3박4일", thumbnailURL: mockImageURLs[13], category: .singapore), - PopularTrip(id: 12, title: "센토사 리조트 힐링", authorName: "힐링여행", destination: "싱가포르", duration: "4박5일", thumbnailURL: mockImageURLs[14], category: .singapore) - ], - .japan: [ - PopularTrip(id: 13, title: "오사카 맛집 투어", authorName: "일본통", destination: "오사카", duration: "3박4일", thumbnailURL: mockImageURLs[0], category: .japan), - PopularTrip(id: 14, title: "도쿄 쇼핑 여행", authorName: "쇼핑퀸", destination: "도쿄", duration: "4박5일", thumbnailURL: mockImageURLs[1], category: .japan), - PopularTrip(id: 15, title: "교토 전통 문화", authorName: "문화탐방", destination: "교토", duration: "3박4일", thumbnailURL: mockImageURLs[2], category: .japan) - ] - ]) - } - - func fetchRecommendations() async -> Result<[Recommendation], HomeError> { - try? await Task.sleep(nanoseconds: 300_000_000) - - return .success([ - Recommendation( - id: 1, - title: "인플루언서 A의 발리 여행기", - authorName: "인플루언서 A", - destination: "인도네시아 발리", - duration: "7박 8일", - thumbnailURL: mockImageURLs[0] - ), - Recommendation( - id: 2, - title: "작가 B의 유럽 배낭여행", - authorName: "여행작가 B", - destination: "유럽 5개국", - duration: "14박 15일", - thumbnailURL: mockImageURLs[1] - ), - Recommendation( - id: 3, - title: "셰프 C의 태국 미식 여행", - authorName: "셰프 C", - destination: "태국 방콕", - duration: "4박 5일", - thumbnailURL: mockImageURLs[2] - ) - ]) - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/CategoryCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/CategoryCell.swift deleted file mode 100644 index 6e09893..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/Cells/CategoryCell.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// CategoryCell.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Core -import DSKit -import UIKit -import SnapKit -import Then - -final class CategoryCell: UICollectionViewCell { - - static let identifier = "CategoryCell" - - // MARK: - UI Components - - private let containerView = UIView().then { - $0.layer.borderWidth = 1 - $0.clipsToBounds = true - } - - private let iconImageView = UIImageView().then { - $0.contentMode = .scaleAspectFit - $0.image = UIImage(systemName: "play.rectangle.fill") - } - - private let titleLabel = UILabel() - - private let stackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - $0.alignment = .center - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Layout - - override func layoutSubviews() { - super.layoutSubviews() - contentView.layoutIfNeeded() - containerView.layer.cornerRadius = containerView.bounds.height / 2 - } - - // MARK: - Setup - - private func setupUI() { - contentView.addSubview(containerView) - containerView.addSubview(stackView) - [iconImageView, titleLabel].forEach { - stackView.addArrangedSubview($0) - } - } - - private func setupConstraints() { - containerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - stackView.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(14) - $0.verticalEdges.equalToSuperview().inset(6) - } - - iconImageView.snp.makeConstraints { - $0.size.equalTo(20).priority(.high) - } - } - - // MARK: - Configuration - - func configure(title: String, isSelected: Bool, isFirstItem: Bool) { - titleLabel.setText(.bodyMSB, text: title, color: isSelected ? UIColor(hexCode: "#FFFFFF") : UIColor(hexCode: "#757575")) - iconImageView.isHidden = isFirstItem - - if isSelected { - containerView.backgroundColor = UIColor(hexCode: "#2C2C2C") - containerView.layer.borderColor = UIColor.clear.cgColor - iconImageView.tintColor = .white - } else { - containerView.backgroundColor = UIColor(hexCode: "#FFFFFF") - containerView.layer.borderColor = UIColor(hexCode: "#D9D9D9").cgColor - iconImageView.tintColor = UIColor(hexCode: "#2C2C2C") - } - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendContentCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendContentCell.swift deleted file mode 100644 index f09209b..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendContentCell.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// RecommendContentCell.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import UIKit -import DSKit -import Kingfisher -import SnapKit -import Then - -final class RecommendContentCell: UICollectionViewCell { - - static let identifier = "RecommendContentCell" - - // MARK: - Properties - - private var currentThumbnailURL: String? - - // MARK: - UI Components - - private let thumbnailImageView = UIImageView().then { - $0.contentMode = .scaleAspectFill - $0.backgroundColor = .systemGray5 - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - } - - private let titleLabel = UILabel().then { - $0.numberOfLines = 2 - } - - private let authorInfoView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - $0.alignment = .center - } - - private let playIcon = UIImageView().then { - $0.image = UIImage(systemName: "play.rectangle.fill") - $0.tintColor = UIColor(hexCode: "#444444") - $0.contentMode = .scaleAspectFit - } - - private let authorLabel = UILabel() - - private let durationLabel = UILabel() - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - currentThumbnailURL = nil - thumbnailImageView.kf.cancelDownloadTask() - thumbnailImageView.image = nil - thumbnailImageView.backgroundColor = .systemGray5 - } - - // MARK: - Setup - - private func setupUI() { - contentView.addSubview(thumbnailImageView) - contentView.addSubview(titleLabel) - contentView.addSubview(authorInfoView) - - [playIcon, authorLabel, durationLabel].forEach { - authorInfoView.addArrangedSubview($0) - } - } - - private func setupConstraints() { - thumbnailImageView.snp.makeConstraints { - $0.top.leading.trailing.equalToSuperview() - $0.height.equalTo(180) - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(thumbnailImageView.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview() - } - - authorInfoView.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(6) - $0.leading.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } - - playIcon.snp.makeConstraints { - $0.size.equalTo(14) - } - } - - // MARK: - Configuration - - func configure(with recommendation: Recommendation) { - titleLabel.setText(.bodyMSB, text: recommendation.title, color: UIColor(hexCode: "#2C2C2C")) - authorLabel.setText(.bodySR, text: recommendation.authorName, color: UIColor(hexCode: "#2C2C2C")) - durationLabel.setText(.bodySR, text: " · \(recommendation.duration)", color: UIColor(hexCode: "#2C2C2C")) - - // URL 저장 및 이미지 로딩 (교차 검증) - let thumbnailURL = recommendation.thumbnailURL - currentThumbnailURL = thumbnailURL - - if let urlString = thumbnailURL, let url = URL(string: urlString) { - thumbnailImageView.kf.setImage( - with: url, - placeholder: nil, - options: [ - .transition(.fade(0.2)), - .cacheOriginalImage - ] - ) { [weak self] result in - guard let self = self else { return } - guard self.currentThumbnailURL == urlString else { return } - - switch result { - case .success: - break - case .failure: - self.thumbnailImageView.backgroundColor = .systemGray5 - } - } - } else { - thumbnailImageView.backgroundColor = .systemGray5 - } - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/YoutuberContentCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/YoutuberContentCell.swift deleted file mode 100644 index ec38db7..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/Cells/YoutuberContentCell.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// YoutuberContentCell.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import UIKit -import DSKit -import Kingfisher -import SnapKit -import Then - -final class YoutuberContentCell: UICollectionViewCell { - - static let identifier = "YoutuberContentCell" - - // MARK: - Properties - - private var currentThumbnailURL: String? - - // MARK: - UI Components - - private let thumbnailImageView = UIImageView().then { - $0.contentMode = .scaleAspectFill - $0.backgroundColor = .systemGray5 - $0.layer.cornerRadius = 8 - $0.clipsToBounds = true - } - - private let titleLabel = UILabel().then { - $0.numberOfLines = 1 - } - - private let infoLabel = UILabel().then { - $0.numberOfLines = 1 - } - - private let textStackView = UIStackView().then { - $0.axis = .vertical - $0.spacing = 4 - $0.alignment = .leading - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - currentThumbnailURL = nil - thumbnailImageView.kf.cancelDownloadTask() - thumbnailImageView.image = nil - thumbnailImageView.backgroundColor = .systemGray5 - } - - // MARK: - Setup - - private func setupUI() { - [thumbnailImageView, textStackView].forEach { - contentView.addSubview($0) - } - [titleLabel, infoLabel].forEach { - textStackView.addArrangedSubview($0) - } - } - - private func setupConstraints() { - thumbnailImageView.snp.makeConstraints { - $0.leading.top.bottom.equalToSuperview() - $0.width.equalTo(136) - } - - textStackView.snp.makeConstraints { - $0.leading.equalTo(thumbnailImageView.snp.trailing).offset(12) - $0.trailing.equalToSuperview() - $0.centerY.equalToSuperview() - } - } - - // MARK: - Configuration - - func configure(with trip: PopularTrip) { - // placeholder trip인 경우 숨김 처리 - guard trip.id >= 0 else { - contentView.isHidden = true - return - } - contentView.isHidden = false - - titleLabel.setText(.bodyMSB, text: trip.title, color: UIColor(hexCode: "#2C2C2C")) - infoLabel.setText(.bodySM, text: "\(trip.authorName) · \(trip.destination) · \(trip.duration)", color: UIColor(hexCode: "#2C2C2C")) - - // URL 저장 및 이미지 로딩 (교차 검증) - let thumbnailURL = trip.thumbnailURL - currentThumbnailURL = thumbnailURL - - if let urlString = thumbnailURL, let url = URL(string: urlString) { - thumbnailImageView.kf.setImage( - with: url, - placeholder: nil, - options: [ - .transition(.fade(0.2)), - .cacheOriginalImage - ] - ) { [weak self] result in - guard let self = self else { return } - // 교차 검증: 현재 URL이 요청한 URL과 같은지 확인 - guard self.currentThumbnailURL == urlString else { return } - - switch result { - case .success: - break - case .failure: - self.thumbnailImageView.backgroundColor = .systemGray5 - } - } - } else { - thumbnailImageView.backgroundColor = .systemGray5 - } - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/CategoryCollectionView.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionViews/CategoryCollectionView.swift deleted file mode 100644 index f7e7283..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/CategoryCollectionView.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// CategoryCollectionView.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import UIKit - -protocol CategoryCollectionViewDelegate: AnyObject { - func categoryCollectionView(_ collectionView: CategoryCollectionView, didSelectCategoryAt index: Int) -} - -final class CategoryCollectionView: UICollectionView { - - // MARK: - Models - - private struct CategoryItem: Hashable { - let index: Int - let title: String - let isSelected: Bool - let isFirstItem: Bool - } - - // MARK: - Properties - - weak var categoryDelegate: CategoryCollectionViewDelegate? - - private var diffableDataSource: UICollectionViewDiffableDataSource? - - // MARK: - Initialization - - init() { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = 8 - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - - super.init(frame: .zero, collectionViewLayout: layout) - setupCollectionView() - setupDataSource() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func setupCollectionView() { - backgroundColor = .clear - showsHorizontalScrollIndicator = false - delegate = self - register(CategoryCell.self, forCellWithReuseIdentifier: CategoryCell.identifier) - } - - private func setupDataSource() { - diffableDataSource = UICollectionViewDiffableDataSource( - collectionView: self - ) { collectionView, indexPath, item in - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: CategoryCell.identifier, - for: indexPath - ) as? CategoryCell else { - return UICollectionViewCell() - } - cell.configure(title: item.title, isSelected: item.isSelected, isFirstItem: item.isFirstItem) - return cell - } - } - - // MARK: - Public Methods - - func applySnapshot(categories: [String], selectedIndex: Int) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([0]) - - let items = categories.enumerated().map { index, title in - CategoryItem( - index: index, - title: title, - isSelected: index == selectedIndex, - isFirstItem: index == 0 - ) - } - snapshot.appendItems(items) - - diffableDataSource?.apply(snapshot, animatingDifferences: false) - } -} - -// MARK: - UICollectionViewDelegate - -extension CategoryCollectionView: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - categoryDelegate?.categoryCollectionView(self, didSelectCategoryAt: indexPath.item) - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/RecommendContentCollectionView.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionViews/RecommendContentCollectionView.swift deleted file mode 100644 index ec29737..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/RecommendContentCollectionView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// RecommendContentCollectionView.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import UIKit - -protocol RecommendContentCollectionViewDelegate: AnyObject { - func recommendContentCollectionView(_ collectionView: RecommendContentCollectionView, didSelectItemAt index: Int) -} - -final class RecommendContentCollectionView: UICollectionView { - - // MARK: - Properties - - weak var contentDelegate: RecommendContentCollectionViewDelegate? - - private var diffableDataSource: UICollectionViewDiffableDataSource? - - // MARK: - Initialization - - init() { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = 12 - layout.itemSize = CGSize(width: 200, height: 260) - - super.init(frame: .zero, collectionViewLayout: layout) - setupCollectionView() - setupDataSource() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func setupCollectionView() { - backgroundColor = .clear - showsHorizontalScrollIndicator = false - contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 24) - delegate = self - register(RecommendContentCell.self, forCellWithReuseIdentifier: RecommendContentCell.identifier) - } - - private func setupDataSource() { - diffableDataSource = UICollectionViewDiffableDataSource( - collectionView: self - ) { collectionView, indexPath, recommendation in - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: RecommendContentCell.identifier, - for: indexPath - ) as? RecommendContentCell else { - return UICollectionViewCell() - } - cell.configure(with: recommendation) - return cell - } - } - - // MARK: - Public Methods - - func applySnapshot(recommendations: [Recommendation]) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([0]) - snapshot.appendItems(recommendations) - diffableDataSource?.apply(snapshot, animatingDifferences: false) - } -} - -// MARK: - UICollectionViewDelegate - -extension RecommendContentCollectionView: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - contentDelegate?.recommendContentCollectionView(self, didSelectItemAt: indexPath.item) - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/YoutuberContentCollectionView.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionViews/YoutuberContentCollectionView.swift deleted file mode 100644 index 0ee929e..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/CollectionViews/YoutuberContentCollectionView.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// YoutuberContentCollectionView.swift -// HomeFeature -// -// Created by kimnahun on 2026-01-22. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import UIKit - -protocol YoutuberContentCollectionViewDelegate: AnyObject { - func youtuberContentCollectionView(_ collectionView: YoutuberContentCollectionView, didSelectItemAt index: Int, in section: Int) - func youtuberContentCollectionView(_ collectionView: YoutuberContentCollectionView, didScrollToSection section: Int) -} - -final class YoutuberContentCollectionView: UICollectionView { - - // MARK: - Properties - - weak var contentDelegate: YoutuberContentCollectionViewDelegate? - - private let maxItemCountPerSection = 3 - private let peekWidth: CGFloat = 10 - private var diffableDataSource: UICollectionViewDiffableDataSource? - private var categories: [TripCategory] = [] - - // MARK: - Initialization - - init() { - super.init(frame: .zero, collectionViewLayout: UICollectionViewLayout()) - collectionViewLayout = createLayout() - setupCollectionView() - setupDataSource() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Layout - - private func createLayout() -> UICollectionViewCompositionalLayout { - let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, environment in - guard let self = self else { return nil } - - let containerWidth = environment.container.contentSize.width - let sectionWidth = containerWidth - self.peekWidth - - // Item - 한 셀의 크기 - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .absolute(88) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - // Group - 세로로 3개씩 묶음 - let groupSize = NSCollectionLayoutSize( - widthDimension: .absolute(sectionWidth), - heightDimension: .absolute(88 * 3 + 12 * 2) - ) - let group = NSCollectionLayoutGroup.vertical( - layoutSize: groupSize, - subitems: [item] - ) - group.interItemSpacing = .fixed(12) - - let section = NSCollectionLayoutSection(group: group) - section.orthogonalScrollingBehavior = .groupPaging - - return section - } - - return layout - } - - // MARK: - Setup - - private func setupCollectionView() { - backgroundColor = .clear - showsHorizontalScrollIndicator = false - isScrollEnabled = false - delegate = self - register(YoutuberContentCell.self, forCellWithReuseIdentifier: YoutuberContentCell.identifier) - } - - private func setupDataSource() { - diffableDataSource = UICollectionViewDiffableDataSource( - collectionView: self - ) { collectionView, indexPath, trip in - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: YoutuberContentCell.identifier, - for: indexPath - ) as? YoutuberContentCell else { - return UICollectionViewCell() - } - cell.configure(with: trip) - return cell - } - } - - // MARK: - Public Methods - - func applySnapshot(tripsByCategory: [TripCategory: [PopularTrip]], categories: [TripCategory]) { - self.categories = categories - - var snapshot = NSDiffableDataSourceSnapshot() - - for category in categories { - snapshot.appendSections([category]) - let trips = tripsByCategory[category] ?? [] - let limitedTrips = Array(trips.prefix(maxItemCountPerSection)) - snapshot.appendItems(limitedTrips, toSection: category) - } - - diffableDataSource?.apply(snapshot, animatingDifferences: false) - } - - func scrollToCategory(at index: Int, animated: Bool = true) { - guard index < categories.count else { return } - - let indexPath = IndexPath(item: 0, section: index) - scrollToItem(at: indexPath, at: .left, animated: animated) - } -} - -// MARK: - UICollectionViewDelegate - -extension YoutuberContentCollectionView: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - contentDelegate?.youtuberContentCollectionView(self, didSelectItemAt: indexPath.item, in: indexPath.section) - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - updateCurrentSection() - } - - func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - updateCurrentSection() - } - - private func updateCurrentSection() { - let visibleRect = CGRect(origin: contentOffset, size: bounds.size) - let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY) - - if let indexPath = indexPathForItem(at: visiblePoint) { - contentDelegate?.youtuberContentCollectionView(self, didScrollToSection: indexPath.section) - } - } -} diff --git a/Projects/Features/HomeFeature/Sources/Views/MyTravelView.swift b/Projects/Features/HomeFeature/Sources/Views/MyTravelView.swift deleted file mode 100644 index e9e56c9..0000000 --- a/Projects/Features/HomeFeature/Sources/Views/MyTravelView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// MyTravelView.swift -// HomeFeature -// -// Created by kimnahun on 1/22/26. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Core -import DSKit -import UIKit -import SnapKit -import Then - -final class MyTravelView: UIView { - - private let messageLabel = UILabel().then { - $0.setText(.bodyLSB, text: "아직 등록된 여행지가 없어요", color: UIColor(hexCode: "#2C2C2C")) - } - private let subMessageLabel = UILabel().then { - $0.setText(.bodyMM, text: "새 여행 일정을 만들어 보세요!", color: UIColor(hexCode: "#2C2C2C")) - } - - private let imageView = UIImageView(image: DSKitAsset.Assets.icAirplane1.image) - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupConstraints() - } - - required init?(coder: NSCoder) { fatalError() } - - private func setupUI() { - backgroundColor = UIColor(hexCode: "#2C2C2C") - layer.cornerRadius = 4 - layer.borderWidth = 1.0 - layer.borderColor = UIColor.init(hexCode: "#F1F1F1").cgColor - - addSubview(messageLabel) - addSubview(subMessageLabel) - addSubview(imageView) - - } - - private func setupConstraints() { - messageLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(17.5) - $0.leading.equalToSuperview().offset(16) - } - subMessageLabel.snp.makeConstraints { - $0.top.equalTo(messageLabel.snp.bottom).offset(6) - $0.leading.equalTo(messageLabel) - } - imageView.snp.makeConstraints { - $0.verticalEdges.equalToSuperview().inset(8.19) - $0.trailing.equalToSuperview().offset(-16) - $0.width.equalTo(63.63) - } - } -} diff --git a/Projects/Modules/Networks/Sources/Base/BaseResponse.swift b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift index 0f398d6..4495fb5 100644 --- a/Projects/Modules/Networks/Sources/Base/BaseResponse.swift +++ b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift @@ -7,7 +7,7 @@ // import Foundation - + struct BaseResponse: Decodable, Sendable { let code: String let message: String diff --git a/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift index 5b7d197..acc88b7 100644 --- a/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift +++ b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift @@ -20,5 +20,3 @@ public enum NetworkConfiguration { return url } } - - From 24b21fe7b8d31d51a60b9b849868972b6cec95c2 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:34:52 +0900 Subject: [PATCH 08/47] =?UTF-8?q?fix:=20#18=20-=20=EB=A7=90=20=EC=A4=84?= =?UTF-8?q?=EC=9E=84=ED=91=9C=20=EB=AF=B8=EC=A0=81=EC=9A=A9=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/DSKit/Sources/Extensions/UIKit+/UILabel+.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Modules/DSKit/Sources/Extensions/UIKit+/UILabel+.swift b/Projects/Modules/DSKit/Sources/Extensions/UIKit+/UILabel+.swift index f679bc1..65007a1 100644 --- a/Projects/Modules/DSKit/Sources/Extensions/UIKit+/UILabel+.swift +++ b/Projects/Modules/DSKit/Sources/Extensions/UIKit+/UILabel+.swift @@ -36,7 +36,7 @@ public extension UILabel { if let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle { paragraphStyle.alignment = alignment - paragraphStyle.lineBreakMode = self.lineBreakMode + paragraphStyle.lineBreakMode = .byTruncatingTail attributes[.paragraphStyle] = paragraphStyle } From 1cb99e72c68090dbed98ea8625b798d2552d3511 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:35:11 +0900 Subject: [PATCH 09/47] =?UTF-8?q?feat:=20#18=20-=20NetworkError=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/Networks/Sources/Base/NetworkError.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Projects/Modules/Networks/Sources/Base/NetworkError.swift b/Projects/Modules/Networks/Sources/Base/NetworkError.swift index f1c1bb3..757f07d 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkError.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkError.swift @@ -11,6 +11,7 @@ import Foundation public enum NetworkError: Error, Sendable { case connectionFailed case decodingFailed + case noData case unknown(String) public var message: String { @@ -19,6 +20,8 @@ public enum NetworkError: Error, Sendable { return "네트워크 연결을 확인해주세요" case .decodingFailed: return "데이터 처리 중 오류가 발생했습니다" + case .noData: + return "응답 데이터가 없습니다." case .unknown(let description): return "알 수 없는 오류: \(description)" } From cdc04ffd46847d3531d99a5865a60551bdc868b7 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:35:35 +0900 Subject: [PATCH 10/47] =?UTF-8?q?design:=20#18=20-=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=97=90=EB=9F=AC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Component/NDGLErrorView.swift | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 Projects/Modules/DSKit/Sources/Component/NDGLErrorView.swift diff --git a/Projects/Modules/DSKit/Sources/Component/NDGLErrorView.swift b/Projects/Modules/DSKit/Sources/Component/NDGLErrorView.swift new file mode 100644 index 0000000..9213467 --- /dev/null +++ b/Projects/Modules/DSKit/Sources/Component/NDGLErrorView.swift @@ -0,0 +1,97 @@ +// +// NDGLErrorView.swift +// DSKit +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import RxSwift + +public final class NDGLErrorView: UIView { + private let imageView = UIImageView() + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + private let button = NDGLBtn(title: "다시 시도", style: .primary, size: .large) + private let titleStackView = UIStackView() + private let containerStackView = UIStackView() + + public var buttonDidTap: Observable { + button.rx.tap.asObservable() + } + + override public init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension NDGLErrorView { + func setStyle() { + imageView.do { + $0.image = DSKitAsset.Assets.icServerError.image + $0.contentMode = .scaleAspectFit + } + + titleLabel.do { + $0.setText( + .titleMSB, + text: "정보를 불러올 수 없어요", + color: DSKitAsset.Colors.black700.color, + alignment: .center + ) + } + + subtitleLabel.do { + $0.setText( + .bodyLM, + text: "인터넷 연결 확인 후 다시 시도해 주세요", + color: DSKitAsset.Colors.black500.color, + alignment: .center + ) + } + + titleStackView.do { + $0.axis = .vertical + $0.spacing = 10.adjustedH + $0.alignment = .center + } + + containerStackView.do { + $0.axis = .vertical + $0.spacing = 16.adjustedH + $0.alignment = .center + } + } + + func setUI() { + titleStackView.addArrangedSubviews(titleLabel, subtitleLabel) + containerStackView.addArrangedSubviews(imageView, titleStackView) + addSubviews(containerStackView, button) + } + + func setLayout() { + imageView.snp.makeConstraints { + $0.size.equalTo(140.adjustedH) + } + + containerStackView.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.centerY.equalTo(button.snp.top).dividedBy(2) + } + + button.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(24.adjusted) + $0.bottom.equalToSuperview().inset(16.adjustedH) + } + } +} From 4a19bc2566c6a99a9e0f87d3bd5508dc9247954c Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:36:07 +0900 Subject: [PATCH 11/47] =?UTF-8?q?design:=20#18=20-=20CategoryChipCell=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Component/CategoryChipCell.swift | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Projects/Modules/DSKit/Sources/Component/CategoryChipCell.swift diff --git a/Projects/Modules/DSKit/Sources/Component/CategoryChipCell.swift b/Projects/Modules/DSKit/Sources/Component/CategoryChipCell.swift new file mode 100644 index 0000000..b135142 --- /dev/null +++ b/Projects/Modules/DSKit/Sources/Component/CategoryChipCell.swift @@ -0,0 +1,128 @@ +// +// CategoryChipCell.swift +// DSKit +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +public final class CategoryChipCell: UICollectionViewCell { + public static let defaultHeight = 30.adjustedH + + private let iconView = UIImageView() + private let titleLabel = UILabel() + private let stackView = UIStackView() + + override public init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func prepareForReuse() { + super.prepareForReuse() + + iconView.image = nil + titleLabel.text = nil + } + + public func configure(_ icon: ChipIconType, _ title: String, _ isSelected: Bool = false) { + let contentColor = isSelected ? DSKitAsset.Colors.white.color : DSKitAsset.Colors.black400.color + iconView.do { + $0.image = icon.image + $0.tintColor = contentColor + $0.isHidden = icon == .none + } + + titleLabel.setText( + .bodyMM, + text: title, + color: contentColor, + alignment: .center + ) + + contentView.layer.borderWidth = isSelected ? 0.0 : 1.0 + contentView.backgroundColor = isSelected ? DSKitAsset.Colors.black900.color : DSKitAsset.Colors.white.color + + stackView.snp.updateConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(icon.horizontalPadding) + } + } +} + +private extension CategoryChipCell { + func setStyle() { + contentView.do { + $0.layer.cornerRadius = 15.adjustedH + $0.layer.borderWidth = 1.0 + $0.layer.borderColor = DSKitAsset.Colors.black200.color.cgColor + $0.clipsToBounds = true + } + + iconView.do { + $0.contentMode = .scaleAspectFit + } + + titleLabel.do { + $0.numberOfLines = 1 + } + + stackView.do { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + $0.isUserInteractionEnabled = false + } + } + + func setUI() { + stackView.addArrangedSubviews(iconView, titleLabel) + contentView.addSubview(stackView) + } + + func setLayout() { + stackView.snp.makeConstraints { + $0.height.equalTo(20.adjustedH) + $0.centerY.equalToSuperview() + $0.directionalHorizontalEdges.equalToSuperview().inset(14.adjusted) + } + + iconView.snp.makeConstraints { + $0.size.equalTo(20.adjustedH) + } + } +} + +public enum ChipIconType { + case none + case youtube + case tv + + fileprivate var image: UIImage? { + switch self { + case .none: + return nil + case .youtube: + return DSKitAsset.Assets.icVideo1.image.withRenderingMode(.alwaysTemplate) + case .tv: + return DSKitAsset.Assets.icTv1.image.withRenderingMode(.alwaysTemplate) + } + } + + fileprivate var horizontalPadding: CGFloat { + switch self { + case .none: + return 23.5 + case .youtube, .tv: + return 14.adjusted + } + } +} From 1b0f08a37f5d7da6a990c49db46f7e5f8b473a04 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:37:50 +0900 Subject: [PATCH 12/47] =?UTF-8?q?design:=20#18=20-=20PopularInfoCell=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Component/PopularInfoCell.swift | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift diff --git a/Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift b/Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift new file mode 100644 index 0000000..131ba9c --- /dev/null +++ b/Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift @@ -0,0 +1,138 @@ +// +// PopularInfoCell.swift +// DSKit +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import Kingfisher + +public final class PopularInfoCell: UICollectionViewCell { + public static let defaultHeight = 88.adjustedH + + private let thumbnailView = UIImageView() + private let nationalFlagLabel = UILabel() + private let nationLabel = UILabel() + private let nationStackView = UIStackView() + private let titleLabel = UILabel() + private let cityLabel = UILabel() + private let dotLabel = UILabel() + private let scheduleLabel = UILabel() + private let infoStackView = UIStackView() + private let textContainerStackView = UIStackView() + + override public init(frame: CGRect) { + super.init(frame: .zero) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func prepareForReuse() { + super.prepareForReuse() + + thumbnailView.kf.cancelDownloadTask() + thumbnailView.image = nil + nationalFlagLabel.text = nil + cityLabel.text = nil + titleLabel.text = nil + nationLabel.text = nil + scheduleLabel.text = nil + } + + public func configure( + thumbnailUrl: String, + city: String, + title: String, + nation: String, + schedule: String + ) { + if let url = URL(string: thumbnailUrl) { + thumbnailView.kf.setImage(with: url, options: [.transition(.fade(0.3))]) + } + + nationalFlagLabel.text = nation.toFlag() + cityLabel.setText(.bodyMM, text: city, color: DSKitAsset.Colors.black400.color) + titleLabel.setText(.bodyLM, text: title, color: DSKitAsset.Colors.black800.color) + nationLabel.setText(.bodyMM, text: nation.toKoreanCountryName(), color: DSKitAsset.Colors.black400.color) + scheduleLabel.setText(.bodyMM, text: schedule, color: DSKitAsset.Colors.black400.color) + } +} + +private extension PopularInfoCell { + func setStyle() { + thumbnailView.do { + $0.layer.cornerRadius = 6 + $0.clipsToBounds = true + $0.contentMode = .scaleAspectFill + $0.backgroundColor = .systemGray6 + } + + nationalFlagLabel.do { + $0.font = .systemFont(ofSize: 13.5 * max(1.adjusted, 1.adjustedH)) + } + + nationStackView.do { + $0.axis = .horizontal + $0.spacing = 4.adjusted + } + + titleLabel.do { + $0.numberOfLines = 2 + } + + dotLabel.do { + $0.setText(.bodyMM, text: "•", color: DSKitAsset.Colors.black400.color) + } + + infoStackView.do { + $0.axis = .vertical + $0.spacing = 4 + $0.alignment = .leading + } + + textContainerStackView.do { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + } + + + } + + func setUI() { + nationStackView.addArrangedSubviews(nationalFlagLabel, nationLabel) + textContainerStackView.addArrangedSubviews(cityLabel, dotLabel, scheduleLabel) + infoStackView.addArrangedSubviews(titleLabel, textContainerStackView) + contentView.addSubviews(thumbnailView, nationStackView, infoStackView) + } + + func setLayout() { + thumbnailView.snp.makeConstraints { + $0.width.equalTo(140.adjusted) + $0.height.equalTo(thumbnailView.snp.width).multipliedBy(88.0 / 140.0) + $0.leading.top.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + + nationStackView.snp.makeConstraints { + $0.leading.equalTo(thumbnailView.snp.trailing).offset(12.adjusted) + $0.top.equalToSuperview() + } + + infoStackView.snp.makeConstraints { + $0.leading.equalTo(thumbnailView.snp.trailing).offset(12.adjusted) + $0.top.equalTo(nationalFlagLabel.snp.bottom).offset(10.adjustedH) + $0.trailing.lessThanOrEqualToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + } +} From 97cf0aa5ecbeff2301dcb8c72c730c3644f57fcf Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:38:44 +0900 Subject: [PATCH 13/47] =?UTF-8?q?feat:=20#18=20-=20asyncThowsRequest=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/MoyaProvider+Async.swift | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift index 5599e7a..51a7ef8 100644 --- a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -105,7 +105,47 @@ extension MoyaProvider { } } } - + + func asyncThowsRequest(_ target: Target) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + request(target) { result in + NetworkLogger.logRequest(target) + + switch result { + case .success(let response): + NetworkLogger.logResponse(response) + if (200...299).contains(response.statusCode) { + do { + let decodedData = try response.map(BaseResponse.self) + + if let data = decodedData.data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: NetworkError.noData) + } + } catch { + continuation.resume(throwing: NetworkError.decodingFailed) + } + } else { + do { + let errorResponse = try response.map(ErrorResponse.self) + continuation.resume( + throwing: NetworkError.unknown( + errorResponse.message ?? "알 수 없는 오류가 발생했습니다." + ) + ) + } catch { + continuation.resume(throwing: NetworkError.unknown("알 수 없는 오류가 발생했습니다.")) + } + } + case .failure(let error): + NetworkLogger.logError(error) + continuation.resume(throwing: NetworkError.unknown(error.localizedDescription)) + } + } + } + } + private static func mapMoyaError(_ error: MoyaError) -> NetworkError { switch error { case .underlying(let nsError as NSError, _) From 777b41e4778056c65ba808482ab57b05f2591c81 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:39:10 +0900 Subject: [PATCH 14/47] =?UTF-8?q?design:=20#18=20-=20TabItem=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TabBarFeature/Sources/Components/NDGLTabItem.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Projects/Features/TabBarFeature/Sources/Components/NDGLTabItem.swift b/Projects/Features/TabBarFeature/Sources/Components/NDGLTabItem.swift index 091cb23..92cb9c9 100644 --- a/Projects/Features/TabBarFeature/Sources/Components/NDGLTabItem.swift +++ b/Projects/Features/TabBarFeature/Sources/Components/NDGLTabItem.swift @@ -8,6 +8,8 @@ import UIKit +import DSKit + final class NDGLTabItem: UIControl { private let containerStackView = UIStackView() private let iconView = UIImageView() @@ -34,7 +36,7 @@ final class NDGLTabItem: UIControl { func setup(title: String, image: UIImage) { iconView.image = image - titleLabel.setText(.bodyLM, text: title, color: UIColor(hexCode: "#2C2C2C")) + titleLabel.setText(.bodyLM, text: title, color: DSKitAsset.Colors.white.color) updateState(animation: false) } } @@ -88,8 +90,8 @@ private extension NDGLTabItem { self.titleLabel.alpha = self.isTabSelected ? 1 : 0 self.iconView.tintColor = self.isTabSelected - ? UIColor(hexCode: "#2C2C2C") - : UIColor(hexCode: "#2C2C2C") + ? DSKitAsset.Colors.white.color + : DSKitAsset.Colors.black600.color self.containerStackView.layoutIfNeeded() From 1a0e32e4e90e33d6ea650ffdac346875547285b4 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:43:37 +0900 Subject: [PATCH 15/47] =?UTF-8?q?feat:=20#18=20-=20Home=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20Domain=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/HomeRepositoryInterface.swift | 17 +++++ .../Interface/Home/HomeServiceProtocol.swift | 30 ++++---- .../Domain/Sources/Model/Home/HomeError.swift | 10 +-- .../Sources/Model/Home/MyTripSummary.swift | 51 +++++++++++++ .../Sources/Model/Home/TripCategory.swift | 21 ++++++ .../Domain/Sources/Model/Home/TripInfo.swift | 41 ++++++++++ .../Domain/Sources/Model/Home/VideoType.swift | 26 +++++++ .../Sources/Model/Travel/PopularTrip.swift | 74 +++++++++---------- .../Sources/Model/Travel/Recommendation.swift | 48 ++++++------ .../Domain/Sources/UseCase/HomeUsecase.swift | 60 +++++++++++++++ 10 files changed, 297 insertions(+), 81 deletions(-) create mode 100644 Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift create mode 100644 Projects/Domain/Sources/Model/Home/MyTripSummary.swift create mode 100644 Projects/Domain/Sources/Model/Home/TripCategory.swift create mode 100644 Projects/Domain/Sources/Model/Home/TripInfo.swift create mode 100644 Projects/Domain/Sources/Model/Home/VideoType.swift create mode 100644 Projects/Domain/Sources/UseCase/HomeUsecase.swift diff --git a/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift b/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift new file mode 100644 index 0000000..9d7c377 --- /dev/null +++ b/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift @@ -0,0 +1,17 @@ +// +// HomeRepositoryInterface.swift +// Domain +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +// MARK: - API 나오기 전 임시 +public protocol HomeRepositoryInterface { + func fetchMyTripInfo() async throws -> MyTripSummary? + func fetchCategoryList() async throws -> [TripCategory] + func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] + func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] +} diff --git a/Projects/Domain/Sources/Interface/Home/HomeServiceProtocol.swift b/Projects/Domain/Sources/Interface/Home/HomeServiceProtocol.swift index 75c59a5..981b935 100644 --- a/Projects/Domain/Sources/Interface/Home/HomeServiceProtocol.swift +++ b/Projects/Domain/Sources/Interface/Home/HomeServiceProtocol.swift @@ -6,18 +6,18 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Foundation - -public protocol HomeServiceProtocol: Sendable { - /// 내가 등록한 여행지 목록 조회 - func fetchMyTrips() async -> Result<[MyTrip], HomeError> - - /// 인기 여행 따라가기 목록 조회 (단일 카테고리) - func fetchPopularTrips(category: TripCategory) async -> Result<[PopularTrip], HomeError> - - /// 인기 여행 따라가기 전체 조회 (모든 카테고리별로 그룹화) - func fetchAllPopularTrips() async -> Result<[TripCategory: [PopularTrip]], HomeError> - - /// 추천 따라하기 콘텐츠 목록 조회 - func fetchRecommendations() async -> Result<[Recommendation], HomeError> -} +//import Foundation +// +//public protocol HomeServiceProtocol: Sendable { +// /// 내가 등록한 여행지 목록 조회 +// func fetchMyTrips() async -> Result<[MyTrip], HomeError> +// +// /// 인기 여행 따라가기 목록 조회 (단일 카테고리) +// func fetchPopularTrips(category: TripCategory) async -> Result<[PopularTrip], HomeError> +// +// /// 인기 여행 따라가기 전체 조회 (모든 카테고리별로 그룹화) +// func fetchAllPopularTrips() async -> Result<[TripCategory: [PopularTrip]], HomeError> +// +// /// 추천 따라하기 콘텐츠 목록 조회 +// func fetchRecommendations() async -> Result<[Recommendation], HomeError> +//} diff --git a/Projects/Domain/Sources/Model/Home/HomeError.swift b/Projects/Domain/Sources/Model/Home/HomeError.swift index a7443df..bb5ecda 100644 --- a/Projects/Domain/Sources/Model/Home/HomeError.swift +++ b/Projects/Domain/Sources/Model/Home/HomeError.swift @@ -8,8 +8,8 @@ import Foundation -public enum HomeError: Error, Sendable { - case networkError(message: String) - case serverError(message: String) - case unknown(code: String, message: String) -} +//public enum HomeError: Error, Sendable { +// case networkError(message: String) +// case serverError(message: String) +// case unknown(code: String, message: String) +//} diff --git a/Projects/Domain/Sources/Model/Home/MyTripSummary.swift b/Projects/Domain/Sources/Model/Home/MyTripSummary.swift new file mode 100644 index 0000000..bc5c14b --- /dev/null +++ b/Projects/Domain/Sources/Model/Home/MyTripSummary.swift @@ -0,0 +1,51 @@ +// +// MyTripSummary.swift +// Domain +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +// MARK: - API 나오기 전 임시 +public struct MyTripSummary { + public let title: String + public let startDay: Date + public let endDay: Date + public let tripSchedule: [Schedule] + + public init(title: String, startDay: Date, endDay: Date, tripSchedule: [Schedule]) { + self.title = title + self.startDay = startDay + self.endDay = endDay + self.tripSchedule = tripSchedule + } +} + +// MARK: - API 나오기 전 임시 +public struct Schedule { + // 시작 시간이 있어야 홈 상단에 현재 갈 장소 카드로 보여줄 수 있음 + public let id: Int + public let day: Int + public let placeName: String + public let thumbnailUrl: String + public let transport: String + public let estimatedDuration: Int + + public init( + id: Int, + day: Int, + placeName: String, + thumbnailUrl: String, + transport: String, + estimatedDuration: Int + ) { + self.id = id + self.day = day + self.placeName = placeName + self.thumbnailUrl = thumbnailUrl + self.transport = transport + self.estimatedDuration = estimatedDuration + } +} diff --git a/Projects/Domain/Sources/Model/Home/TripCategory.swift b/Projects/Domain/Sources/Model/Home/TripCategory.swift new file mode 100644 index 0000000..8e4f56f --- /dev/null +++ b/Projects/Domain/Sources/Model/Home/TripCategory.swift @@ -0,0 +1,21 @@ +// +// TripCategory.swift +// Domain +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct TripCategory { + public let id: Int + public let creator: String + public let viedoType: VideoType + + public init(id: Int, creator: String, viedoType: VideoType) { + self.id = id + self.creator = creator + self.viedoType = viedoType + } +} diff --git a/Projects/Domain/Sources/Model/Home/TripInfo.swift b/Projects/Domain/Sources/Model/Home/TripInfo.swift new file mode 100644 index 0000000..3e22ec0 --- /dev/null +++ b/Projects/Domain/Sources/Model/Home/TripInfo.swift @@ -0,0 +1,41 @@ +// +// TripInfo.swift +// Domain +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct TripInfo { + public let id: String + public let title: String + public let thumbnailUrl: String + public let creator: String + public let country: String + public let city: String + public let nights: Int + public let days: Int + + public init( + id: String, + title: String, + thumbnailUrl: String, + creator: String, + country: String, + city: String, + nights: Int, + days: Int, + countryCode: String + ) { + self.id = id + self.title = title + self.thumbnailUrl = thumbnailUrl + self.creator = creator + self.country = country + self.city = city + self.nights = nights + self.days = days + } +} diff --git a/Projects/Domain/Sources/Model/Home/VideoType.swift b/Projects/Domain/Sources/Model/Home/VideoType.swift new file mode 100644 index 0000000..428fcdb --- /dev/null +++ b/Projects/Domain/Sources/Model/Home/VideoType.swift @@ -0,0 +1,26 @@ +// +// VideoType.swift +// Domain +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public enum VideoType: String { + case youtube = "YOUTUBE" + case tv = "TV" + case none + + public init(rawValue: String) { + switch rawValue { + case "YOUTUBE": + self = .youtube + case "TV": + self = .tv + default: + self = .none + } + } +} diff --git a/Projects/Domain/Sources/Model/Travel/PopularTrip.swift b/Projects/Domain/Sources/Model/Travel/PopularTrip.swift index b8f370f..5406d1f 100644 --- a/Projects/Domain/Sources/Model/Travel/PopularTrip.swift +++ b/Projects/Domain/Sources/Model/Travel/PopularTrip.swift @@ -8,40 +8,40 @@ import Foundation -/// 인기 여행 따라가기 -public struct PopularTrip: Hashable { - public let id: Int - public let title: String - public let authorName: String - public let destination: String - public let duration: String - public let thumbnailURL: String? - public let category: TripCategory - - public init( - id: Int, - title: String, - authorName: String, - destination: String, - duration: String, - thumbnailURL: String?, - category: TripCategory - ) { - self.id = id - self.title = title - self.authorName = authorName - self.destination = destination - self.duration = duration - self.thumbnailURL = thumbnailURL - self.category = category - } -} - -public enum TripCategory: String, CaseIterable, Hashable { - case all = "전체" - case vietnam = "베트남" - case europe = "유럽" - case hongkong = "홍콩/마카오" - case singapore = "싱가포르" - case japan = "일본" -} +///// 인기 여행 따라가기 +//public struct PopularTrip: Hashable { +// public let id: Int +// public let title: String +// public let authorName: String +// public let destination: String +// public let duration: String +// public let thumbnailURL: String? +// public let category: TripCategory +// +// public init( +// id: Int, +// title: String, +// authorName: String, +// destination: String, +// duration: String, +// thumbnailURL: String?, +// category: TripCategory +// ) { +// self.id = id +// self.title = title +// self.authorName = authorName +// self.destination = destination +// self.duration = duration +// self.thumbnailURL = thumbnailURL +// self.category = category +// } +//} +// +//public enum TripCategory: String, CaseIterable, Hashable { +// case all = "전체" +// case vietnam = "베트남" +// case europe = "유럽" +// case hongkong = "홍콩/마카오" +// case singapore = "싱가포르" +// case japan = "일본" +//} diff --git a/Projects/Domain/Sources/Model/Travel/Recommendation.swift b/Projects/Domain/Sources/Model/Travel/Recommendation.swift index 2307ecc..54565c5 100644 --- a/Projects/Domain/Sources/Model/Travel/Recommendation.swift +++ b/Projects/Domain/Sources/Model/Travel/Recommendation.swift @@ -9,27 +9,27 @@ import Foundation /// 추천 따라하기 콘텐츠 -public struct Recommendation: Hashable { - public let id: Int - public let title: String - public let authorName: String - public let destination: String - public let duration: String - public let thumbnailURL: String? - - public init( - id: Int, - title: String, - authorName: String, - destination: String, - duration: String, - thumbnailURL: String? - ) { - self.id = id - self.title = title - self.authorName = authorName - self.destination = destination - self.duration = duration - self.thumbnailURL = thumbnailURL - } -} +//public struct Recommendation: Hashable { +// public let id: Int +// public let title: String +// public let authorName: String +// public let destination: String +// public let duration: String +// public let thumbnailURL: String? +// +// public init( +// id: Int, +// title: String, +// authorName: String, +// destination: String, +// duration: String, +// thumbnailURL: String? +// ) { +// self.id = id +// self.title = title +// self.authorName = authorName +// self.destination = destination +// self.duration = duration +// self.thumbnailURL = thumbnailURL +// } +//} diff --git a/Projects/Domain/Sources/UseCase/HomeUsecase.swift b/Projects/Domain/Sources/UseCase/HomeUsecase.swift new file mode 100644 index 0000000..55de0ed --- /dev/null +++ b/Projects/Domain/Sources/UseCase/HomeUsecase.swift @@ -0,0 +1,60 @@ +// +// HomeUsecase.swift +// Domain +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +// MARK: - API 나오기 전 임시 +public protocol HomeUsecaseProtocol { + func fetchMyTripInfo() async throws -> MyTripSummary? + func fetchCategoryList() async throws -> [TripCategory] + func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] + func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] +} + +public extension HomeUsecaseProtocol { + func fetchPopularTripList( + id: Int? = nil, + page: Int? = nil, + size: Int? = nil + ) async throws -> [TripInfo] { + try await fetchPopularTripList(id: id, page: page, size: size) + } + + func fetchRecommendTripList( + page: Int? = nil, + size: Int? = nil + ) async throws -> [TripInfo] { + try await fetchRecommendTripList(page: page, size: size) + } +} + +public final class HomeUsecase { + private let repository: HomeRepositoryInterface + + public init(repository: HomeRepositoryInterface) { + self.repository = repository + } +} + +extension HomeUsecase: HomeUsecaseProtocol { + public func fetchMyTripInfo() async throws -> MyTripSummary? { + try await repository.fetchMyTripInfo() + } + + public func fetchCategoryList() async throws -> [TripCategory] { + try await repository.fetchCategoryList() + } + + public func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] { + try await repository.fetchPopularTripList(id: id, page: page, size: size) + } + + public func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] { + try await repository.fetchRecommendTripList(page: page, size: size) + } +} From c2727fb6f1c23d67cdfbb96e7451ae52c119001f Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 18:44:17 +0900 Subject: [PATCH 16/47] =?UTF-8?q?feat:=20#18=20-=20Home=20=EA=B4=80?= =?UTF-8?q?=ED=98=84=20Data=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Sources/DI/HomeServiceFactory.swift | 17 +++++++++ .../Repository/Home/HomeRepository.swift | 36 +++++++++++++++++++ .../Sources/Transform/ProgramResponse+.swift | 18 ++++++++++ .../Sources/Transform/TripResponse+.swift | 30 ++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 Projects/Data/Sources/DI/HomeServiceFactory.swift create mode 100644 Projects/Data/Sources/Repository/Home/HomeRepository.swift create mode 100644 Projects/Data/Sources/Transform/ProgramResponse+.swift create mode 100644 Projects/Data/Sources/Transform/TripResponse+.swift diff --git a/Projects/Data/Sources/DI/HomeServiceFactory.swift b/Projects/Data/Sources/DI/HomeServiceFactory.swift new file mode 100644 index 0000000..81f6021 --- /dev/null +++ b/Projects/Data/Sources/DI/HomeServiceFactory.swift @@ -0,0 +1,17 @@ +// +// HomeServiceFactory.swift +// Data +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Networks +import Domain + +import Moya + +public func makeHomeService(tokenProvider: TokenProviding) -> HomeServiceProtocol { + let provider: MoyaProvider = NetworkProviderFactory.makeAuthenticatedProvider(tokenProvider: tokenProvider) + return HomeService(provider: provider) +} diff --git a/Projects/Data/Sources/Repository/Home/HomeRepository.swift b/Projects/Data/Sources/Repository/Home/HomeRepository.swift new file mode 100644 index 0000000..3215f8f --- /dev/null +++ b/Projects/Data/Sources/Repository/Home/HomeRepository.swift @@ -0,0 +1,36 @@ +// +// HomeRepository.swift +// Data +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +public final class HomeRepository: HomeRepositoryInterface { + private let homeService: HomeServiceProtocol + + public init(homeService: HomeServiceProtocol) { + self.homeService = homeService + } + + public func fetchMyTripInfo() async throws -> MyTripSummary? { + return MyTripSummary(title: "임시", startDay: .now, endDay: .now, tripSchedule: [Schedule(id: 1, day: 1, placeName: "임시", thumbnailUrl: "", transport: "", estimatedDuration: 2)]) + } + + public func fetchCategoryList() async throws -> [TripCategory] { + try await homeService.getCategoryList().map { $0.toDomain() } + } + + public func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] { + try await homeService.getPopularTripList(id: id, page: page, size: size).toDomain() + } + + public func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] { + try await homeService.getRecommendTripList(page: page, size: size).toDomain() + } +} diff --git a/Projects/Data/Sources/Transform/ProgramResponse+.swift b/Projects/Data/Sources/Transform/ProgramResponse+.swift new file mode 100644 index 0000000..80aefd9 --- /dev/null +++ b/Projects/Data/Sources/Transform/ProgramResponse+.swift @@ -0,0 +1,18 @@ +// +// ProgramResponse+.swift +// Data +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension ProgramResponse { + public func toDomain() -> TripCategory { + .init(id: self.id, creator: self.name, viedoType: VideoType(rawValue: self.type)) + } +} diff --git a/Projects/Data/Sources/Transform/TripResponse+.swift b/Projects/Data/Sources/Transform/TripResponse+.swift new file mode 100644 index 0000000..64c6d05 --- /dev/null +++ b/Projects/Data/Sources/Transform/TripResponse+.swift @@ -0,0 +1,30 @@ +// +// TripResponse+.swift +// Data +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension TripResponse { + public func toDomain() -> [TripInfo] { + self.content.map { + .init( + id: $0.travelId, + title: $0.title, + thumbnailUrl: $0.thumbnail ?? "", + creator: $0.programName, + country: $0.country, + city: $0.city, + nights: $0.nights, + days: $0.days, + countryCode: $0.country + ) + } + } +} From abba5117b5556356c1ac5a43d12032515885b575 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:15:08 +0900 Subject: [PATCH 17/47] =?UTF-8?q?feat:=20#18=20-=20HomePresentationModel?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/HomePresentationModel.swift | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift diff --git a/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift new file mode 100644 index 0000000..0380755 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift @@ -0,0 +1,117 @@ +// +// HomePresentationModel.swift +// HomeFeature +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain + +struct HomePresentationModel { + let banner: HomePresentationModel.Banner + let category: [HomePresentationModel.Category] + let popularTrip: [HomePresentationModel.PopularTrip] + let recommendedTrip: [HomePresentationModel.RecommendedTrip] + + struct Banner: Hashable { + let id = UUID() + let title: String + let startDay: Date + let endDay: Date + let tripSchedule: [Schedule] + } + + struct Schedule: Hashable { + let id: Int + let day: Int + let placeName: String + let thumbnailUrl: String + let transport: String + let estimatedDuration: Int + } + + struct Category: Hashable { + let id: Int + let creator: String + let viedoType: VideoType + } + + struct PopularTrip: Hashable { + let id: String + let title: String + let thumbnailUrl: String + let creator: String + let schedule: String + let country: String + let city: String + } + + struct RecommendedTrip: Hashable { + let id: String + let title: String + let thumbnailUrl: String + let creator: String + let country: String + let schedule: String + let city: String + } +} + +extension MyTripSummary { + func toPresention() -> HomePresentationModel.Banner { + return HomePresentationModel.Banner( + title: self.title, + startDay: self.startDay, + endDay: self.endDay, + tripSchedule: self.tripSchedule.map { + HomePresentationModel.Schedule( + id: $0.id, + day: $0.day, + placeName: $0.placeName, + thumbnailUrl: $0.thumbnailUrl, + transport: $0.transport, + estimatedDuration: $0.estimatedDuration + ) + } + ) + } +} + +extension TripCategory { + func toPresentaion() -> HomePresentationModel.Category { + return HomePresentationModel.Category( + id: self.id, + creator: self.creator, + viedoType: self.viedoType + ) + } +} + +extension TripInfo { + func toPopularPresentaion() -> HomePresentationModel.PopularTrip { + return HomePresentationModel.PopularTrip( + id: self.id, + title: self.title, + thumbnailUrl: self.thumbnailUrl, + creator: self.creator, + schedule: "\(self.nights)박 \(self.days)일", + country: self.country, + city: self.city + ) + } + + func toPresentaion() -> HomePresentationModel.RecommendedTrip { + return HomePresentationModel.RecommendedTrip( + id: self.id, + title: self.title, + thumbnailUrl: self.thumbnailUrl, + creator: self.creator, + country: self.country, + schedule: "\(self.nights)박 \(self.days)일", + city: self.city + ) + } +} From b5c9c3200f62fbc802ab11905d952101362a8dcc Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:15:33 +0900 Subject: [PATCH 18/47] =?UTF-8?q?design:=20#18=20-=20HomeHeaderView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Views/Cells/HomeHeaderView.swift | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/HomeHeaderView.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/HomeHeaderView.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeHeaderView.swift new file mode 100644 index 0000000..06733bf --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeHeaderView.swift @@ -0,0 +1,58 @@ +// +// HomeHeaderView.swift +// HomeFeature +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +final class HomeHeaderView: UICollectionReusableView { + private let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + + titleLabel.text = nil + } + + func configure(title: String) { + titleLabel.do { + $0.setText(.subTitleLSB, text: title, color: DSKitAsset.Colors.black900.color) + } + } +} + +private extension HomeHeaderView { + func setStyle() { + titleLabel.do { + $0.numberOfLines = 2 + } + } + + func setUI() { + addSubview(titleLabel) + } + + func setLayout() { + titleLabel.snp.makeConstraints { + $0.leading.equalToSuperview() + $0.directionalVerticalEdges.equalToSuperview() + } + } +} From f981fb099856d778a84fd5a682040832e17d647a Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:15:53 +0900 Subject: [PATCH 19/47] =?UTF-8?q?design:=20#18=20-=20HomeFooterButtonView?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Cells/HomeFooterButtonView.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/HomeFooterButtonView.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/HomeFooterButtonView.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeFooterButtonView.swift new file mode 100644 index 0000000..edcdb6c --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeFooterButtonView.swift @@ -0,0 +1,72 @@ +// +// HomeFooterButtonView.swift +// HomeFeature +// +// Created by 최안용 on 1/31/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +import RxCocoa +import RxSwift + +final class HomeFooterButtonView: UICollectionReusableView { + private let plusButton = UIButton() + + var disposeBag = DisposeBag() + var plusBtnTapped: Observable { + plusButton.rx.tap.asObservable() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag = DisposeBag() + } +} + +private extension HomeFooterButtonView { + func setStyle() { + plusButton.do { + var configure = UIButton.Configuration.plain() + let fontAttributes = UIFont.NDGL.bodyMSB.attributes + configure.baseForegroundColor = DSKitAsset.Colors.black600.color + configure.attributedTitle = AttributedString( + "여행 따라가기 더보기", + attributes: AttributeContainer(fontAttributes) + ) + configure.background.cornerRadius = 8.adjustedH + configure.background.strokeWidth = 1 + configure.background.strokeColor = DSKitAsset.Colors.black200.color + $0.configuration = configure + } + } + + func setUI() { + addSubview(plusButton) + } + + func setLayout() { + plusButton.snp.makeConstraints { + $0.top.equalToSuperview() + $0.height.equalTo(40.adjustedH) + $0.directionalHorizontalEdges.equalToSuperview() + } + } +} + From 9af5995e832e050fa8a842af8718575879c5a38b Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:16:14 +0900 Subject: [PATCH 20/47] =?UTF-8?q?design:=20#18=20-=20HomeBannerCell=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Views/Cells/HomeBannerCell.swift | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift new file mode 100644 index 0000000..2c51376 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift @@ -0,0 +1,164 @@ +// +// HomeBannerCell.swift +// HomeFeature +// +// Created by 최안용 on 2/3/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +final class HomeBannerCell: UICollectionViewCell { + static let defaultWidth = 327.adjusted + + private var type: HomeBannerType = .empty + + private let emptyView = HomeBannerEmptyView() + private let upCommingView = HomeBannerUpCommingView() + private let onGoingView = HomeBannerOnGoingView() + private let stackView = UIStackView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + + [emptyView, upCommingView, onGoingView].forEach { $0.isHidden = true } + + upCommingView.prepareForReuse() + onGoingView.prepareForReuse() + } + + func configure(_ model: HomePresentationModel.Banner) { + [emptyView, upCommingView, onGoingView].forEach { $0.isHidden = true } + + if model.tripSchedule.isEmpty { + self.type = .empty + emptyView.isHidden = false + return + } + + let now = Date() + let calendar = Calendar.current + + let startOfToday = calendar.startOfDay(for: now) + let startOfTravel = calendar.startOfDay(for: model.startDay) + let startOfEnd = calendar.startOfDay(for: model.endDay) + let dateRangeString = formatDateRange(start: model.startDay, end: model.endDay) + + if startOfToday >= startOfTravel && startOfToday <= startOfEnd { + let schedule = model.tripSchedule.first + + self.type = .onGoing( + title: model.title, + date: dateRangeString, + transportIcon: DSKitAsset.Assets.icBus2.image, + duration: "\(schedule?.estimatedDuration ?? 0)분", + place: schedule?.placeName ?? "", + imageUrl: schedule?.thumbnailUrl ?? "" + ) + onGoingView.isHidden = false + + } + + else if startOfToday < startOfTravel { + let dDayValue = calendar.dateComponents([.day], from: startOfToday, to: startOfTravel).day ?? 0 + + self.type = .upComming( + title: model.title, + date: dateRangeString, + dDay: dDayValue, + imageUrl: model.tripSchedule.first?.thumbnailUrl ?? "" + ) + upCommingView.isHidden = false + } + + else { + self.type = .empty + emptyView.isHidden = false + } + + updateViewWithCurrentType() + } +} + +private extension HomeBannerCell { + func formatDateRange(start: Date, end: Date) -> String { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "M월 d일" + + let startStr = formatter.string(from: start) + let endStr = formatter.string(from: end) + + return "\(startStr) ~ \(endStr)" + } + + func updateViewWithCurrentType() { + switch type { + case .upComming(let title, let date, let dDay, let imageUrl): + upCommingView.configure(title: title, date: date, dDay: dDay, imageUrl: imageUrl) + case .onGoing(let title, let date, let transportIcon, let duration, let place, let imageUrl): + onGoingView.configure( + title: title, + date: date, + transportIcon: transportIcon, + transport: "대중교통", + duration: duration, + place: place, + imageUrl: imageUrl + ) + case .empty: + break + } + } + func setStyle() { + contentView.do { + $0.backgroundColor = DSKitAsset.Colors.black50.color + $0.layer.cornerRadius = 8.adjustedH + $0.clipsToBounds = true + } + + stackView.do { + $0.axis = .vertical + $0.alignment = .fill + $0.distribution = .fill + } + } + + func setUI() { + contentView.addSubview(stackView) + stackView.addArrangedSubviews(emptyView, upCommingView, onGoingView) + } + + func setLayout() { + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} + +enum HomeBannerType: Hashable { + case empty + case upComming(title: String, date: String, dDay: Int, imageUrl: String) + case onGoing( + title: String, + date: String, + transportIcon: UIImage?, + duration: String, + place: String, + imageUrl: String + ) +} From 46cc2983b1ecfb69e91ade2c8f6f3caf2dfef3ad Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:16:31 +0900 Subject: [PATCH 21/47] =?UTF-8?q?design:=20#18=20-=20RecommendInfoCell=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Cells/RecommendInfoCell.swift | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift new file mode 100644 index 0000000..a75ab9e --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift @@ -0,0 +1,136 @@ +// +// RecommendInfoCell.swift +// HomeFeature +// +// Created by 최안용 on 1/31/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +final class RecommendInfoCell: UICollectionViewCell { + static let defaultWidth = 240.adjusted + static let defaultHeight = 253.adjustedH + + // MARK: - UI Components + private let thumbnailView = UIImageView() + private let nationalFlagLabel = UILabel() + private let nationLabel = UILabel() + private let titleLabel = UILabel() + private let firstDotLabel = UILabel() + private let secondDotLabel = UILabel() + private let nameLabel = UILabel() + private let scheduleLabel = UILabel() + private let cityLabel = UILabel() + + private let nationStackView = UIStackView() + private let subInfoStackView = UIStackView() + private let infoStackView = UIStackView() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Configure + func configure(_ model: HomePresentationModel.RecommendedTrip) { + if let url = URL(string: model.thumbnailUrl) { + thumbnailView.kf.setImage(with: url, options: [.transition(.fade(0.3))]) + } + nationalFlagLabel.text = model.country.toFlag() + nationLabel.setText(.bodyMM, text: model.country.toKoreanCountryName(), color: DSKitAsset.Colors.black400.color) + titleLabel.setText(.bodyLSB, text: model.title, color: DSKitAsset.Colors.black700.color) + nameLabel.setText(.bodyMM, text: model.creator, color: DSKitAsset.Colors.black400.color) + cityLabel.setText(.bodyMM, text: model.city, color: DSKitAsset.Colors.black400.color) + scheduleLabel.setText(.bodyMM, text: model.schedule, color: DSKitAsset.Colors.black400.color) + } +} + +private extension RecommendInfoCell { + func setStyle() { + thumbnailView.do { + $0.layer.cornerRadius = 8 + $0.layer.maskedCorners = CACornerMask(arrayLiteral: [.layerMinXMinYCorner, .layerMaxXMinYCorner]) + $0.clipsToBounds = true + $0.contentMode = .scaleAspectFill + $0.backgroundColor = .systemGray6 + } + + nationalFlagLabel.do { + $0.font = .systemFont(ofSize: 10.5 * max(1.adjusted, 1.adjustedH)) + } + + titleLabel.do { + $0.numberOfLines = 2 + $0.lineBreakMode = .byTruncatingTail + } + + firstDotLabel.do { + $0.setText(.bodyMM, text: "•", color: DSKitAsset.Colors.black400.color) + } + + secondDotLabel.do { + $0.setText(.bodyMM, text: "•", color: DSKitAsset.Colors.black400.color) + } + + nationStackView.do { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + } + + subInfoStackView.do { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + } + + infoStackView.do { + $0.axis = .vertical + $0.alignment = .leading + $0.spacing = 4 + } + } + + func setUI() { + contentView.addSubviews(thumbnailView, nationStackView, infoStackView) + + nationStackView.addArrangedSubviews(nationalFlagLabel, nationLabel) + + subInfoStackView.addArrangedSubviews( + nameLabel, + firstDotLabel, + cityLabel, + secondDotLabel, + scheduleLabel + ) + infoStackView.addArrangedSubviews(titleLabel, subInfoStackView) + } + + func setLayout() { + thumbnailView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.height.equalTo(thumbnailView.snp.width).multipliedBy(140.0 / 240.0) + } + + nationStackView.snp.makeConstraints { + $0.leading.equalToSuperview() + $0.top.equalTo(thumbnailView.snp.bottom).offset(16.adjustedH) + } + + infoStackView.snp.makeConstraints { + $0.top.equalTo(nationStackView.snp.bottom).offset(10.adjustedH) + $0.leading.trailing.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview().inset(18.adjustedH) + } + } +} From 7670fc467bd6521f97e2d8827799bb68621f0ce6 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:16:50 +0900 Subject: [PATCH 22/47] =?UTF-8?q?design:=20#18=20-=20HomeBannerEmptyView?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Component/HomeBannerEmptyView.swift | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerEmptyView.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerEmptyView.swift b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerEmptyView.swift new file mode 100644 index 0000000..6971486 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerEmptyView.swift @@ -0,0 +1,86 @@ +// +// HomeBannerEmptyView.swift +// HomeFeature +// +// Created by 최안용 on 2/3/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +final class HomeBannerEmptyView: UIView { + private let titleLabel = UILabel() + private let subTitleLabel = UILabel() + private let titleStackView = UIStackView() + private let stackView = UIStackView() + private let imageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension HomeBannerEmptyView { + func setStyle() { + backgroundColor = .clear + + titleLabel.do { + $0.setText( + .bodyLSB, + text: "아직 등록된 여행지가 없어요", + color: DSKitAsset.Colors.black700.color + ) + } + + subTitleLabel.do { + $0.setText( + .bodyMM, + text: "새 여행 일정을 만들어 보세요!", + color: DSKitAsset.Colors.black400.color + ) + } + + titleStackView.do { + $0.axis = .vertical + $0.spacing = 6.adjustedH + $0.alignment = .leading + } + + stackView.do { + $0.axis = .horizontal + $0.spacing = 4.adjusted + $0.alignment = .center + } + + imageView.do { + $0.image = DSKitAsset.Assets.icEmptyTrip.image + } + } + + func setUI() { + titleStackView.addArrangedSubviews(titleLabel, subTitleLabel) + stackView.addArrangedSubviews(titleStackView, imageView) + addSubviews(stackView) + } + + func setLayout() { + imageView.snp.makeConstraints { + $0.size.equalTo(76.adjustedH) + } + + stackView.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(16) + $0.directionalVerticalEdges.equalToSuperview().inset(2).priority(.high) + } + } +} From 39d6928cafaa1587d69e0348e9ce2fef89fc2897 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:17:02 +0900 Subject: [PATCH 23/47] =?UTF-8?q?design:=20#18=20-=20HomeBannerOnGogingVie?= =?UTF-8?q?w=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Component/HomeBannerOnGoingView.swift | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerOnGoingView.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerOnGoingView.swift b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerOnGoingView.swift new file mode 100644 index 0000000..7c75970 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerOnGoingView.swift @@ -0,0 +1,169 @@ +// +// HomeBannerOnGoingView.swift +// HomeFeature +// +// Created by 최안용 on 2/3/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit +final class HomeBannerOnGoingView: UIView { + private let titleLabel = UILabel() + private let dateLabel = UILabel() + private let iconImageView = UIImageView() + private let transportLabel = UILabel() + private let dotLabel = UILabel() + private let durationLabel = UILabel() + private let placeLabel = UILabel() + private let imageView = UIImageView() + private let titleStackView = UIStackView() + private let subInfoStackView = UIStackView() + private let infoStackView = UIStackView() + private let routeStackView = UIStackView() + private let routeCardView = UIView() + private let containerStackView = UIStackView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure( + title: String, + date: String, + transportIcon: UIImage?, + transport: String, + duration: String, + place: String, + imageUrl: String + ) { + titleLabel.setText(.bodyMSB, text: title, color: DSKitAsset.Colors.black700.color) + dateLabel.setText(.bodyMR, text: date, color: DSKitAsset.Colors.black500.color) + iconImageView.image = transportIcon?.withRenderingMode(.alwaysTemplate) + transportLabel.setText(.bodySM, text: transport, color: DSKitAsset.Colors.black400.color) + durationLabel.setText(.bodySM, text: "\(duration) 체류 예상", color: DSKitAsset.Colors.black400.color) + placeLabel.setText(.bodyLSB, text: place, color: DSKitAsset.Colors.black900.color) + + if let url = URL(string: imageUrl) { + imageView.kf.setImage(with: url) + } else { + imageView.backgroundColor = .systemGray5 + } + } + + func prepareForReuse() { + imageView.kf.cancelDownloadTask() + titleLabel.text = nil + dateLabel.text = nil + placeLabel.text = nil + durationLabel.text = nil + imageView.image = nil + iconImageView.image = nil + transportLabel.text = nil + } +} + +private extension HomeBannerOnGoingView { + func setStyle() { + backgroundColor = .clear + + dotLabel.do { + $0.setText(.bodyMM, text: "•", color: DSKitAsset.Colors.black400.color) + } + + iconImageView.do { + $0.tintColor = DSKitAsset.Colors.black500.color + } + + imageView.do { + $0.layer.cornerRadius = 4.adjustedH + $0.backgroundColor = .systemGray6 + $0.clipsToBounds = true + } + + titleStackView.do { + $0.axis = .vertical + $0.spacing = 4.adjustedH + $0.alignment = .leading + } + + subInfoStackView.do { + $0.axis = .horizontal + $0.spacing = 4.adjusted + $0.alignment = .center + } + + infoStackView.do { + $0.axis = .vertical + $0.spacing = 10.adjustedH + $0.alignment = .leading + } + + routeStackView.do { + $0.axis = .horizontal + $0.spacing = 12.adjusted + $0.alignment = .center + } + + routeCardView.do { + $0.backgroundColor = DSKitAsset.Colors.white.color + $0.layer.cornerRadius = 16.adjustedH + $0.clipsToBounds = true + } + + containerStackView.do { + $0.axis = .vertical + $0.spacing = 16.adjustedH + $0.alignment = .leading + } + } + + func setUI() { + titleStackView.addArrangedSubviews(titleLabel, dateLabel) + subInfoStackView.addArrangedSubviews(iconImageView, transportLabel, dotLabel, durationLabel) + infoStackView.addArrangedSubviews(subInfoStackView, placeLabel) + routeStackView.addArrangedSubviews(infoStackView, imageView) + routeCardView.addSubview(routeStackView) + containerStackView.addArrangedSubviews(titleStackView, routeCardView) + addSubview(containerStackView) + } + + func setLayout() { + iconImageView.snp.makeConstraints { + $0.size.equalTo(14.adjustedH) + } + + imageView.snp.makeConstraints { + $0.size.equalTo(56.adjustedH) + } + + routeStackView.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(16.adjusted) + $0.top.equalToSuperview().inset(12.adjustedH) + $0.bottom.equalToSuperview().inset(16.adjustedH) + } + + routeCardView.snp.makeConstraints { + $0.width.equalToSuperview() + } + + titleStackView.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview() + } + + containerStackView.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(16.adjusted) + $0.top.equalToSuperview().inset(16.adjustedH) + $0.bottom.equalToSuperview().inset(23.adjustedH).priority(.low) + } + } +} From ee416c68c2da8815624d7fb8040c1923272b0ea5 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:17:15 +0900 Subject: [PATCH 24/47] =?UTF-8?q?design:=20#18=20-=20HomeBannerUpCommingVi?= =?UTF-8?q?ew=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Component/HomeBannerUpCommingView.swift | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerUpCommingView.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerUpCommingView.swift b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerUpCommingView.swift new file mode 100644 index 0000000..b1756bb --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/Component/HomeBannerUpCommingView.swift @@ -0,0 +1,120 @@ +// +// HomeBannerUpCommingView.swift +// HomeFeature +// +// Created by 최안용 on 2/3/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit +final class HomeBannerUpCommingView: UIView { + private let imageView = UIImageView() + private let badge = UIView() + private let dDayLabel = UILabel() + private let titleLabel = UILabel() + private let dateLabel = UILabel() + private let titleStackView = UIStackView() + private let infoStackView = UIStackView() + private let stackView = UIStackView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(title: String, date: String, dDay: Int, imageUrl: String) { + titleLabel.setText(.subTitleMSB, text: title, color: DSKitAsset.Colors.black700.color) + dateLabel.setText(.bodyMR, text: date, color: DSKitAsset.Colors.black600.color) + dDayLabel.setText(.bodyMM, text: "D-\(dDay)", color: DSKitAsset.Colors.black400.color) + + if let url = URL(string: imageUrl) { + imageView.kf.setImage(with: url) + } else { + imageView.backgroundColor = .systemGray5 + } + } + + func prepareForReuse() { + imageView.kf.cancelDownloadTask() + titleLabel.text = nil + dateLabel.text = nil + dDayLabel.text = nil + imageView.image = nil + } +} + +private extension HomeBannerUpCommingView { + func setStyle() { + backgroundColor = .clear + + imageView.do { + $0.layer.cornerRadius = 64.adjustedH / 2 + $0.clipsToBounds = true + $0.contentMode = .scaleAspectFill + } + + badge.do { + $0.backgroundColor = DSKitAsset.Colors.black100.color + $0.layer.cornerRadius = 26.adjustedH / 2 + $0.clipsToBounds = true + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + $0.setContentHuggingPriority(.required, for: .horizontal) + } + + titleLabel.do { + $0.numberOfLines = 1 + $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + + titleStackView.do { + $0.axis = .horizontal + $0.spacing = 8.adjusted + $0.alignment = .center + } + + infoStackView.do { + $0.axis = .vertical + $0.spacing = 6.adjustedH + $0.alignment = .leading + } + + stackView.do { + $0.axis = .horizontal + $0.spacing = 12.adjusted + $0.alignment = .center + } + } + + func setUI() { + badge.addSubview(dDayLabel) + titleStackView.addArrangedSubviews(badge, titleLabel) + infoStackView.addArrangedSubviews(titleStackView, dateLabel) + stackView.addArrangedSubviews(imageView, infoStackView) + addSubview(stackView) + } + + func setLayout() { + imageView.snp.makeConstraints { + $0.size.equalTo(64.adjustedH) + } + + dDayLabel.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(12.adjusted) + $0.directionalVerticalEdges.equalToSuperview().inset(4.adjustedH) + } + + stackView.snp.makeConstraints { + $0.directionalHorizontalEdges.equalToSuperview().inset(16.adjusted) + $0.directionalVerticalEdges.equalToSuperview().inset(8.adjustedH).priority(.high) + } + } +} From c8a331bd3402543f1c0da68e8d828f845a4e830c Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Tue, 10 Feb 2026 19:18:50 +0900 Subject: [PATCH 25/47] =?UTF-8?q?feat:=20#18=20-=20Home=20CompositionalLay?= =?UTF-8?q?out=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeCompositionalLayout.swift | 187 ++++++++++++++++++ .../Views/CollectionView/Item/HomeItem.swift | 16 ++ .../Registration/HomeRegistration.swift | 71 +++++++ .../SectionKind/HomeSectionKind.swift | 26 +++ 4 files changed, 300 insertions(+) create mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift create mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionView/Item/HomeItem.swift create mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionView/Registration/HomeRegistration.swift create mode 100644 Projects/Features/HomeFeature/Sources/Views/CollectionView/SectionKind/HomeSectionKind.swift diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift new file mode 100644 index 0000000..d1d978e --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift @@ -0,0 +1,187 @@ +// +// HomeCompositionalLayout.swift +// HomeFeature +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +extension HomeViewController { + func createLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in + guard let sectionKind = HomeSectionKind(rawValue: sectionIndex) else { + return self?.emptyLayout() + } + + switch sectionKind { + case .banner: + return self?.createBannerSection() + case .category: + return self?.createCategorySection() + case .popularTrip: + return self?.createPopularTripSection() + case .recommendedTrip: + return self?.createRecommendedTripSection() + } + } + } +} + +private extension HomeViewController { + func createBannerSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(80) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(HomeBannerCell.defaultWidth), + heightDimension: .estimated(80) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + repeatingSubitem: item, + count: 1 + ) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .none + section.contentInsets = .init( + top: 21.adjustedH, + leading: 24.adjusted, + bottom: 40.adjustedH, + trailing: 24.adjusted + ) + return section + } + + func createCategorySection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(72), + heightDimension: .absolute(CategoryChipCell.defaultHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .estimated(72), + heightDimension: .absolute(CategoryChipCell.defaultHeight) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.interGroupSpacing = 8.adjusted + section.contentInsets = .init( + top: 24.adjustedH, + leading: 24.adjusted, + bottom: 16.adjustedH, + trailing: 24.adjusted + ) + section.boundarySupplementaryItems = [createHeaderLayout()] + return section + } + + func createPopularTripSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(PopularInfoCell.defaultHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let itemSpacing: CGFloat = 12.adjustedH + let totalGroupHeight = (PopularInfoCell.defaultHeight * 3) + (itemSpacing * 2) + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(311.adjusted), + heightDimension: .estimated(totalGroupHeight) + ) + let group = NSCollectionLayoutGroup.vertical( + layoutSize: groupSize, + repeatingSubitem: item, + count: 3 + ) + group.interItemSpacing = .fixed(itemSpacing) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 8.adjusted + section.contentInsets = .init( + top: 0, + leading: 16.adjusted, + bottom: 24.adjustedH, + trailing: 24.adjusted + ) + section.orthogonalScrollingBehavior = .groupPagingCentered + + let footerSize = NSCollectionLayoutSize( + widthDimension: .absolute(327.adjusted), + heightDimension: .estimated(80.adjustedH) + ) + + let footer = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: footerSize, + elementKind: UICollectionView.elementKindSectionFooter, + alignment: .bottom + ) + footer.contentInsets = .init(top: 0, leading: 8.adjusted, bottom: 0, trailing: 0) + section.boundarySupplementaryItems = [footer] + + return section + } + + func createRecommendedTripSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(RecommendInfoCell.defaultHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(RecommendInfoCell.defaultWidth), + heightDimension: .estimated(RecommendInfoCell.defaultHeight) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 16.adjusted + section.contentInsets = .init(top: 24, leading: 24, bottom: 81.adjustedH, trailing: 24) + section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary + + section.boundarySupplementaryItems = [createHeaderLayout()] + return section + } + + func createHeaderLayout() -> NSCollectionLayoutBoundarySupplementaryItem { + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(60) + ) + + return NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + } + + func emptyLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0) + ) + let group = NSCollectionLayoutGroup(layoutSize: groupSize) + + let section = NSCollectionLayoutSection(group: group) + + return section + } +} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionView/Item/HomeItem.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionView/Item/HomeItem.swift new file mode 100644 index 0000000..9a7b881 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/CollectionView/Item/HomeItem.swift @@ -0,0 +1,16 @@ +// +// HomeItem.swift +// HomeFeature +// +// Created by 최안용 on 2/4/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum HomeItem: Hashable { + case banner(HomePresentationModel.Banner) + case category(HomePresentationModel.Category, isSelected: Bool) + case popularTrip(HomePresentationModel.PopularTrip) + case recommendedTrip(HomePresentationModel.RecommendedTrip) +} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionView/Registration/HomeRegistration.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionView/Registration/HomeRegistration.swift new file mode 100644 index 0000000..f1558f3 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/CollectionView/Registration/HomeRegistration.swift @@ -0,0 +1,71 @@ +// +// HomeRegistration.swift +// HomeFeature +// +// Created by 최안용 on 2/6/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +extension HomeViewController { + func createBannerCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.configure(item) + } + } + + func createCategoryCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + let (chip, isSelected) = item + + let chipType: ChipIconType = { + switch chip.viedoType { + case .tv: ChipIconType.tv + case .youtube: ChipIconType.youtube + case .none: ChipIconType.none + } + }() + + cell.configure(chipType, chip.creator, isSelected) + } + } + + func createPopularTripCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.configure( + thumbnailUrl: item.thumbnailUrl, + city: item.city, + title: item.title, + nation: item.country, + schedule: item.schedule + ) + } + } + + func createRecommedTripCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.configure(item) + } + } + + func createHeaderRegistration() -> UICollectionView.SupplementaryRegistration { + return UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { headerView,elementKind,indexPath in + guard let sectionKind = HomeSectionKind(rawValue: indexPath.section) else { return } + + headerView.configure(title: sectionKind.headerTitle) + } + } + + func createPopularFooterRegistration() -> UICollectionView.SupplementaryRegistration { + return UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionFooter) { [weak self] footerView,elementKind,indexPath in + guard let self = self else { return } + + footerView.plusBtnTapped + .bind(to: self.moreButtonTapped) + .disposed(by: footerView.disposeBag) + } + } +} diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionView/SectionKind/HomeSectionKind.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionView/SectionKind/HomeSectionKind.swift new file mode 100644 index 0000000..1dc7642 --- /dev/null +++ b/Projects/Features/HomeFeature/Sources/Views/CollectionView/SectionKind/HomeSectionKind.swift @@ -0,0 +1,26 @@ +// +// HomeSectionKind.swift +// HomeFeature +// +// Created by 최안용 on 1/30/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum HomeSectionKind: Int, CaseIterable { + case banner + case category + case popularTrip + case recommendedTrip + + var headerTitle: String { + switch self { + case .category: + "인기 여행 따라가기" + case .recommendedTrip: + "나혜주님께 추천하는\n따라가기 여행 콘텐츠에요!" + default: "" + } + } +} From 19dd8aa53bdf074c6b632d48238cf486b18ff1a0 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:19:37 +0900 Subject: [PATCH 26/47] =?UTF-8?q?feat:=20#18=20-=20feature=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency+Project.swift | 28 +++++++++++++++++ Projects/Features/HomeFeature/Project.swift | 4 +-- Projects/Features/MainFeature/Project.swift | 30 +++++++++++++++++++ .../PopularTravelFeature/Project.swift | 25 ++++++++++++++++ Projects/Features/SearchFeature/Project.swift | 25 ++++++++++++++++ .../Features/SettingFeature/Project.swift | 25 ++++++++++++++++ Projects/Features/TabBarFeature/Project.swift | 3 +- 7 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 Projects/Features/MainFeature/Project.swift create mode 100644 Projects/Features/PopularTravelFeature/Project.swift create mode 100644 Projects/Features/SearchFeature/Project.swift create mode 100644 Projects/Features/SettingFeature/Project.swift diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index f12a831..3b943f2 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -9,10 +9,14 @@ import ProjectDescription public extension TargetDependency { struct Features { + public struct Main {} public struct Home {} public struct TabBar {} public struct Follow {} public struct Travel {} + public struct Search {} + public struct Setting {} + public struct PopularTravel {} } struct Modules {} @@ -63,3 +67,27 @@ public extension TargetDependency.Features.Travel { static let feature = TargetDependency.Features.project(name: "Feature", group: group) } + +public extension TargetDependency.Features.Search { + static let group = "Search" + + static let feature = TargetDependency.Features.project(name: "Feature", group: group) +} + +public extension TargetDependency.Features.Setting { + static let group = "Setting" + + static let feature = TargetDependency.Features.project(name: "Feature", group: group) +} + +public extension TargetDependency.Features.Main { + static let group = "Main" + + static let feature = TargetDependency.Features.project(name: "Feature", group: group) +} + +public extension TargetDependency.Features.PopularTravel { + static let group = "PopularTravel" + + static let feature = TargetDependency.Features.project(name: "Feature", group: group) +} diff --git a/Projects/Features/HomeFeature/Project.swift b/Projects/Features/HomeFeature/Project.swift index a5cc848..a241295 100644 --- a/Projects/Features/HomeFeature/Project.swift +++ b/Projects/Features/HomeFeature/Project.swift @@ -15,9 +15,7 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "HomeFeature", dependencies: [ - .Features.baseFeatureDependency, - .Features.Search.feature, - .Features.Setting.feature + .Features.baseFeatureDependency ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/MainFeature/Project.swift b/Projects/Features/MainFeature/Project.swift new file mode 100644 index 0000000..d04eb4e --- /dev/null +++ b/Projects/Features/MainFeature/Project.swift @@ -0,0 +1,30 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by 최안용 on 2026/02/11. +// + +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: "MainFeature", + targets: [ + .makeFrameworkTarget( + name: "MainFeature", + dependencies: [ + .Features.baseFeatureDependency, + .Features.Follow.feature, + .Features.Search.feature, + .Features.Setting.feature, + .Features.TabBar.feature, + .Features.PopularTravel.feature + ], + scripts: [.swiftLint], + isStatic: true, + hasResources: false + ) + ] +) diff --git a/Projects/Features/PopularTravelFeature/Project.swift b/Projects/Features/PopularTravelFeature/Project.swift new file mode 100644 index 0000000..d3ffd92 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Project.swift @@ -0,0 +1,25 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by 최안용 on 2026/02/12. +// + +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: "PopularTravelFeature", + targets: [ + .makeFrameworkTarget( + name: "PopularTravelFeature", + dependencies: [ + .Features.baseFeatureDependency + ], + scripts: [.swiftLint], + isStatic: true, + hasResources: false + ) + ] +) diff --git a/Projects/Features/SearchFeature/Project.swift b/Projects/Features/SearchFeature/Project.swift new file mode 100644 index 0000000..a515219 --- /dev/null +++ b/Projects/Features/SearchFeature/Project.swift @@ -0,0 +1,25 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by 최안용 on 2026/02/07. +// + +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: "SearchFeature", + targets: [ + .makeFrameworkTarget( + name: "SearchFeature", + dependencies: [ + .Features.baseFeatureDependency + ], + scripts: [.swiftLint], + isStatic: true, + hasResources: false + ) + ] +) diff --git a/Projects/Features/SettingFeature/Project.swift b/Projects/Features/SettingFeature/Project.swift new file mode 100644 index 0000000..88d3e63 --- /dev/null +++ b/Projects/Features/SettingFeature/Project.swift @@ -0,0 +1,25 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by 최안용 on 2026/02/09. +// + +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: "SettingFeature", + targets: [ + .makeFrameworkTarget( + name: "SettingFeature", + dependencies: [ + .Features.baseFeatureDependency + ], + scripts: [.swiftLint], + isStatic: true, + hasResources: false + ) + ] +) diff --git a/Projects/Features/TabBarFeature/Project.swift b/Projects/Features/TabBarFeature/Project.swift index 7b002e3..7f7d9af 100644 --- a/Projects/Features/TabBarFeature/Project.swift +++ b/Projects/Features/TabBarFeature/Project.swift @@ -16,8 +16,7 @@ let project = Project.makeModule( name: "TabBarFeature", dependencies: [ .Features.Home.feature, - .Features.Travel.feature, - .Features.Search.feature + .Features.Travel.feature ], scripts: [.swiftLint], isStatic: true, From e5c1cfc0e36bf731a7139e1d13af93ce05521156 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:21:20 +0900 Subject: [PATCH 27/47] =?UTF-8?q?feat:=20#18=20-=20Setting=20TableView=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/UIKit+/UITableViewCell+.swift | 15 +++++++ .../Sources/UITableView/SettingCellItem.swift | 41 +++++++++++++++++++ .../Sources/UITableView/SettingCellType.swift | 24 +++++++++++ .../Sources/UITableView/SettingSection.swift | 19 +++++++++ 4 files changed, 99 insertions(+) create mode 100644 Projects/Core/Sources/Extensions/UIKit+/UITableViewCell+.swift create mode 100644 Projects/Features/SettingFeature/Sources/UITableView/SettingCellItem.swift create mode 100644 Projects/Features/SettingFeature/Sources/UITableView/SettingCellType.swift create mode 100644 Projects/Features/SettingFeature/Sources/UITableView/SettingSection.swift diff --git a/Projects/Core/Sources/Extensions/UIKit+/UITableViewCell+.swift b/Projects/Core/Sources/Extensions/UIKit+/UITableViewCell+.swift new file mode 100644 index 0000000..5f07558 --- /dev/null +++ b/Projects/Core/Sources/Extensions/UIKit+/UITableViewCell+.swift @@ -0,0 +1,15 @@ +// +// UITableViewCell+.swift +// Core +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +public extension UITableViewCell { + static var cellIdentifier: String { + return String(describing: self) + } +} diff --git a/Projects/Features/SettingFeature/Sources/UITableView/SettingCellItem.swift b/Projects/Features/SettingFeature/Sources/UITableView/SettingCellItem.swift new file mode 100644 index 0000000..0d83393 --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/UITableView/SettingCellItem.swift @@ -0,0 +1,41 @@ +// +// SettingCellItem.swift +// SettingFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public enum SettingCellItem: Int, CaseIterable { + case notification + case faq + case recommendLink + case identificationCode + case terms + case version + + var title: String { + switch self { + case .notification: return "알림 설정" + case .faq: return "FAQ" + case .recommendLink: return "콘텐츠 추천 링크 넣기" + case .identificationCode: return "내 식별코드" + case .terms: return "서비스 약관" + case .version: return "버전 정보" + } + } + + var cellType: SettingCellType { + switch self { + case .notification: + return .toggle(isOn: true) + case .version: + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" + return .detailText(text: version) + default: + return .icon + } + } +} diff --git a/Projects/Features/SettingFeature/Sources/UITableView/SettingCellType.swift b/Projects/Features/SettingFeature/Sources/UITableView/SettingCellType.swift new file mode 100644 index 0000000..3e9e0d4 --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/UITableView/SettingCellType.swift @@ -0,0 +1,24 @@ +// +// SettingCellType.swift +// SettingFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum SettingCellType { + case toggle(isOn: Bool) + case icon + case detailText(text: String) + + var cellHeight: CGFloat { + switch self { + case .toggle: + return 63.adjustedH + case .icon, .detailText: + return 56.adjustedH + } + } +} diff --git a/Projects/Features/SettingFeature/Sources/UITableView/SettingSection.swift b/Projects/Features/SettingFeature/Sources/UITableView/SettingSection.swift new file mode 100644 index 0000000..3b360da --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/UITableView/SettingSection.swift @@ -0,0 +1,19 @@ +// +// SettingSection.swift +// SettingFeature +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum SettingSection: Int, CaseIterable { + case menu + + var items: [SettingCellItem] { + switch self { + case .menu: [.notification, .faq, .recommendLink, .terms, .version] + } + } +} From 8498de801eadfe0910076e4a5548c2a2f691cadf Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:21:49 +0900 Subject: [PATCH 28/47] =?UTF-8?q?design:=20#18=20-=20SettingView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/SettingViewController.swift | 138 ++++++++++++++++++ .../UITableView/Cells/SettingMenuCell.swift | 109 ++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 Projects/Features/SettingFeature/Sources/SettingViewController.swift create mode 100644 Projects/Features/SettingFeature/Sources/UITableView/Cells/SettingMenuCell.swift diff --git a/Projects/Features/SettingFeature/Sources/SettingViewController.swift b/Projects/Features/SettingFeature/Sources/SettingViewController.swift new file mode 100644 index 0000000..9a5494b --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/SettingViewController.swift @@ -0,0 +1,138 @@ +// +// SettingViewController.swift +// SettingFeature +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +import RIBs +import RxSwift + +public protocol SettingPresentableListener: AnyObject { + func detachSetting() + func didTapMenu(item: SettingCellItem) +} + +final class SettingViewController: UIViewController, SettingPresentable, SettingViewControllable { + weak var listener: SettingPresentableListener? + + private let navigationBar = NDGLNavigationBar( + title: "설정", + leadingIcon: DSKitAsset.Assets.icChevronLeft3.image + ) + private let tableView = UITableView() + + private let disposeBag = DisposeBag() + + override func viewDidLoad() { + setStyle() + setUI() + setLayout() + setDelegate() + setupActions() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if isMovingFromParent { + listener?.detachSetting() + } + } +} + +private extension SettingViewController { + func setStyle() { + view.backgroundColor = DSKitAsset.Colors.white.color + + tableView.do { + $0.isScrollEnabled = false + $0.backgroundColor = .clear + $0.separatorStyle = .singleLine + $0.separatorColor = DSKitAsset.Colors.black50.color + } + } + + func setUI() { + view.addSubviews(navigationBar, tableView) + } + + func setLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.leading.trailing.equalToSuperview() + } + + tableView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom).offset(24.adjustedH) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + func setDelegate() { + tableView.delegate = self + tableView.dataSource = self + + tableView.register(SettingMenuCell.self, forCellReuseIdentifier: SettingMenuCell.cellIdentifier) + } + + func setupActions() { + navigationBar.leadingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + } +} + +extension SettingViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + SettingSection.allCases.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + SettingSection.allCases[section].items.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let menu = SettingSection.allCases[indexPath.section].items[indexPath.row] + return menu.cellType.cellHeight + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: SettingMenuCell.cellIdentifier, + for: indexPath + ) as? SettingMenuCell else { return UITableViewCell() } + + let menu = SettingSection.allCases[indexPath.section].items[indexPath.row] + let type = menu.cellType + + cell.configure(title: menu.title, type: type) + + if menu == .notification { + cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: tableView.bounds.width) + } else { + cell.separatorInset = UIEdgeInsets(top: 0, left: 25.adjusted, bottom: 0, right: 24.adjusted) + } + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let menu = SettingSection.allCases[indexPath.section].items[indexPath.row] + + switch menu.cellType { + case .toggle, .icon: + listener?.didTapMenu(item: menu) + default: + break + } + } +} diff --git a/Projects/Features/SettingFeature/Sources/UITableView/Cells/SettingMenuCell.swift b/Projects/Features/SettingFeature/Sources/UITableView/Cells/SettingMenuCell.swift new file mode 100644 index 0000000..b573be7 --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/UITableView/Cells/SettingMenuCell.swift @@ -0,0 +1,109 @@ +// +// SettingMenuCell.swift +// SettingFeature +// +// Created by 최안용 on 2/10/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +final class SettingMenuCell: UITableViewCell { + private let titleLabel = UILabel() + private let chevronImageView = UIImageView() + private let toggleSwitch = UISwitch() + private let detailLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setStyle() + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView.frame = contentView.frame.inset( + by: .init( + top: 0, + left: 25.adjusted, + bottom: 0, + right: 24.adjusted + ) + ) + } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + detailLabel.text = nil + } + + func configure(title: String, type: SettingCellType) { + titleLabel.setText(.bodyLR, text: title, color: DSKitAsset.Colors.black700.color) + + [chevronImageView, toggleSwitch, detailLabel].forEach { $0.isHidden = true } + + switch type { + case .toggle(let isOn): + toggleSwitch.isHidden = false + toggleSwitch.isOn = isOn + self.selectionStyle = .none + + case .icon: + chevronImageView.isHidden = false + self.selectionStyle = .gray + + case .detailText(let text): + detailLabel.isHidden = false + detailLabel.setText(.bodyLR, text: text, color: DSKitAsset.Colors.black400.color) + self.selectionStyle = .none + } + } +} + +private extension SettingMenuCell { + func setStyle() { + self.backgroundColor = .clear + + chevronImageView.do { + $0.image = DSKitAsset.Assets.icChevronRight2.image + $0.contentMode = .scaleAspectFit + } + + toggleSwitch.do { + $0.onTintColor = DSKitAsset.Colors.green500.color + $0.thumbTintColor = DSKitAsset.Colors.white.color + } + } + + func setUI() { + contentView.addSubviews(titleLabel, chevronImageView, toggleSwitch, detailLabel) + } + + func setLayout() { + titleLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(8.adjusted) + $0.centerY.equalToSuperview() + } + + chevronImageView.snp.makeConstraints { + $0.size.equalTo(24.adjustedH) + } + + [chevronImageView, toggleSwitch, detailLabel].forEach { view in + view.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(8.adjusted) + $0.centerY.equalToSuperview() + } + } + } +} From a32080b1262519b05c21eab807c873781d530970 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:24:38 +0900 Subject: [PATCH 29/47] =?UTF-8?q?feat:=20#18=20-=20Setting=20RIBs=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/SettingBuilder.swift | 36 +++++++++++ .../Sources/SettingInteractor.swift | 64 +++++++++++++++++++ .../Sources/SettingRouter.swift | 26 ++++++++ 3 files changed, 126 insertions(+) create mode 100644 Projects/Features/SettingFeature/Sources/SettingBuilder.swift create mode 100644 Projects/Features/SettingFeature/Sources/SettingInteractor.swift create mode 100644 Projects/Features/SettingFeature/Sources/SettingRouter.swift diff --git a/Projects/Features/SettingFeature/Sources/SettingBuilder.swift b/Projects/Features/SettingFeature/Sources/SettingBuilder.swift new file mode 100644 index 0000000..98e4360 --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/SettingBuilder.swift @@ -0,0 +1,36 @@ +// +// SettingBuilder.swift +// SettingFeature +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs + +public protocol SettingDependency: Dependency { + +} + +final class SettingComponent: Component { + +} + +public protocol SettingBuildable: Buildable { + func build(withListener listener: SettingListener) -> SettingRouting +} + +public final class SettingBuilder: Builder, SettingBuildable { + + override public init(dependency: SettingDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: SettingListener) -> SettingRouting { + let component = SettingComponent(dependency: dependency) + let viewController = SettingViewController() + let interactor = SettingInteractor(presenter: viewController) + interactor.listener = listener + return SettingRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Projects/Features/SettingFeature/Sources/SettingInteractor.swift b/Projects/Features/SettingFeature/Sources/SettingInteractor.swift new file mode 100644 index 0000000..5a9386a --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/SettingInteractor.swift @@ -0,0 +1,64 @@ +// +// SettingInteractor.swift +// SettingFeature +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs +import RxSwift + +public protocol SettingRouting: ViewableRouting { + +} + +public protocol SettingPresentable: Presentable { + var listener: SettingPresentableListener? { get set } +} + +public protocol SettingListener: AnyObject { + func detachSetting() +} + +final class SettingInteractor: PresentableInteractor, SettingInteractable, SettingPresentableListener { + weak var router: SettingRouting? + weak var listener: SettingListener? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: SettingPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + } + + override func willResignActive() { + super.willResignActive() + } + + func detachSetting() { + // 부모 RIB에게 이 화면을 닫아달라고 알림 + listener?.detachSetting() + } + + func didTapMenu(item: SettingCellItem) { + // 각 메뉴 타이틀에 따른 동작 처리 + switch item { + case .notification: + print("알림") + case .faq: + print("FAQ") + case .recommendLink: + print("추천 링크") + case .identificationCode: + print("내 식별코드") + case .terms: + print("서비스 약관") + default: break + } + } +} diff --git a/Projects/Features/SettingFeature/Sources/SettingRouter.swift b/Projects/Features/SettingFeature/Sources/SettingRouter.swift new file mode 100644 index 0000000..49a0c7a --- /dev/null +++ b/Projects/Features/SettingFeature/Sources/SettingRouter.swift @@ -0,0 +1,26 @@ +// +// SettingRouter.swift +// SettingFeature +// +// Created by 최안용 on 2/9/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs + +protocol SettingInteractable: Interactable { + var router: SettingRouting? { get set } + var listener: SettingListener? { get set } +} + +protocol SettingViewControllable: ViewControllable { + +} + +final class SettingRouter: ViewableRouter, SettingRouting { + + override init(interactor: SettingInteractable, viewController: SettingViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} From ca136158b92ad4c7047d5cb341c9d264b84c97f1 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:25:22 +0900 Subject: [PATCH 30/47] =?UTF-8?q?refactor:=20#18=20-=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=A0=84=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TabBarFeature/Sources/TabBarBuilder.swift | 5 ++ .../Sources/TabBarInteractor.swift | 29 +++++++- .../TabBarFeature/Sources/TabBarRouter.swift | 6 -- .../Sources/TabBarViewController.swift | 68 +++---------------- 4 files changed, 41 insertions(+), 67 deletions(-) diff --git a/Projects/Features/TabBarFeature/Sources/TabBarBuilder.swift b/Projects/Features/TabBarFeature/Sources/TabBarBuilder.swift index 3ddf667..7a3cf85 100644 --- a/Projects/Features/TabBarFeature/Sources/TabBarBuilder.swift +++ b/Projects/Features/TabBarFeature/Sources/TabBarBuilder.swift @@ -15,6 +15,7 @@ import TravelFeature public protocol TabBarDependency: Dependency { var tokenProvider: TokenProviding { get } + var homeUsecase: HomeUsecaseProtocol { get } } // MARK: - TabBarComponent @@ -23,6 +24,10 @@ final class TabBarComponent: Component, HomeDependency, Travel var tokenProvider: TokenProviding { dependency.tokenProvider } + + var homeUsecase: HomeUsecaseProtocol { + dependency.homeUsecase + } } // MARK: - TabBarBuildable diff --git a/Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift b/Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift index e226fa0..e5fdc77 100644 --- a/Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift +++ b/Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift @@ -11,9 +11,10 @@ import HomeFeature import RIBs import RxSwift -// MARK: - TabBarListener +// MARK: - TabBarRouting -public protocol TabBarListener: AnyObject { +public protocol TabBarRouting: ViewableRouting { + func attachTabs() } // MARK: - TabBarPresentable @@ -24,6 +25,15 @@ protocol TabBarPresentable: Presentable { func switchToTab(at index: Int) } +// MARK: - TabBarListener + +public protocol TabBarListener: AnyObject { + func routeToFollow(with recommendationId: Int) + func routeToSetting() + func routeToSearch() + func routeToPopularTravel() +} + // MARK: - TabBarInteractor final class TabBarInteractor: PresentableInteractor, TabBarInteractable { @@ -55,6 +65,21 @@ extension TabBarInteractor: TabBarPresentableListener { // MARK: - HomeListener extension TabBarInteractor: HomeListener { + func homeDidTapPopularTravel() { + listener?.routeToPopularTravel() + } + + func homeDidTapFollowDetail(with recommendationId: Int) { + listener?.routeToFollow(with: recommendationId) + } + + func homeDidTapSearch() { + listener?.routeToSearch() + } + + func homeDidTapSetting() { + listener?.routeToSetting() + } func homeDidAddTrip(title: String, startDate: Date, endDate: Date) { presenter.switchToTab(at: 2) diff --git a/Projects/Features/TabBarFeature/Sources/TabBarRouter.swift b/Projects/Features/TabBarFeature/Sources/TabBarRouter.swift index 095779e..aec5c6d 100644 --- a/Projects/Features/TabBarFeature/Sources/TabBarRouter.swift +++ b/Projects/Features/TabBarFeature/Sources/TabBarRouter.swift @@ -24,12 +24,6 @@ public protocol TabBarViewControllable: ViewControllable { func setViewControllers(_ viewControllers: [ViewControllable]) } -// MARK: - TabBarRouting - -public protocol TabBarRouting: ViewableRouting { - func attachTabs() -} - // MARK: - TabBarRouter final class TabBarRouter: ViewableRouter, TabBarRouting { diff --git a/Projects/Features/TabBarFeature/Sources/TabBarViewController.swift b/Projects/Features/TabBarFeature/Sources/TabBarViewController.swift index 954ed28..2b79f2e 100644 --- a/Projects/Features/TabBarFeature/Sources/TabBarViewController.swift +++ b/Projects/Features/TabBarFeature/Sources/TabBarViewController.swift @@ -13,6 +13,8 @@ import RxSwift import SnapKit import Then +import DSKit + // MARK: - TabBarPresentableListener protocol TabBarPresentableListener: AnyObject { @@ -38,18 +40,15 @@ public final class TabBarViewController: UITabBarController, TabBarPresentable, // MARK: - Lifecycle - public override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() + + self.tabBar.isHidden = true setupStyle() setupUI() setupConstraints() } - - public override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - tabBar.isHidden = true - } - + // MARK: - TabBarViewControllable public func setViewControllers(_ viewControllers: [ViewControllable]) { @@ -66,8 +65,6 @@ public final class TabBarViewController: UITabBarController, TabBarPresentable, let homeNav = UINavigationController(rootViewController: homeVC) let travelNav = UINavigationController(rootViewController: travelVC) - [infoNav, homeNav, travelNav].forEach { $0.delegate = self } - super.setViewControllers([infoNav, homeNav, travelNav], animated: false) setupTabItems() } @@ -75,31 +72,19 @@ public final class TabBarViewController: UITabBarController, TabBarPresentable, func switchToTab(at index: Int) { guard index < tabItems.count else { return } - viewControllers?.forEach { viewController in - if let navController = viewController as? UINavigationController { - navController.popToRootViewController(animated: false) - } - } - updateSelection(at: index) - - DispatchQueue.main.async { - self.customTabBarContainer.isHidden = false - self.customTabBarContainer.alpha = 1 - } } } // MARK: - Setup private extension TabBarViewController { - func setupStyle() { customTabBarContainer.do { if #available(iOS 26.0, *) { let glass = UIGlassEffect(style: .regular) glass.isInteractive = true - glass.tintColor = .white.withAlphaComponent(0.1) + glass.tintColor = DSKitAsset.Colors.white.color.withAlphaComponent(0.1) $0.effect = glass } else { $0.effect = UIBlurEffect(style: .extraLight) @@ -119,7 +104,7 @@ private extension TabBarViewController { if #available(iOS 26.0, *) { let glass = UIGlassEffect(style: .regular) glass.isInteractive = true - glass.tintColor = UIColor(hexCode: "#2C2C2C") + glass.tintColor = DSKitAsset.Colors.black900.color $0.effect = glass } else { $0.effect = UIBlurEffect(style: .dark) @@ -153,7 +138,7 @@ private extension TabBarViewController { $0.size.equalTo(56.adjusted) } } - + func setupTabItems() { tabItems.forEach { $0.removeFromSuperview() } tabItems.removeAll() @@ -206,38 +191,3 @@ private extension TabBarViewController { } } } - -// MARK: - UINavigationControllerDelegate - -extension TabBarViewController: UINavigationControllerDelegate { - - public func navigationController( - _ navigationController: UINavigationController, - willShow viewController: UIViewController, - animated: Bool - ) { - let shouldHideTabBar = navigationController.viewControllers.count > 1 - - guard animated else { - customTabBarContainer.isHidden = shouldHideTabBar - customTabBarContainer.alpha = shouldHideTabBar ? 0 : 1 - return - } - - if shouldHideTabBar { - UIView.animate(withDuration: 0.3) { - self.customTabBarContainer.alpha = 0 - } completion: { _ in - self.customTabBarContainer.isHidden = true - } - } else { - customTabBarContainer.isHidden = false - customTabBarContainer.alpha = 0 - customTabBarContainer.layoutIfNeeded() - - UIView.animate(withDuration: 0.3) { - self.customTabBarContainer.alpha = 1 - } - } - } -} From 1c31756306f766f132a39a71af637ccee9b99ac0 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:25:52 +0900 Subject: [PATCH 31/47] =?UTF-8?q?design:=20#18=20-=20SearchView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/SearchViewController.swift | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 Projects/Features/SearchFeature/Sources/SearchViewController.swift diff --git a/Projects/Features/SearchFeature/Sources/SearchViewController.swift b/Projects/Features/SearchFeature/Sources/SearchViewController.swift new file mode 100644 index 0000000..1c8ce0c --- /dev/null +++ b/Projects/Features/SearchFeature/Sources/SearchViewController.swift @@ -0,0 +1,134 @@ +// +// SearchViewController.swift +// SearchFeature +// +// Created by 최안용 on 2/7/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +import RIBs +import RxSwift + +protocol SearchPresentableListener: AnyObject { + func detachSearch() +} + +final class SearchViewController: UIViewController, SearchPresentable, SearchViewControllable{ + weak var listener: SearchPresentableListener? + + private let searchBar = NDGLSearchBar( + placeholder: "검색어를 입력하세요", + DSKitAsset.Assets.icChevronLeft3.image, + DSKitAsset.Assets.icSearch2.image + ) + private let emptyImageView = UIImageView() + private let titleLabel = UILabel() + private let containerView = UIView() + + private let disposeBag = DisposeBag() + + override func viewDidLoad() { + setStyle() + setUI() + setLayout() + bindKeyboard() + setupActions() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if isMovingFromParent { + listener?.detachSearch() + } + } +} + +private extension SearchViewController { + func setStyle() { + emptyImageView.do { + $0.image = DSKitAsset.Assets.icEmptySearch.image + $0.contentMode = .scaleAspectFit + } + + titleLabel.do { + $0.setText( + .bodyLM, + text: "좋아하는 유튜버나 가고 싶은\n여행지를 검색해봐요", + color: DSKitAsset.Colors.black400.color, + alignment: .center + ) + $0.numberOfLines = 2 + } + } + + func setUI() { + view.addSubviews(searchBar, containerView) + containerView.addSubviews(emptyImageView, titleLabel) + } + + func setLayout() { + searchBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.directionalHorizontalEdges.equalToSuperview() + } + + containerView.snp.makeConstraints { + $0.top.equalTo(searchBar.snp.bottom) + $0.directionalHorizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + + emptyImageView.snp.makeConstraints { + $0.width.equalTo(215.adjusted) + $0.height.equalTo(198.adjustedH) + $0.center.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(emptyImageView.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + } + + func bindKeyboard() { + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .subscribe(onNext: { [weak self] notification in + guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + let keyboardHeight = keyboardFrame.cgRectValue.height + + self?.containerView.snp.updateConstraints { + $0.bottom.equalToSuperview().inset(keyboardHeight) + } + + UIView.animate(withDuration: 0.3) { + self?.view.layoutIfNeeded() + } + }) + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .subscribe(onNext: { [weak self] _ in + self?.containerView.snp.updateConstraints { + $0.bottom.equalToSuperview() + } + + UIView.animate(withDuration: 0.3) { + self?.view.layoutIfNeeded() + } + }) + .disposed(by: disposeBag) + } + + func setupActions() { + searchBar.leadingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + } +} From d73fd164ce6ab1eaee88b404a4363310db653622 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:26:08 +0900 Subject: [PATCH 32/47] =?UTF-8?q?feat:=20#18=20-=20Search=20RIBS=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchFeature/Sources/SearchBuilder.swift | 37 ++++++++++++++ .../Sources/SearchInteractor.swift | 50 +++++++++++++++++++ .../SearchFeature/Sources/SearchRouter.swift | 27 ++++++++++ 3 files changed, 114 insertions(+) create mode 100644 Projects/Features/SearchFeature/Sources/SearchBuilder.swift create mode 100644 Projects/Features/SearchFeature/Sources/SearchInteractor.swift create mode 100644 Projects/Features/SearchFeature/Sources/SearchRouter.swift diff --git a/Projects/Features/SearchFeature/Sources/SearchBuilder.swift b/Projects/Features/SearchFeature/Sources/SearchBuilder.swift new file mode 100644 index 0000000..b91ca9e --- /dev/null +++ b/Projects/Features/SearchFeature/Sources/SearchBuilder.swift @@ -0,0 +1,37 @@ +// +// SearchBuilder.swift +// SearchFeature +// +// Created by 최안용 on 2/7/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs + +public protocol SearchDependency: Dependency { + +} + +final class SearchComponent: Component { + +} + +public protocol SearchBuildable: Buildable { + func build(withListener listener: SearchListener) -> SearchRouting +} + +public final class SearchBuilder: Builder, SearchBuildable { + + override public init(dependency: SearchDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: SearchListener) -> SearchRouting { + let component = SearchComponent(dependency: dependency) + let viewController = SearchViewController() + let interactor = SearchInteractor(presenter: viewController) + interactor.listener = listener + + return SearchRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Projects/Features/SearchFeature/Sources/SearchInteractor.swift b/Projects/Features/SearchFeature/Sources/SearchInteractor.swift new file mode 100644 index 0000000..2920ae4 --- /dev/null +++ b/Projects/Features/SearchFeature/Sources/SearchInteractor.swift @@ -0,0 +1,50 @@ +// +// SearchInteractor.swift +// SearchFeature +// +// Created by 최안용 on 2/7/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs +import RxSwift + +public protocol SearchRouting: ViewableRouting { + +} + +protocol SearchPresentable: Presentable { + var listener: SearchPresentableListener? { get set } + // TODO: Declare methods the interactor can invoke the presenter to present data. +} + +public protocol SearchListener: AnyObject { + func detachSearch() +} + +final class SearchInteractor: PresentableInteractor, SearchInteractable, SearchPresentableListener { + + weak var router: SearchRouting? + weak var listener: SearchListener? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: SearchPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + // TODO: Implement business logic here. + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } + + func detachSearch() { + listener?.detachSearch() + } +} diff --git a/Projects/Features/SearchFeature/Sources/SearchRouter.swift b/Projects/Features/SearchFeature/Sources/SearchRouter.swift new file mode 100644 index 0000000..f6860c5 --- /dev/null +++ b/Projects/Features/SearchFeature/Sources/SearchRouter.swift @@ -0,0 +1,27 @@ +// +// SearchRouter.swift +// SearchFeature +// +// Created by 최안용 on 2/7/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs + +protocol SearchInteractable: Interactable { + var router: SearchRouting? { get set } + var listener: SearchListener? { get set } +} + +protocol SearchViewControllable: ViewControllable { + +} + +final class SearchRouter: ViewableRouter, SearchRouting { + + // TODO: Constructor inject child builder protocols to allow building children. + override init(interactor: SearchInteractable, viewController: SearchViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} From b6324e894f419f284e1c2fad19f210b4f114da67 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:27:14 +0900 Subject: [PATCH 33/47] =?UTF-8?q?refactor:=20#18=20-=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=A0=84=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Features/RootFeature/Project.swift | 2 +- .../RootFeature/Sources/RootBuilder.swift | 16 ++++++--- .../RootFeature/Sources/RootInteractor.swift | 13 +++++-- .../RootFeature/Sources/RootRouter.swift | 36 ++++++++----------- 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift index aec99fa..9595aed 100644 --- a/Projects/Features/RootFeature/Project.swift +++ b/Projects/Features/RootFeature/Project.swift @@ -15,7 +15,7 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "RootFeature", dependencies: [ - .Features.TabBar.feature + .Features.Main.feature ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/RootFeature/Sources/RootBuilder.swift b/Projects/Features/RootFeature/Sources/RootBuilder.swift index 1bdfba0..4246664 100644 --- a/Projects/Features/RootFeature/Sources/RootBuilder.swift +++ b/Projects/Features/RootFeature/Sources/RootBuilder.swift @@ -7,21 +7,27 @@ // import Domain +import MainFeature + import RIBs -import TabBarFeature // MARK: - RootDependency public protocol RootDependency: Dependency { var tokenProvider: TokenProviding { get } + var homeUsecase: HomeUsecaseProtocol { get } } // MARK: - RootComponent -final class RootComponent: Component, TabBarDependency { +final class RootComponent: Component, MainDependency { var tokenProvider: TokenProviding { dependency.tokenProvider } + + var homeUsecase: HomeUsecaseProtocol { + dependency.homeUsecase + } } // MARK: - RootBuildable @@ -34,7 +40,7 @@ public protocol RootBuildable: Buildable { public final class RootBuilder: Builder, RootBuildable { - public override init(dependency: RootDependency) { + override public init(dependency: RootDependency) { super.init(dependency: dependency) } @@ -43,12 +49,12 @@ public final class RootBuilder: Builder, RootBuildable { let viewController = RootViewController() let interactor = RootInteractor(presenter: viewController) - let tabBarBuilder = TabBarBuilder(dependency: component) + let mainBuilder = MainBuilder(dependency: component) let router = RootRouter( interactor: interactor, viewController: viewController, - tabBarBuilder: tabBarBuilder + mainBuilder: mainBuilder ) return router diff --git a/Projects/Features/RootFeature/Sources/RootInteractor.swift b/Projects/Features/RootFeature/Sources/RootInteractor.swift index 7347e0d..94d061e 100644 --- a/Projects/Features/RootFeature/Sources/RootInteractor.swift +++ b/Projects/Features/RootFeature/Sources/RootInteractor.swift @@ -9,10 +9,11 @@ import RIBs import RxSwift -// MARK: - RootListener +// MARK: - RootRouting -public protocol RootListener: AnyObject { - // Root는 최상위 RIB이므로 Listener가 없음 +public protocol RootRouting: ViewableRouting { + func attachMain() + func detachMain() } // MARK: - RootPresentable @@ -21,6 +22,12 @@ protocol RootPresentable: Presentable { var listener: RootPresentableListener? { get set } } +// MARK: - RootListener + +public protocol RootListener: AnyObject { + // Root는 최상위 RIB이므로 Listener가 없음 +} + // MARK: - RootInteractor final class RootInteractor: PresentableInteractor, RootInteractable { diff --git a/Projects/Features/RootFeature/Sources/RootRouter.swift b/Projects/Features/RootFeature/Sources/RootRouter.swift index a6efb28..d1dca91 100644 --- a/Projects/Features/RootFeature/Sources/RootRouter.swift +++ b/Projects/Features/RootFeature/Sources/RootRouter.swift @@ -8,11 +8,11 @@ import RIBs -import TabBarFeature +import MainFeature // MARK: - RootInteractable -protocol RootInteractable: Interactable, TabBarListener { +protocol RootInteractable: Interactable, MainListener { var router: RootRouting? { get set } var listener: RootListener? { get set } } @@ -25,50 +25,42 @@ public protocol RootViewControllable: ViewControllable { func setRootViewController(_ viewController: ViewControllable) } -// MARK: - RootRouting - -public protocol RootRouting: ViewableRouting { - func attachTabBar() - func detachTabBar() -} - // MARK: - RootRouter final class RootRouter: LaunchRouter, RootRouting { - - private let tabBarBuilder: TabBarBuildable - private var tabBarRouter: TabBarRouting? + private let mainBuilder: MainBuildable + private var mainRouter: MainRouting? init( interactor: RootInteractable, viewController: RootViewControllable, - tabBarBuilder: TabBarBuildable + mainBuilder: MainBuildable ) { - self.tabBarBuilder = tabBarBuilder + self.mainBuilder = mainBuilder super.init(interactor: interactor, viewController: viewController) interactor.router = self } override func didLoad() { super.didLoad() - attachTabBar() + attachMain() } // MARK: - RootRouting - func attachTabBar() { - guard tabBarRouter == nil else { return } + func attachMain() { + guard mainRouter == nil else { return } - let router = tabBarBuilder.build(withListener: interactor) - tabBarRouter = router + let router = mainBuilder.build(withListener: interactor) + mainRouter = router attachChild(router) viewController.setRootViewController(router.viewControllable) } - func detachTabBar() { - guard let router = tabBarRouter else { return } + func detachMain() { + guard let router = mainRouter else { return } detachChild(router) - tabBarRouter = nil + mainRouter = nil } } From 9ad4d992d98f2c383d0332e2cccae4bcc79df56e Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:28:13 +0900 Subject: [PATCH 34/47] =?UTF-8?q?feat:=20#18=20-=20PopularTravel=20Composi?= =?UTF-8?q?tionalLayout=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopularTravelPresentationModel.swift | 56 ++++++++++ .../PopularTravelCompositionalLayout.swift | 104 ++++++++++++++++++ .../Item/PopularTravelItem.swift | 14 +++ .../PopularTravelRegistration.swift | 41 +++++++ .../PopularTravelSectionKind.swift | 14 +++ 5 files changed, 229 insertions(+) create mode 100644 Projects/Features/PopularTravelFeature/Sources/Models/PopularTravelPresentationModel.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/CompositionalLayout/PopularTravelCompositionalLayout.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Item/PopularTravelItem.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Registration/PopularTravelRegistration.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/SectionKind/PopularTravelSectionKind.swift diff --git a/Projects/Features/PopularTravelFeature/Sources/Models/PopularTravelPresentationModel.swift b/Projects/Features/PopularTravelFeature/Sources/Models/PopularTravelPresentationModel.swift new file mode 100644 index 0000000..efadaec --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/Models/PopularTravelPresentationModel.swift @@ -0,0 +1,56 @@ +// +// PopularTravelPresentationModel.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain + +struct PopularTravelPresentationModel { + let category: [PopularTravelPresentationModel.Category] + let popularTrip: [PopularTravelPresentationModel.PopularTrip] + + struct Category: Hashable { + let id: Int + let creator: String + let viedoType: VideoType + } + + struct PopularTrip: Hashable { + let id: String + let title: String + let thumbnailUrl: String + let creator: String + let schedule: String + let country: String + let city: String + } +} + +extension TripCategory { + func toPopularTravelModel() -> PopularTravelPresentationModel.Category { + return PopularTravelPresentationModel.Category( + id: self.id, + creator: self.creator, + viedoType: self.viedoType + ) + } +} + +extension TripInfo { + func toPopularTravelModel() -> PopularTravelPresentationModel.PopularTrip { + return PopularTravelPresentationModel.PopularTrip( + id: self.id, + title: self.title, + thumbnailUrl: self.thumbnailUrl, + creator: self.creator, + schedule: "\(self.nights)박 \(self.days)일", + country: self.country, + city: self.city + ) + } +} diff --git a/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/CompositionalLayout/PopularTravelCompositionalLayout.swift b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/CompositionalLayout/PopularTravelCompositionalLayout.swift new file mode 100644 index 0000000..265b55d --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/CompositionalLayout/PopularTravelCompositionalLayout.swift @@ -0,0 +1,104 @@ +// +// PopularTravelCompositionalLayout.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +extension PopularTravelViewController { + func createLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in + guard let sectionKind = PopularTravelSectionKind(rawValue: sectionIndex) else { + return self?.emptyLayout() + } + + switch sectionKind { + case .category: + return self?.createCategorySection() + case .popularTrip: + return self?.createPopularTripSection() + } + } + } +} + +private extension PopularTravelViewController { + func createCategorySection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(72), + heightDimension: .absolute(CategoryChipCell.defaultHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .estimated(72), + heightDimension: .absolute(CategoryChipCell.defaultHeight) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.interGroupSpacing = 8.adjusted + section.contentInsets = .init( + top: 20.adjustedH, + leading: 24.adjusted, + bottom: 32.adjustedH, + trailing: 24.adjusted + ) + return section + } + + func createPopularTripSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(PopularInfoCell.defaultHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(PopularInfoCell.defaultHeight) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 16.adjustedH + + section.contentInsets = .init( + top: 0, + leading: 24.adjusted, + bottom: 12.adjustedH, + trailing: 24.adjusted + ) + section.orthogonalScrollingBehavior = .none + + return section + } + + + func emptyLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0) + ) + let group = NSCollectionLayoutGroup(layoutSize: groupSize) + + let section = NSCollectionLayoutSection(group: group) + + return section + } +} diff --git a/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Item/PopularTravelItem.swift b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Item/PopularTravelItem.swift new file mode 100644 index 0000000..ba7156d --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Item/PopularTravelItem.swift @@ -0,0 +1,14 @@ +// +// PopularTravelItem.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum PopularTravelItem: Hashable { + case category(PopularTravelPresentationModel.Category, isSelected: Bool) + case popularTrip(PopularTravelPresentationModel.PopularTrip) +} diff --git a/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Registration/PopularTravelRegistration.swift b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Registration/PopularTravelRegistration.swift new file mode 100644 index 0000000..28104c8 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/Registration/PopularTravelRegistration.swift @@ -0,0 +1,41 @@ +// +// PopularTravelRegistration.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +extension PopularTravelViewController { + func createCategoryCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + let (chip, isSelected) = item + + let chipType: ChipIconType = { + switch chip.viedoType { + case .tv: ChipIconType.tv + case .youtube: ChipIconType.youtube + case .none: ChipIconType.none + } + }() + + cell.configure(chipType, chip.creator, isSelected) + } + } + + func createPopularTripCellRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.configure( + thumbnailUrl: item.thumbnailUrl, + city: item.city, + title: item.title, + nation: item.country, + schedule: item.schedule + ) + } + } +} diff --git a/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/SectionKind/PopularTravelSectionKind.swift b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/SectionKind/PopularTravelSectionKind.swift new file mode 100644 index 0000000..fc94819 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/Views/CollectionView/SectionKind/PopularTravelSectionKind.swift @@ -0,0 +1,14 @@ +// +// PopularTravelSectionKind.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum PopularTravelSectionKind: Int, CaseIterable { + case category + case popularTrip +} From c3e3bcdf8241cf86dc3247e5ab102ddd3b4c9673 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:28:37 +0900 Subject: [PATCH 35/47] =?UTF-8?q?design:=20#18=20-=20PopularTravelView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/PopularTravelViewController.swift | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift new file mode 100644 index 0000000..524f5d2 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift @@ -0,0 +1,226 @@ +// +// PopularTravelViewController.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +import RIBs +import RxCocoa +import RxSwift + +protocol PopularTravelPresentableListener: AnyObject { + func detachPopularTravel() + func searchBtnTapped() + func itemSelected(item: PopularTravelItem) + func reloadBtnTapped() +} + +final class PopularTravelViewController: UIViewController, PopularTravelViewControllable { + + weak var listener: PopularTravelPresentableListener? + + private let disposeBag = DisposeBag() + + private let navigationBar = NDGLNavigationBar( + style: .white, + title: "인기 여행 따라가기", + leadingIcon: DSKitAsset.Assets.icChevronLeft3.image, + trailingIcon: DSKitAsset.Assets.icSearch2.image + ) + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) + private let loadingIndicator = UIActivityIndicatorView(style: .medium) + private let networkErrorView = NDGLErrorView() + + private var dataSource: UICollectionViewDiffableDataSource? + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + setStyle() + setUI() + setLayout() + + setCollectionView() + setDataSource() + bindInteractor() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if isMovingFromParent { + listener?.detachPopularTravel() + } + } +} + +private extension PopularTravelViewController { + func setStyle() { + view.backgroundColor = DSKitAsset.Colors.white.color + + collectionView.do { + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.backgroundColor = .clear + $0.contentInset = .zero + $0.isScrollEnabled = true + } + + loadingIndicator.do { + $0.color = DSKitAsset.Colors.green300.color + } + + networkErrorView.do { + $0.isHidden = true + } + } + + func setUI() { + view.addSubviews(collectionView, navigationBar, loadingIndicator, networkErrorView) + } + + func setLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.directionalHorizontalEdges.equalToSuperview() + } + + collectionView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.bottom.equalToSuperview() + $0.directionalHorizontalEdges.equalToSuperview() + } + + loadingIndicator.snp.makeConstraints { + $0.center.equalToSuperview() + } + + networkErrorView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.directionalHorizontalEdges.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16.adjustedH) + } + } + + func setCollectionView() { + collectionView.do { + $0.register( + CategoryChipCell.self, + forCellWithReuseIdentifier: CategoryChipCell.cellIdentifier + ) + + $0.register( + PopularInfoCell.self, + forCellWithReuseIdentifier: PopularInfoCell.cellIdentifier + ) + } + } +} + +private extension PopularTravelViewController { + func bindInteractor() { + navigationBar.leadingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + + navigationBar.trailingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.searchBtnTapped() + } + .disposed(by: disposeBag) + + navigationBar.trailingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.searchBtnTapped() + } + .disposed(by: disposeBag) + + collectionView.rx.itemSelected + .compactMap { [weak self] indexPath in + self?.dataSource?.itemIdentifier(for: indexPath) + } + .subscribe(with: self) { owner, item in + owner.listener?.itemSelected(item: item) + } + .disposed(by: disposeBag) + + networkErrorView.buttonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.reloadBtnTapped() + } + .disposed(by: disposeBag) + } + + func applySnapshot(with sections: [PopularTravelSectionModel]) { + var snapshot = NSDiffableDataSourceSnapshot() + sections.forEach { + snapshot.appendSections([$0.section]) + snapshot.appendItems($0.items, toSection: $0.section) + } + dataSource?.apply(snapshot, animatingDifferences: true) + } + + func setDataSource() { + let categoryRegistration = createCategoryCellRegistration() + let popularTripRegistration = createPopularTripCellRegistration() + + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in + switch item { + case .category(let category): + return collectionView.dequeueConfiguredReusableCell( + using: categoryRegistration, + for: indexPath, + item: category + ) + case .popularTrip(let tripList): + return collectionView.dequeueConfiguredReusableCell( + using: popularTripRegistration, + for: indexPath, + item: tripList + ) + } + } + } +} + +extension PopularTravelViewController: PopularTravelPresentable { + func update(with sections: [PopularTravelSectionModel]) { + applySnapshot(with: sections) + } + + func setLoading(_ isLoading: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + if isLoading { + self.loadingIndicator.startAnimating() + self.collectionView.alpha = 0.5 + } else { + self.loadingIndicator.stopAnimating() + self.collectionView.alpha = 1.0 + } + } + } + + func showErrorView(_ isError: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + self.networkErrorView.isHidden = !isError + + self.collectionView.isHidden = isError + if isError { + self.loadingIndicator.stopAnimating() + } + } + } +} From d4fd2bdcb4037f198ea5602429011b780858732d Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:29:06 +0900 Subject: [PATCH 36/47] =?UTF-8?q?feat:=20#18=20-=20PopularTravel=20RIBs=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/PopularTravelBuilder.swift | 45 ++++++ .../Sources/PopularTravelInteractor.swift | 152 ++++++++++++++++++ .../Sources/PopularTravelRouter.swift | 26 +++ 3 files changed, 223 insertions(+) create mode 100644 Projects/Features/PopularTravelFeature/Sources/PopularTravelBuilder.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift create mode 100644 Projects/Features/PopularTravelFeature/Sources/PopularTravelRouter.swift diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelBuilder.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelBuilder.swift new file mode 100644 index 0000000..f148d3d --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelBuilder.swift @@ -0,0 +1,45 @@ +// +// PopularTravelBuilder.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain + +import RIBs + +public protocol PopularTravelDependency: Dependency { + var homeUsecase: HomeUsecaseProtocol { get } +} + +final class PopularTravelComponent: Component { + var homeUsecase: HomeUsecaseProtocol { + return dependency.homeUsecase + } +} + +// MARK: - Builder + +public protocol PopularTravelBuildable: Buildable { + func build(withListener listener: PopularTravelListener) -> PopularTravelRouting +} + +public final class PopularTravelBuilder: Builder, PopularTravelBuildable { + + override public init(dependency: PopularTravelDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: PopularTravelListener) -> PopularTravelRouting { + let component = PopularTravelComponent(dependency: dependency) + let viewController = PopularTravelViewController() + let interactor = PopularTravelInteractor( + presenter: viewController, + usecase: component.homeUsecase + ) + interactor.listener = listener + return PopularTravelRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift new file mode 100644 index 0000000..e878541 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift @@ -0,0 +1,152 @@ +// +// PopularTravelInteractor.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain + +import RIBs +import RxCocoa +import RxRelay +import RxSwift + +struct PopularTravelSectionModel { + let section: PopularTravelSectionKind + let items: [PopularTravelItem] +} + +public protocol PopularTravelRouting: ViewableRouting { + +} + +protocol PopularTravelPresentable: Presentable { + var listener: PopularTravelPresentableListener? { get set } + + func update(with sections: [PopularTravelSectionModel]) + func setLoading(_ isLoading: Bool) + func showErrorView(_ isError: Bool) +} + +public protocol PopularTravelListener: AnyObject { + func popularTravelDidTapFollowDetail(with recommendationId: Int) + func popularTravelDidTapSearch() + func detachPopularTravel() +} + +final class PopularTravelInteractor: PresentableInteractor, PopularTravelInteractable { + + weak var router: PopularTravelRouting? + weak var listener: PopularTravelListener? + + private var fetchDataTask: Task? + private let usecase: HomeUsecaseProtocol + private let disposeBag = DisposeBag() + + private let popularTravelDataRelay = BehaviorRelay(value: nil) + private let selectedCategoryRelay = BehaviorRelay(value: nil) + + init(presenter: PopularTravelPresentable, usecase: HomeUsecaseProtocol) { + self.usecase = usecase + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + setupStream() + fetchData() + } + + override func willResignActive() { + super.willResignActive() + + fetchDataTask?.cancel() + fetchDataTask = nil + } + + private func setupStream() { + Observable.combineLatest( + popularTravelDataRelay.compactMap { $0 }, + selectedCategoryRelay + ) + .map { model, selectedId -> [PopularTravelSectionModel] in + return [ + .init(section: .category, items: model.category.map { + .category($0, isSelected: $0.id == selectedId) + }), + .init(section: .popularTrip, items: model.popularTrip.map { .popularTrip($0) }) + ] + } + .subscribe(with: self) { owner, sections in + owner.presenter.update(with: sections) + } + .disposed(by: disposeBag) + + popularTravelDataRelay + .map { $0 == nil } + .subscribe(with: self) { owner, isLoading in + owner.presenter.setLoading(isLoading) + } + .disposed(by: disposeBag) + } + + private func fetchData() { + fetchDataTask?.cancel() + + presenter.setLoading(true) + presenter.showErrorView(false) + + fetchDataTask = Task { + do { + guard !Task.isCancelled else { return } + + async let categories = self.usecase.fetchCategoryList().map { $0.toPopularTravelModel() } + async let populars = self.usecase.fetchPopularTripList().map { $0.toPopularTravelModel() } + + let model = try await PopularTravelPresentationModel( + category: categories, + popularTrip: populars + ) + + if self.selectedCategoryRelay.value == nil, let firstId = model.category.first?.id { + self.selectedCategoryRelay.accept(firstId) + } + + popularTravelDataRelay.accept(model) + presenter.setLoading(false) + } catch let error { + presenter.setLoading(false) + presenter.showErrorView(true) + } + + fetchDataTask = nil + } + } +} + +extension PopularTravelInteractor: PopularTravelPresentableListener { + func detachPopularTravel() { + listener?.detachPopularTravel() + } + + func searchBtnTapped() { + listener?.popularTravelDidTapSearch() + } + + func itemSelected(item: PopularTravelItem) { + switch item { + case .category(let category, _): + selectedCategoryRelay.accept(category.id) + case .popularTrip(let trip): + listener?.popularTravelDidTapFollowDetail(with: Int(trip.id) ?? 0) + } + } + + func reloadBtnTapped() { + fetchData() + } +} diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelRouter.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelRouter.swift new file mode 100644 index 0000000..8229ee8 --- /dev/null +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelRouter.swift @@ -0,0 +1,26 @@ +// +// PopularTravelRouter.swift +// PopularTravelFeature +// +// Created by 최안용 on 2/12/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import RIBs + +protocol PopularTravelInteractable: Interactable { + var router: PopularTravelRouting? { get set } + var listener: PopularTravelListener? { get set } +} + +protocol PopularTravelViewControllable: ViewControllable { + +} + +final class PopularTravelRouter: ViewableRouter, PopularTravelRouting { + + override init(interactor: PopularTravelInteractable, viewController: PopularTravelViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} From 1e5c07978d1ccc61bfa0d2655a380fddfda06514 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:30:50 +0900 Subject: [PATCH 37/47] =?UTF-8?q?feat:=20#18=20-=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EB=8B=B4=EB=8B=B9=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainFeature/Sources/MainBuilder.swift | 76 +++++++++ .../MainFeature/Sources/MainInteractor.swift | 94 +++++++++++ .../MainFeature/Sources/MainRouter.swift | 151 ++++++++++++++++++ .../Sources/MainViewController.swift | 67 ++++++++ 4 files changed, 388 insertions(+) create mode 100644 Projects/Features/MainFeature/Sources/MainBuilder.swift create mode 100644 Projects/Features/MainFeature/Sources/MainInteractor.swift create mode 100644 Projects/Features/MainFeature/Sources/MainRouter.swift create mode 100644 Projects/Features/MainFeature/Sources/MainViewController.swift diff --git a/Projects/Features/MainFeature/Sources/MainBuilder.swift b/Projects/Features/MainFeature/Sources/MainBuilder.swift new file mode 100644 index 0000000..fce1da2 --- /dev/null +++ b/Projects/Features/MainFeature/Sources/MainBuilder.swift @@ -0,0 +1,76 @@ +// +// MainBuilder.swift +// MainFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Data +import FollowFeature +import PopularTravelFeature +import SettingFeature +import SearchFeature +import TabBarFeature + +import RIBs + +public protocol MainDependency: Dependency { + var homeUsecase: HomeUsecaseProtocol { get } + var tokenProvider: TokenProviding { get } +} + +final class MainComponent: Component, FollowDetailDependency, PopularTravelDependency,SearchDependency, SettingDependency, TabBarDependency { + var followService: FollowServiceProtocol { + makeFollowService() + } + + var travelService: TravelServiceProtocol { + makeTravelService(tokenProvider: tokenProvider) + } + + var tokenProvider: TokenProviding { + dependency.tokenProvider + } + + var homeUsecase: HomeUsecaseProtocol { + dependency.homeUsecase + } +} + +// MARK: - Builder + +public protocol MainBuildable: Buildable { + func build(withListener listener: MainListener) -> MainRouting +} + +public final class MainBuilder: Builder, MainBuildable { + + override public init(dependency: MainDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: MainListener) -> MainRouting { + let component = MainComponent(dependency: dependency) + let viewController = MainViewController() + let interactor = MainInteractor(presenter: viewController) + interactor.listener = listener + + let followBuilder = FollowDetailBuilder(dependency: component) + let popularTravelBuilder = PopularTravelBuilder(dependency: component) + let searchBuilder = SearchBuilder(dependency: component) + let settingBuilder = SettingBuilder(dependency: component) + let tabBarBuilder = TabBarBuilder(dependency: component) + + return MainRouter( + interactor: interactor, + viewController: viewController, + followBuilder: followBuilder, + popularTravelBuilder: popularTravelBuilder, + searchBuilder: searchBuilder, + settingBuilder: settingBuilder, + tabBarBuilder: tabBarBuilder + ) + } +} diff --git a/Projects/Features/MainFeature/Sources/MainInteractor.swift b/Projects/Features/MainFeature/Sources/MainInteractor.swift new file mode 100644 index 0000000..bad8191 --- /dev/null +++ b/Projects/Features/MainFeature/Sources/MainInteractor.swift @@ -0,0 +1,94 @@ +// +// MainInteractor.swift +// MainFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import RIBs +import RxSwift + +public protocol MainRouting: ViewableRouting { + func attachFollow(with recommendationId: Int) + func detachFollow() + func attachPopularTravel() + func detachPopularTravel() + func attachSearch() + func detachSearch() + func attachSetting() + func detachSetting() + func attachTabBar() +} + +protocol MainPresentable: Presentable { + var listener: MainPresentableListener? { get set } +} + +public protocol MainListener: AnyObject { } + +final class MainInteractor: PresentableInteractor, MainInteractable, MainPresentableListener { + weak var router: MainRouting? + weak var listener: MainListener? + + override init(presenter: MainPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + router?.attachTabBar() + } + + override func willResignActive() { + super.willResignActive() + } + + func followDetailDidTapClose() { + router?.detachFollow() + } + + func followDetailDidAddTrip(title: String, startDate: Date, endDate: Date) { + // 이건 뭐임 + } + + func popularTravelDidTapFollowDetail(with recommendationId: Int) { + router?.attachFollow(with: recommendationId) + } + + func popularTravelDidTapSearch() { + router?.attachSearch() + } + + func routeToPopularTravel() { + router?.attachPopularTravel() + } + + func detachPopularTravel() { + router?.detachPopularTravel() + } + + func detachSearch() { + router?.detachSearch() + } + + func detachSetting() { + router?.detachSetting() + } + + func routeToFollow(with recommendationId: Int) { + router?.attachFollow(with: recommendationId) + } + + func routeToSetting() { + router?.attachSetting() + } + + func routeToSearch() { + router?.attachSearch() + } +} diff --git a/Projects/Features/MainFeature/Sources/MainRouter.swift b/Projects/Features/MainFeature/Sources/MainRouter.swift new file mode 100644 index 0000000..70ec49a --- /dev/null +++ b/Projects/Features/MainFeature/Sources/MainRouter.swift @@ -0,0 +1,151 @@ +// +// MainRouter.swift +// MainFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import FollowFeature +import PopularTravelFeature +import SearchFeature +import SettingFeature +import TabBarFeature + +import RIBs + +protocol MainInteractable: Interactable, FollowDetailListener, PopularTravelListener, SearchListener, SettingListener, TabBarListener { + var router: MainRouting? { get set } + var listener: MainListener? { get set } +} + +public protocol MainViewControllable: ViewControllable { + func setViewControllers(_ viewControllables: [ViewControllable]) + func pushViewController(_ viewControllable: ViewControllable, animated: Bool) + func popRootViewController(animated: Bool) + func containsInStack(_ viewControllable: ViewControllable) -> Bool +} + +final class MainRouter: ViewableRouter, MainRouting { + private let followBuilder: FollowDetailBuildable + private var followRouter: FollowDetailRouting? + + private let popularTravelBuilder: PopularTravelBuildable + private var popularTravelRouter: PopularTravelRouting? + + private let searchBuilder: SearchBuildable + private var searchRouter: SearchRouting? + + private let settingBuilder: SettingBuildable + private var settingRouter: SettingRouting? + + private let tabBarBuilder: TabBarBuildable + private var tabBarRouter: TabBarRouting? + + init( + interactor: MainInteractable, + viewController: MainViewControllable, + followBuilder: FollowDetailBuildable, + popularTravelBuilder: PopularTravelBuildable, + searchBuilder: SearchBuildable, + settingBuilder: SettingBuildable, + tabBarBuilder: TabBarBuildable + ) { + self.followBuilder = followBuilder + self.popularTravelBuilder = popularTravelBuilder + self.searchBuilder = searchBuilder + self.settingBuilder = settingBuilder + self.tabBarBuilder = tabBarBuilder + + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + func attachFollow(with recommendationId: Int) { + guard followRouter == nil else { return } + let router = followBuilder.build( + withListener: interactor, + recommendationId: recommendationId + ) + self.followRouter = router + attachChild(router) + viewController.pushViewController(router.viewControllable, animated: true) + } + + func detachFollow() { + guard let router = followRouter else { return } + + if viewController.containsInStack(router.viewControllable) { + viewController.popRootViewController(animated: true) + } + + detachChild(router) + self.followRouter = nil + } + + func attachPopularTravel() { + guard popularTravelRouter == nil else { return } + let router = popularTravelBuilder.build(withListener: interactor) + self.popularTravelRouter = router + attachChild(router) + viewController.pushViewController(router.viewControllable, animated: true) + } + + func detachPopularTravel() { + guard let router = popularTravelRouter else { return } + + if viewController.containsInStack(router.viewControllable) { + viewController.popRootViewController(animated: true) + } + + detachChild(router) + self.popularTravelRouter = nil + } + + func attachSearch() { + guard searchRouter == nil else { return } + let router = searchBuilder.build(withListener: interactor) + self.searchRouter = router + attachChild(router) + viewController.pushViewController(router.viewControllable, animated: true) + } + + func detachSearch() { + guard let router = searchRouter else { return } + + if viewController.containsInStack(router.viewControllable) { + viewController.popRootViewController(animated: true) + } + + detachChild(router) + self.searchRouter = nil + } + + func attachSetting() { + guard settingRouter == nil else { return } + let router = settingBuilder.build(withListener: interactor) + self.settingRouter = router + attachChild(router) + viewController.pushViewController(router.viewControllable, animated: true) + } + + func detachSetting() { + guard let router = settingRouter else { return } + + if viewController.containsInStack(router.viewControllable) { + viewController.popRootViewController(animated: true) + } + + detachChild(router) + self.settingRouter = nil + } + + func attachTabBar() { + guard tabBarRouter == nil else { return } + let router = tabBarBuilder.build(withListener: interactor) + self.tabBarRouter = router + attachChild(router) + + viewController.setViewControllers([router.viewControllable]) + } +} diff --git a/Projects/Features/MainFeature/Sources/MainViewController.swift b/Projects/Features/MainFeature/Sources/MainViewController.swift new file mode 100644 index 0000000..4f870d7 --- /dev/null +++ b/Projects/Features/MainFeature/Sources/MainViewController.swift @@ -0,0 +1,67 @@ +// +// MainViewController.swift +// MainFeature +// +// Created by 최안용 on 2/11/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import UIKit + +import DSKit + +import RIBs + +protocol MainPresentableListener: AnyObject { + +} + +final class MainViewController: UINavigationController, MainPresentable, MainViewControllable { + weak var listener: MainPresentableListener? + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.setNavigationBarHidden(true, animated: animated) + } + + override func viewDidLoad() { + super.viewDidLoad() + + setStyle() + setupDelegate() + } + + func setViewControllers(_ viewControllables: [ViewControllable]) { + let viewControllers = viewControllables.map { $0.uiviewController } + self.setViewControllers(viewControllers, animated: false) + } + + func pushViewController(_ viewControllable: ViewControllable, animated: Bool) { + self.pushViewController(viewControllable.uiviewController, animated: animated) + } + + func popRootViewController(animated: Bool) { + self.popViewController(animated: animated) + } + + func containsInStack(_ viewControllable: ViewControllable) -> Bool { + self.viewControllers.contains(viewControllable.uiviewController) + } +} + +private extension MainViewController { + func setStyle() { + self.view.backgroundColor = DSKitAsset.Colors.white.color + } +} + +extension MainViewController: UIGestureRecognizerDelegate { + private func setupDelegate() { + self.interactivePopGestureRecognizer?.delegate = self + } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + viewControllers.count > 1 + } +} From d80fbe8b5b900500695cd9dcdef59553f7e2b1b9 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:31:26 +0900 Subject: [PATCH 38/47] =?UTF-8?q?refactor:=20#18=20-=20Home=20=EB=A6=AC?= =?UTF-8?q?=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Application/AppComponent.swift | 7 +- .../HomeFeature/Sources/HomeBuilder.swift | 34 +- .../HomeFeature/Sources/HomeInteractor.swift | 229 ++++----- .../HomeFeature/Sources/HomeRouter.swift | 50 +- .../Sources/HomeViewController.swift | 446 ++++++++++-------- .../Models/HomePresentationModel.swift | 6 +- .../HomeCompositionalLayout.swift | 2 +- 7 files changed, 381 insertions(+), 393 deletions(-) diff --git a/Projects/App/Sources/Application/AppComponent.swift b/Projects/App/Sources/Application/AppComponent.swift index 1944d6e..41d4beb 100644 --- a/Projects/App/Sources/Application/AppComponent.swift +++ b/Projects/App/Sources/Application/AppComponent.swift @@ -8,10 +8,15 @@ import Data import Domain -import RIBs import RootFeature +import RIBs + final class AppComponent: Component, RootDependency { + var homeUsecase: HomeUsecaseProtocol { + let homeRepository = HomeRepository(homeService: makeHomeService(tokenProvider: tokenProvider)) + return HomeUsecase(repository: homeRepository) + } var tokenProvider: TokenProviding { shared { TokenRepositoryFactory.makeTokenProvider() } diff --git a/Projects/Features/HomeFeature/Sources/HomeBuilder.swift b/Projects/Features/HomeFeature/Sources/HomeBuilder.swift index 91b90dc..e59cab2 100644 --- a/Projects/Features/HomeFeature/Sources/HomeBuilder.swift +++ b/Projects/Features/HomeFeature/Sources/HomeBuilder.swift @@ -6,31 +6,24 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Data import Domain -import FollowFeature + +// TODO: - 지워야됨 +import Data import RIBs // MARK: - HomeDependency public protocol HomeDependency: Dependency { var tokenProvider: TokenProviding { get } + var homeUsecase: HomeUsecaseProtocol { get } } // MARK: - HomeComponent -final class HomeComponent: Component, FollowDetailDependency { - var homeService: HomeServiceProtocol { - // TODO: 실제 API 연동 시 실제 Service로 교체 - MockHomeService() - } - - var followService: FollowServiceProtocol { - makeFollowService() - } - - var travelService: TravelServiceProtocol { - makeTravelService(tokenProvider: dependency.tokenProvider) +final class HomeComponent: Component { + var homeUsecase: HomeUsecaseProtocol { + dependency.homeUsecase } } @@ -53,18 +46,13 @@ public final class HomeBuilder: Builder, HomeBuildable { let viewController = HomeViewController() let interactor = HomeInteractor( presenter: viewController, - homeService: component.homeService + usecase: component.homeUsecase ) interactor.listener = listener - - let followDetailBuilder = FollowDetailBuilder(dependency: component) - - let router = HomeRouter( + + return HomeRouter( interactor: interactor, - viewController: viewController, - followDetailBuilder: followDetailBuilder + viewController: viewController ) - - return router } } diff --git a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift index 9ccf17c..51a6755 100644 --- a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift +++ b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift @@ -6,166 +6,169 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Domain -import FollowFeature import Foundation + +import Domain + import RIBs +import RxCocoa +import RxRelay import RxSwift -// MARK: - HomeListener +struct HomeSectionModel { + let section: HomeSectionKind + let items: [HomeItem] +} -public protocol HomeListener: AnyObject { - func homeDidAddTrip(title: String, startDate: Date, endDate: Date) +// MARK: - HomeRouting +public protocol HomeRouting: ViewableRouting { + } // MARK: - HomePresentable - protocol HomePresentable: Presentable { var listener: HomePresentableListener? { get set } - - func updateCategories(_ categories: [TripCategory], selectedIndex: Int) - func updateMyTrips(_ trips: [MyTrip]) - func updatePopularTrips(_ tripsByCategory: [TripCategory: [PopularTrip]], categories: [TripCategory]) - func updateRecommendations(_ recommendations: [Recommendation]) - func scrollToCategory(at index: Int) - func showLoading() - func hideLoading() + + func update(with sections: [HomeSectionModel]) + func setLoading(_ isLoading: Bool) + func showErrorView(_ isError: Bool) } -// MARK: - HomePresentableListener - -protocol HomePresentableListener: AnyObject { - func didSelectCategory(at index: Int) - func didScrollToCategory(at index: Int) - func didSelectPopularTrip(at index: Int, in section: Int) - func didSelectRecommendation(at index: Int) - func didTapShowMoreTrips() - func didTapAddButton() - func didTapRefresh() +// MARK: - HomeListener +public protocol HomeListener: AnyObject { + func homeDidTapFollowDetail(with recommendationId: Int) + func homeDidTapSearch() + func homeDidTapSetting() + func homeDidTapPopularTravel() } // MARK: - HomeInteractor final class HomeInteractor: PresentableInteractor, HomeInteractable { - weak var router: HomeRouting? weak var listener: HomeListener? - private let homeService: HomeServiceProtocol + private var fetchDataTask: Task? + private let usecase: HomeUsecaseProtocol private let disposeBag = DisposeBag() // MARK: - Data (Source of Truth) + private let homeDataRelay = BehaviorRelay(value: nil) + private let selectedCategoryRelay = BehaviorRelay(value: nil) - private let categories: [TripCategory] = TripCategory.allCases - private var selectedCategoryIndex: Int = 0 - private var myTrips: [MyTrip] = [] - private var tripsByCategory: [TripCategory: [PopularTrip]] = [:] - private var recommendations: [Recommendation] = [] - - init(presenter: HomePresentable, homeService: HomeServiceProtocol) { - self.homeService = homeService + init(presenter: HomePresentable, usecase: HomeUsecaseProtocol) { + self.usecase = usecase super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() - presenter.updateCategories(categories, selectedIndex: selectedCategoryIndex) - loadHomeData() + + setupStream() + fetchHomeData() } override func willResignActive() { super.willResignActive() + + fetchDataTask?.cancel() + fetchDataTask = nil } - // MARK: - Private Methods - - private func loadHomeData() { - Task { - await MainActor.run { - presenter.showLoading() + private func setupStream() { + Observable.combineLatest( + homeDataRelay.compactMap { $0 }, + selectedCategoryRelay + ) + .map { model, selectedId -> [HomeSectionModel] in + return [ + .init(section: .banner, items: [.banner(model.banner)]), + .init(section: .category, items: model.category.map { + .category($0, isSelected: $0.id == selectedId) + }), + .init(section: .popularTrip, items: model.popularTrip.map { .popularTrip($0) }), + .init(section: .recommendedTrip, items: model.recommendedTrip.map { .recommendedTrip($0) }) + ] + } + .subscribe(with: self) { owner, sections in + owner.presenter.update(with: sections) + } + .disposed(by: disposeBag) + + homeDataRelay + .map { $0 == nil } + .subscribe(with: self) { owner, isLoading in + owner.presenter.setLoading(isLoading) } - - async let myTripsResult = homeService.fetchMyTrips() - async let tripsByCategoryResult = homeService.fetchAllPopularTrips() - async let recommendationsResult = homeService.fetchRecommendations() - - let (myTripsData, tripsByCategoryData, recommendationsData) = await ( - (try? myTripsResult.get()) ?? [], - (try? tripsByCategoryResult.get()) ?? [:], - (try? recommendationsResult.get()) ?? [] - ) - - await MainActor.run { - self.myTrips = myTripsData - self.tripsByCategory = tripsByCategoryData - self.recommendations = recommendationsData - presenter.hideLoading() - presenter.updateMyTrips(myTripsData) - presenter.updatePopularTrips(tripsByCategoryData, categories: categories) - presenter.updateRecommendations(recommendationsData) + .disposed(by: disposeBag) + } + + private func fetchHomeData() { + fetchDataTask?.cancel() + + presenter.setLoading(true) + presenter.showErrorView(false) + + fetchDataTask = Task { + do { + guard !Task.isCancelled else { return } + + async let myTrip = self.usecase.fetchMyTripInfo()?.toPresention() + async let categories = self.usecase.fetchCategoryList().map { $0.toHomeModel() } + async let populars = self.usecase.fetchPopularTripList().map { $0.toPopularHomeModel() } + async let recommended = self.usecase.fetchRecommendTripList().map { $0.toRecommendHomeModel() } + + let model = try await HomePresentationModel( + banner: myTrip ?? .init(title: "", startDay: .now, endDay: .now, tripSchedule: []), + category: categories, + popularTrip: populars, + recommendedTrip: recommended + ) + + if self.selectedCategoryRelay.value == nil, let firstId = model.category.first?.id { + self.selectedCategoryRelay.accept(firstId) + } + + homeDataRelay.accept(model) + presenter.setLoading(false) + } catch let error { + presenter.setLoading(false) + presenter.showErrorView(true) } + + fetchDataTask = nil } } } // MARK: - HomePresentableListener - extension HomeInteractor: HomePresentableListener { - func didSelectCategory(at index: Int) { - guard index != selectedCategoryIndex, index < categories.count else { return } - selectedCategoryIndex = index - presenter.updateCategories(categories, selectedIndex: index) - presenter.scrollToCategory(at: index) - } - - func didScrollToCategory(at index: Int) { - guard index != selectedCategoryIndex, index < categories.count else { return } - selectedCategoryIndex = index - presenter.updateCategories(categories, selectedIndex: index) - } - - func didSelectPopularTrip(at index: Int, in section: Int) { - guard section < categories.count else { return } - let category = categories[section] - guard let trips = tripsByCategory[category], index < trips.count else { return } - // TODO: 실제 API 연동 시 trip.id 사용 - // 현재는 테스트를 위해 항상 id 1로 이동 - router?.routeToFollowDetail(with: 1) - } - - func didSelectRecommendation(at index: Int) { - guard index < recommendations.count else { return } - // TODO: 실제 API 연동 시 recommendation.id 사용 - // 현재는 테스트를 위해 항상 id 1로 이동 - router?.routeToFollowDetail(with: 1) + func reloadBtnTapped() { + fetchHomeData() } - - func didTapShowMoreTrips() { - // TODO: 더보기 화면으로 이동 - print("Show more trips tapped") + + func searchBtnTapped() { + listener?.homeDidTapSearch() } - - func didTapAddButton() { - // TODO: 여행 추가 화면으로 이동 - print("Add button tapped") + + func settingBtnTapped() { + listener?.homeDidTapSetting() } - - func didTapRefresh() { - loadHomeData() - } -} - -// MARK: - FollowDetailListener - -extension HomeInteractor: FollowDetailListener { - func followDetailDidTapClose() { - router?.detachFollowDetail() + + func itemSelected(item: HomeItem) { + switch item { + case .category(let category, _): + selectedCategoryRelay.accept(category.id) + case .popularTrip(let trip): + listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 0) + case .recommendedTrip(let trip): + listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 0) + default: break + } } - - func followDetailDidAddTrip(title: String, startDate: Date, endDate: Date) { - router?.detachFollowDetail() - // TabBar에 알려서 Travel 탭으로 이동 - listener?.homeDidAddTrip(title: title, startDate: startDate, endDate: endDate) + + func moreBtnTapped() { + listener?.homeDidTapPopularTravel() } } diff --git a/Projects/Features/HomeFeature/Sources/HomeRouter.swift b/Projects/Features/HomeFeature/Sources/HomeRouter.swift index 480e5e4..4204d82 100644 --- a/Projects/Features/HomeFeature/Sources/HomeRouter.swift +++ b/Projects/Features/HomeFeature/Sources/HomeRouter.swift @@ -8,11 +8,9 @@ import RIBs -import FollowFeature - // MARK: - HomeInteractable -protocol HomeInteractable: Interactable, FollowDetailListener { +public protocol HomeInteractable: Interactable { var router: HomeRouting? { get set } var listener: HomeListener? { get set } } @@ -20,55 +18,15 @@ protocol HomeInteractable: Interactable, FollowDetailListener { // MARK: - HomeViewControllable public protocol HomeViewControllable: ViewControllable { - func push(_ viewController: ViewControllable) - func pop() -} - -// MARK: - HomeRouting - -public protocol HomeRouting: ViewableRouting { - func routeToFollowDetail(with recommendationId: Int) - func detachFollowDetail() + } // MARK: - HomeRouter final class HomeRouter: ViewableRouter, HomeRouting { - - private let followDetailBuilder: FollowDetailBuildable - private var followDetailRouter: FollowDetailRouting? - - init( - interactor: HomeInteractable, - viewController: HomeViewControllable, - followDetailBuilder: FollowDetailBuildable - ) { - self.followDetailBuilder = followDetailBuilder + + override init(interactor: HomeInteractable, viewController: HomeViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } - - // MARK: - HomeRouting - - func routeToFollowDetail(with recommendationId: Int) { - guard followDetailRouter == nil else { return } - - let router = followDetailBuilder.build(withListener: interactor, recommendationId: recommendationId) - followDetailRouter = router - attachChild(router) - viewController.push(router.viewControllable) - } - - func detachFollowDetail() { - guard let router = followDetailRouter else { return } - - // FollowDetail VC가 아직 네비게이션 스택에 있는 경우에만 pop - if let navController = viewController.uiviewController.navigationController, - navController.viewControllers.contains(router.viewControllable.uiviewController) { - viewController.pop() - } - - detachChild(router) - followDetailRouter = nil - } } diff --git a/Projects/Features/HomeFeature/Sources/HomeViewController.swift b/Projects/Features/HomeFeature/Sources/HomeViewController.swift index 7ada74a..da5e209 100644 --- a/Projects/Features/HomeFeature/Sources/HomeViewController.swift +++ b/Projects/Features/HomeFeature/Sources/HomeViewController.swift @@ -6,256 +6,290 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Core +import UIKit + import Domain import DSKit -import UIKit + import RIBs +import RxCocoa import RxSwift -import SnapKit -import Then -// MARK: - HomeViewController +// MARK: - HomePresentableListener -final class HomeViewController: UIViewController, HomePresentable, HomeViewControllable { +protocol HomePresentableListener: AnyObject { + func searchBtnTapped() + func settingBtnTapped() + func itemSelected(item: HomeItem) + func moreBtnTapped() + func reloadBtnTapped() +} - // MARK: - Properties +// MARK: - HomeViewController +final class HomeViewController: UIViewController, HomeViewControllable { + // MARK: - Properties weak var listener: HomePresentableListener? - + private let disposeBag = DisposeBag() - - // MARK: - UI Components - - private let scrollView = UIScrollView().then { - $0.showsVerticalScrollIndicator = false - $0.contentInset.bottom = 79 - } - - private let contentView = UIView() - - private let loadingIndicator = UIActivityIndicatorView(style: .large).then { - $0.hidesWhenStopped = true - } - - private let myTravelView = MyTravelView() - - private let followGuideLabel = UILabel().then { - $0.setText(.subTitleLSB, text: "인기 여행 따라가기", color: UIColor(hexCode: "#111111")) - } - - private let categoryCollectionView = CategoryCollectionView() - - private let youtuberContentCollectionView = YoutuberContentCollectionView() - - private let showOtherTravelButton = UIButton().then { - $0.setTitle("여행 따라가기 더보기", for: .normal) - $0.setTitleColor(UIColor(hexCode: "#2C2C2C"), for: .normal) - $0.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) - $0.backgroundColor = UIColor(hexCode: "#FFFFFF") - $0.layer.cornerRadius = 8 - $0.layer.borderWidth = 1.0 - $0.layer.borderColor = UIColor(hexCode: "#D9D9D9").cgColor - } - - private let recommendContentGuideLabel = UILabel().then { - $0.setText(.subTitleLSB, text: "OOO님께 추천하는\n따라가기 여행 콘텐츠에요!", color: UIColor(hexCode: "#111111")) - $0.numberOfLines = 2 - } - - private let recommendContentCollectionView = RecommendContentCollectionView() - - private let addFloatingButton = UIButton().then { - $0.backgroundColor = UIColor(hexCode: "#28A745") - $0.layer.cornerRadius = 28 - $0.setImage(DSKitAsset.Assets.icPlus2.image, for: .normal) - $0.tintColor = .white - } - + let moreButtonTapped = PublishSubject() + + // MARK: - UI Components + private let navigationBar = NDGLNavigationBar( + style: .white, + trailingIcon: DSKitAsset.Assets.icSearch2.image, + trailing2Icon: DSKitAsset.Assets.icSettings1.image + ) + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) + private let loadingIndicator = UIActivityIndicatorView(style: .medium) + private let networkErrorView = NDGLErrorView() + + private var dataSource: UICollectionViewDiffableDataSource? + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - setupDelegates() - setupActions() - setupUI() - setupConstraints() + setStyle() + setUI() + setLayout() + + setCollectionView() + setDataSource() + bindInteractor() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) } - - // MARK: - Setup - - private func setupDelegates() { - categoryCollectionView.categoryDelegate = self - youtuberContentCollectionView.contentDelegate = self - recommendContentCollectionView.contentDelegate = self - } - - private func setupActions() { - showOtherTravelButton.addTarget(self, action: #selector(showOtherTravelButtonTapped), for: .touchUpInside) - addFloatingButton.addTarget(self, action: #selector(addFloatingButtonTapped), for: .touchUpInside) - } - - // MARK: - Actions - - @objc private func showOtherTravelButtonTapped() { - listener?.didTapShowMoreTrips() - } - - @objc private func addFloatingButtonTapped() { - listener?.didTapAddButton() - } } -// MARK: - HomePresentable +// MARK: - UI Setup -extension HomeViewController { - func updateMyTrips(_ trips: [Domain.MyTrip]) { +private extension HomeViewController { + func setStyle() { + view.backgroundColor = DSKitAsset.Colors.white.color + + collectionView.do { + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.backgroundColor = .clear + $0.contentInset = .zero + $0.isScrollEnabled = true + } + loadingIndicator.do { + $0.color = DSKitAsset.Colors.green300.color + } + + networkErrorView.do { + $0.isHidden = true + } } - func updateCategories(_ categories: [TripCategory], selectedIndex: Int) { - let categoryNames = categories.map { $0.rawValue } - categoryCollectionView.applySnapshot(categories: categoryNames, selectedIndex: selectedIndex) + func setUI() { + view.addSubviews(collectionView, navigationBar, loadingIndicator, networkErrorView) } - - func updatePopularTrips(_ tripsByCategory: [TripCategory: [PopularTrip]], categories: [TripCategory]) { - youtuberContentCollectionView.applySnapshot(tripsByCategory: tripsByCategory, categories: categories) - } - - func updateRecommendations(_ recommendations: [Recommendation]) { - recommendContentCollectionView.applySnapshot(recommendations: recommendations) - } - - func scrollToCategory(at index: Int) { - youtuberContentCollectionView.scrollToCategory(at: index, animated: true) - } - - func showLoading() { - loadingIndicator.startAnimating() - } - - func hideLoading() { - loadingIndicator.stopAnimating() - } -} - -// MARK: - UI Setup - -extension HomeViewController { - private func setupUI() { - view.backgroundColor = .white - [scrollView, loadingIndicator, addFloatingButton].forEach { - view.addSubview($0) + + func setLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.directionalHorizontalEdges.equalToSuperview() } - scrollView.addSubview(contentView) - - [myTravelView, followGuideLabel, categoryCollectionView, youtuberContentCollectionView, showOtherTravelButton, recommendContentGuideLabel, recommendContentCollectionView].forEach { - contentView.addSubview($0) + + collectionView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.bottom.equalToSuperview() + $0.directionalHorizontalEdges.equalToSuperview() } - - } - - private func setupConstraints() { + loadingIndicator.snp.makeConstraints { $0.center.equalToSuperview() } - scrollView.snp.makeConstraints { - $0.top.bottom.equalTo(view.safeAreaLayoutGuide) - $0.leading.trailing.equalToSuperview() - } - contentView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.width.equalToSuperview() - } - myTravelView.snp.makeConstraints { - $0.top.equalToSuperview().offset(18) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - $0.height.equalTo(80) - } - followGuideLabel.snp.makeConstraints { - $0.top.equalTo(myTravelView.snp.bottom).offset(40) - $0.leading.equalToSuperview().offset(24) - $0.height.equalTo(28) - } - categoryCollectionView.snp.makeConstraints { - $0.top.equalTo(followGuideLabel.snp.bottom).offset(16) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview() - $0.height.equalTo(36) - } - youtuberContentCollectionView.snp.makeConstraints { - $0.top.equalTo(categoryCollectionView.snp.bottom).offset(16) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - $0.height.equalTo(288) - } - showOtherTravelButton.snp.makeConstraints { - $0.top.equalTo(youtuberContentCollectionView.snp.bottom).offset(24) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - $0.height.equalTo(40) - } - recommendContentGuideLabel.snp.makeConstraints { - $0.top.equalTo(showOtherTravelButton.snp.bottom).offset(40) - $0.leading.equalToSuperview().offset(24) - } - recommendContentCollectionView.snp.makeConstraints { - $0.top.equalTo(recommendContentGuideLabel.snp.bottom).offset(24) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview() - $0.height.equalTo(260) - $0.bottom.equalToSuperview().offset(-20) + + networkErrorView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.directionalHorizontalEdges.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-68.adjustedH) } - addFloatingButton.snp.makeConstraints { - $0.size.equalTo(56) - $0.trailing.equalToSuperview().offset(-24) - $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-70) + } + + func setCollectionView() { + collectionView.do { + $0.register( + PopularInfoCell.self, + forCellWithReuseIdentifier: PopularInfoCell.cellIdentifier + ) + $0.register( + CategoryChipCell.self, + forCellWithReuseIdentifier: CategoryChipCell.cellIdentifier + ) + $0.register( + HomeBannerCell.self, + forCellWithReuseIdentifier: HomeBannerCell.cellIdentifier + ) + $0.register( + RecommendInfoCell.self, + forCellWithReuseIdentifier: RecommendInfoCell.cellIdentifier + ) + $0.register( + HomeHeaderView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: HomeHeaderView.reusableViewIdentifier + ) + $0.register( + HomeFooterButtonView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: HomeFooterButtonView.reusableViewIdentifier + ) } } } -// MARK: - CategoryCollectionViewDelegate - -extension HomeViewController: CategoryCollectionViewDelegate { - func categoryCollectionView(_ collectionView: CategoryCollectionView, didSelectCategoryAt index: Int) { - listener?.didSelectCategory(at: index) +// MARK: - Bind +private extension HomeViewController { + func bindInteractor() { + navigationBar.trailingButtonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.searchBtnTapped() + } + .disposed(by: disposeBag) + + navigationBar.trailing2ButtonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.settingBtnTapped() + } + .disposed(by: disposeBag) + + moreButtonTapped + .subscribe(with: self) { owner, _ in + owner.listener?.moreBtnTapped() + } + .disposed(by: disposeBag) + + collectionView.rx.itemSelected + .compactMap { [weak self] indexPath in + self?.dataSource?.itemIdentifier(for: indexPath) + } + .subscribe(with: self) { owner, item in + owner.listener?.itemSelected(item: item) + } + .disposed(by: disposeBag) + + networkErrorView.buttonDidTap + .subscribe(with: self) { owner, _ in + owner.listener?.reloadBtnTapped() + } + .disposed(by: disposeBag) + } + + func applySnapshot(with sections: [HomeSectionModel]) { + var snapshot = NSDiffableDataSourceSnapshot() + sections.forEach { + snapshot.appendSections([$0.section]) + snapshot.appendItems($0.items, toSection: $0.section) + } + dataSource?.apply(snapshot, animatingDifferences: true) } } -// MARK: - YoutuberContentCollectionViewDelegate - -extension HomeViewController: YoutuberContentCollectionViewDelegate { - func youtuberContentCollectionView(_ collectionView: YoutuberContentCollectionView, didSelectItemAt index: Int, in section: Int) { - listener?.didSelectPopularTrip(at: index, in: section) +private extension HomeViewController { + func setDataSource() { + let bannerRegistration = createBannerCellRegistration() + let categoryRegistration = createCategoryCellRegistration() + let popularTripRegistration = createPopularTripCellRegistration() + let recommendedTripRegistration = createRecommedTripCellRegistration() + + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in + switch item { + case .banner(let banner): + return collectionView.dequeueConfiguredReusableCell( + using: bannerRegistration, + for: indexPath, + item: banner + ) + case .category(let category): + return collectionView.dequeueConfiguredReusableCell( + using: categoryRegistration, + for: indexPath, + item: category + ) + case .popularTrip(let tripList): + return collectionView.dequeueConfiguredReusableCell( + using: popularTripRegistration, + for: indexPath, + item: tripList + ) + case .recommendedTrip(let tripList): + return collectionView.dequeueConfiguredReusableCell( + using: recommendedTripRegistration, + for: indexPath, + item: tripList + ) + } + } + + configureSupplementaryView() } - - func youtuberContentCollectionView(_ collectionView: YoutuberContentCollectionView, didScrollToSection section: Int) { - listener?.didScrollToCategory(at: section) + + func configureSupplementaryView() { + let headerRegistration = createHeaderRegistration() + let popularFooterRegistration = createPopularFooterRegistration() + + dataSource?.supplementaryViewProvider = { collectionView, kind, indexPath in + guard HomeSectionKind(rawValue: indexPath.section) != nil else { + return UICollectionReusableView() + } + + if kind == UICollectionView.elementKindSectionHeader { + return collectionView.dequeueConfiguredReusableSupplementary( + using: headerRegistration, + for: indexPath + ) + } + + if kind == UICollectionView.elementKindSectionFooter { + return collectionView.dequeueConfiguredReusableSupplementary( + using: popularFooterRegistration, + for: indexPath + ) + } + + return nil + } } } -// MARK: - RecommendContentCollectionViewDelegate - -extension HomeViewController: RecommendContentCollectionViewDelegate { - func recommendContentCollectionView(_ collectionView: RecommendContentCollectionView, didSelectItemAt index: Int) { - listener?.didSelectRecommendation(at: index) +extension HomeViewController: HomePresentable { + func showErrorView(_ isError: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + self.networkErrorView.isHidden = !isError + + self.collectionView.isHidden = isError + if isError { + self.loadingIndicator.stopAnimating() + } + } } -} - -// MARK: - HomeViewControllable - -extension HomeViewController { - func push(_ viewController: ViewControllable) { - navigationController?.pushViewController(viewController.uiviewController, animated: true) + + func update(with sections: [HomeSectionModel]) { + applySnapshot(with: sections) } - - func pop() { - navigationController?.popViewController(animated: true) + + func setLoading(_ isLoading: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + if isLoading { + self.loadingIndicator.startAnimating() + self.collectionView.alpha = 0.5 + } else { + self.loadingIndicator.stopAnimating() + self.collectionView.alpha = 1.0 + } + } } } diff --git a/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift index 0380755..7db026b 100644 --- a/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift +++ b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift @@ -81,7 +81,7 @@ extension MyTripSummary { } extension TripCategory { - func toPresentaion() -> HomePresentationModel.Category { + func toHomeModel() -> HomePresentationModel.Category { return HomePresentationModel.Category( id: self.id, creator: self.creator, @@ -91,7 +91,7 @@ extension TripCategory { } extension TripInfo { - func toPopularPresentaion() -> HomePresentationModel.PopularTrip { + func toPopularHomeModel() -> HomePresentationModel.PopularTrip { return HomePresentationModel.PopularTrip( id: self.id, title: self.title, @@ -103,7 +103,7 @@ extension TripInfo { ) } - func toPresentaion() -> HomePresentationModel.RecommendedTrip { + func toRecommendHomeModel() -> HomePresentationModel.RecommendedTrip { return HomePresentationModel.RecommendedTrip( id: self.id, title: self.title, diff --git a/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift b/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift index d1d978e..66a8991 100644 --- a/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift +++ b/Projects/Features/HomeFeature/Sources/Views/CollectionView/CompositionalLayout/HomeCompositionalLayout.swift @@ -79,7 +79,7 @@ private extension HomeViewController { section.contentInsets = .init( top: 24.adjustedH, leading: 24.adjusted, - bottom: 16.adjustedH, + bottom: 24.adjustedH, trailing: 24.adjusted ) section.boundarySupplementaryItems = [createHeaderLayout()] From ccc40494169133f027d2e5a7eeb74464730b2b45 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 03:31:54 +0900 Subject: [PATCH 39/47] =?UTF-8?q?chore:=20#18=20-=20=EC=9E=84=EC=8B=9C=20a?= =?UTF-8?q?ccessToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Sources/Adapter/TokenProviderAdapter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift index c440c32..75032b5 100644 --- a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift +++ b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift @@ -18,6 +18,7 @@ public final class TokenProviderAdapter: TokenProviding, @unchecked Sendable { } public func accessToken() -> String? { - tokenRepository.get(.accessToken) +// tokenRepository.get(.accessToken) + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiYmE3ODIwYS0wMDUzLTQxZDctODdhYi00Zjk2ZWM3ZDI1MTMiLCJpYXQiOjE3NzA3OTUwMjYsImV4cCI6MTc3MDg4MTQyNn0.oNCkotV0uA-3kCtTwGhTUwA9fUUhuO85p1k3952oTfRaULOw2Ix3RpXq_ta82ynmUK7F3i8F0Jb1d4_Rl-zcgA" } } From a305883c3ef8fa1771ae9c601a94b363a762f438 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 04:47:37 +0900 Subject: [PATCH 40/47] =?UTF-8?q?design:=20#18=20-=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Component/NDGLNavigationBar.swift | 18 ++++++------- .../Sources/Component/NDGLSearchBar.swift | 26 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Projects/Modules/DSKit/Sources/Component/NDGLNavigationBar.swift b/Projects/Modules/DSKit/Sources/Component/NDGLNavigationBar.swift index fea0873..b0a1f72 100644 --- a/Projects/Modules/DSKit/Sources/Component/NDGLNavigationBar.swift +++ b/Projects/Modules/DSKit/Sources/Component/NDGLNavigationBar.swift @@ -109,12 +109,12 @@ private extension NDGLNavigationBar { titleLabel.setText( .bodyLM, text: title, - color: UIColor(hexCode: "#2C2C2C"), + color: DSKitAsset.Colors.black900.color, alignment: .center ) } - let normalColor = UIColor(hexCode: "#383838") + let normalColor = DSKitAsset.Colors.black600.color [(leadingButton, leading), (trailingButton, trailing), (trailing2Button, trailing2)] .forEach { button, image in @@ -132,7 +132,7 @@ private extension NDGLNavigationBar { containerStackView.do { $0.axis = .horizontal - $0.spacing = 4 + $0.spacing = 4.adjusted $0.alignment = .center } } @@ -152,7 +152,7 @@ private extension NDGLNavigationBar { func setLayout() { [leadingButton, trailingButton, trailing2Button].forEach { - $0.snp.makeConstraints { $0.size.equalTo(40.adjustedH) } + $0.snp.makeConstraints { $0.size.equalTo(40.adjustedH).priority(.high) } } leftSpacer.snp.makeConstraints { @@ -168,12 +168,12 @@ private extension NDGLNavigationBar { } containerStackView.snp.makeConstraints { - $0.directionalHorizontalEdges.equalToSuperview().inset(24) - $0.directionalVerticalEdges.equalToSuperview().inset(4) + $0.directionalHorizontalEdges.equalToSuperview().inset(24.adjusted) + $0.directionalVerticalEdges.equalToSuperview().inset(4.adjustedH) } self.snp.makeConstraints { - $0.height.equalTo(48.adjustedH) + $0.height.greaterThanOrEqualTo(48.adjustedH) } } } @@ -188,9 +188,9 @@ public enum NDGLNavigationBarStyle { var backgroundColor: UIColor { switch self { case .white: - return UIColor(hexCode: "#FFFFFF") + return DSKitAsset.Colors.white.color case .gray: - return UIColor(hexCode: "#F5F5F5") + return DSKitAsset.Colors.black50.color } } } diff --git a/Projects/Modules/DSKit/Sources/Component/NDGLSearchBar.swift b/Projects/Modules/DSKit/Sources/Component/NDGLSearchBar.swift index 2cf865f..b0e2187 100644 --- a/Projects/Modules/DSKit/Sources/Component/NDGLSearchBar.swift +++ b/Projects/Modules/DSKit/Sources/Component/NDGLSearchBar.swift @@ -93,22 +93,22 @@ public final class NDGLSearchBar: UIView { private extension NDGLSearchBar { func setStyle(_ placeholder: String, _ leading: UIImage, _ trailing: UIImage) { searchContainerView.do { - $0.backgroundColor = UIColor(hexCode: "#E6E6E6") + $0.backgroundColor = DSKitAsset.Colors.black100.color $0.layer.cornerRadius = 22.adjustedH $0.clipsToBounds = true } textField.do { var placeHolderAttributes = UIFont.NDGL.bodyLR.attributes - placeHolderAttributes[.foregroundColor] = UIColor(hexCode: "#757575") + placeHolderAttributes[.foregroundColor] = DSKitAsset.Colors.black400.color $0.attributedPlaceholder = NSAttributedString( string: placeholder, attributes: placeHolderAttributes ) $0.font = UIFont.NDGL.bodyLR.font - $0.textColor = UIColor(hexCode: "#2C2C2C") - $0.tintColor = UIColor(hexCode: "#757575") + $0.textColor = DSKitAsset.Colors.black700.color + $0.tintColor = DSKitAsset.Colors.black400.color $0.autocapitalizationType = .none $0.autocorrectionType = .no @@ -116,7 +116,7 @@ private extension NDGLSearchBar { $0.returnKeyType = .search } - let normalColor = UIColor(hexCode: "#383838") + let normalColor = DSKitAsset.Colors.black600.color [(leadingButton, leading), (trailingButton, trailing)].forEach { button, image in var config = UIButton.Configuration.plain() config.image = image.resize(targetSize: 28.adjustedH).withRenderingMode(.alwaysTemplate) @@ -126,7 +126,7 @@ private extension NDGLSearchBar { containerStackView.do { $0.axis = .horizontal - $0.spacing = 8 + $0.spacing = 8.adjusted $0.alignment = .center } } @@ -139,12 +139,12 @@ private extension NDGLSearchBar { func setLayout() { self.snp.makeConstraints { - $0.height.equalTo(48.adjustedH) + $0.height.equalTo(48.adjustedH).priority(.high) } containerStackView.snp.makeConstraints { - $0.directionalHorizontalEdges.equalToSuperview().inset(24) - $0.directionalVerticalEdges.equalToSuperview().inset(2) + $0.directionalHorizontalEdges.equalToSuperview().inset(24.adjusted) + $0.directionalVerticalEdges.equalToSuperview().inset(2.adjusted) } leadingButton.snp.makeConstraints { @@ -152,18 +152,18 @@ private extension NDGLSearchBar { } searchContainerView.snp.makeConstraints { - $0.height.equalTo(44.adjustedH) + $0.height.greaterThanOrEqualTo(44.adjustedH) } textField.snp.makeConstraints { - $0.leading.equalToSuperview().inset(18) - $0.trailing.equalTo(trailingButton.snp.leading).offset(-2) + $0.leading.equalToSuperview().inset(18.adjusted) + $0.trailing.equalTo(trailingButton.snp.leading).offset(-2.adjusted) $0.centerY.equalToSuperview() } trailingButton.snp.makeConstraints { $0.size.equalTo(40.adjustedH) - $0.trailing.equalToSuperview().inset(16) + $0.trailing.equalToSuperview().inset(16.adjusted) $0.centerY.equalToSuperview() } } From 657ea4f55b1f16d73ff962cfa0a2fdef95276c4e Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Thu, 12 Feb 2026 04:47:58 +0900 Subject: [PATCH 41/47] =?UTF-8?q?chore:=20#18=20-=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Features/HomeFeature/Project.swift | 6 +++++- Projects/Features/RootFeature/Project.swift | 6 +++++- Projects/Features/TabBarFeature/Project.swift | 6 +++++- Projects/Modules/Networks/Project.swift | 5 ++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Projects/Features/HomeFeature/Project.swift b/Projects/Features/HomeFeature/Project.swift index a241295..ad3ee86 100644 --- a/Projects/Features/HomeFeature/Project.swift +++ b/Projects/Features/HomeFeature/Project.swift @@ -15,7 +15,11 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "HomeFeature", dependencies: [ - .Features.baseFeatureDependency + .Features.baseFeatureDependency, + + // TODO: - 지워야됨 + .data, + .Modules.networks ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift index 9595aed..d47107c 100644 --- a/Projects/Features/RootFeature/Project.swift +++ b/Projects/Features/RootFeature/Project.swift @@ -15,7 +15,11 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "RootFeature", dependencies: [ - .Features.Main.feature + .Features.Main.feature, + + // TODO: - 지워야됨 + .Modules.networks, + .data ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Features/TabBarFeature/Project.swift b/Projects/Features/TabBarFeature/Project.swift index 7f7d9af..a453a34 100644 --- a/Projects/Features/TabBarFeature/Project.swift +++ b/Projects/Features/TabBarFeature/Project.swift @@ -16,7 +16,11 @@ let project = Project.makeModule( name: "TabBarFeature", dependencies: [ .Features.Home.feature, - .Features.Travel.feature + .Features.Travel.feature, + + // TODO: - 지워야됨 + .data, + .Modules.networks ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift index 0d7fe6a..ec8afd5 100644 --- a/Projects/Modules/Networks/Project.swift +++ b/Projects/Modules/Networks/Project.swift @@ -16,7 +16,10 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "Networks", dependencies: [ - .core + .core, + + //TODO: - 지워야됨 + .domain ], scripts: [.swiftLint], isStatic: true, From 88719198536f7f8f12fa167629bd8d22509a9dde Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Fri, 13 Feb 2026 15:12:08 +0900 Subject: [PATCH 42/47] =?UTF-8?q?fix:=20#18=20-=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=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 --- Projects/Data/Sources/Transform/TripResponse+.swift | 3 +-- Projects/Domain/Sources/Model/Home/TripInfo.swift | 3 +-- .../Features/HomeFeature/Sources/HomeInteractor.swift | 10 +++++----- .../HomeFeature/Sources/HomeViewController.swift | 4 +++- .../Sources/PopularTravelInteractor.swift | 8 ++++---- .../Sources/PopularTravelViewController.swift | 10 +++------- .../SearchFeature/Sources/SearchViewController.swift | 2 ++ .../SettingFeature/Sources/SettingViewController.swift | 2 ++ .../Sources/Extensions/MoyaProvider+Async.swift | 4 ++-- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Projects/Data/Sources/Transform/TripResponse+.swift b/Projects/Data/Sources/Transform/TripResponse+.swift index 64c6d05..a8912db 100644 --- a/Projects/Data/Sources/Transform/TripResponse+.swift +++ b/Projects/Data/Sources/Transform/TripResponse+.swift @@ -22,8 +22,7 @@ extension TripResponse { country: $0.country, city: $0.city, nights: $0.nights, - days: $0.days, - countryCode: $0.country + days: $0.days ) } } diff --git a/Projects/Domain/Sources/Model/Home/TripInfo.swift b/Projects/Domain/Sources/Model/Home/TripInfo.swift index 3e22ec0..d9911b4 100644 --- a/Projects/Domain/Sources/Model/Home/TripInfo.swift +++ b/Projects/Domain/Sources/Model/Home/TripInfo.swift @@ -26,8 +26,7 @@ public struct TripInfo { country: String, city: String, nights: Int, - days: Int, - countryCode: String + days: Int ) { self.id = id self.title = title diff --git a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift index 51a6755..fa58e25 100644 --- a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift +++ b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift @@ -110,10 +110,10 @@ final class HomeInteractor: PresentableInteractor, HomeInteract presenter.setLoading(true) presenter.showErrorView(false) - fetchDataTask = Task { + fetchDataTask = Task { [weak self] in + guard let self, !Task.isCancelled else { return } + do { - guard !Task.isCancelled else { return } - async let myTrip = self.usecase.fetchMyTripInfo()?.toPresention() async let categories = self.usecase.fetchCategoryList().map { $0.toHomeModel() } async let populars = self.usecase.fetchPopularTripList().map { $0.toPopularHomeModel() } @@ -126,6 +126,8 @@ final class HomeInteractor: PresentableInteractor, HomeInteract recommendedTrip: recommended ) + guard !Task.isCancelled else { return } + if self.selectedCategoryRelay.value == nil, let firstId = model.category.first?.id { self.selectedCategoryRelay.accept(firstId) } @@ -136,8 +138,6 @@ final class HomeInteractor: PresentableInteractor, HomeInteract presenter.setLoading(false) presenter.showErrorView(true) } - - fetchDataTask = nil } } } diff --git a/Projects/Features/HomeFeature/Sources/HomeViewController.swift b/Projects/Features/HomeFeature/Sources/HomeViewController.swift index da5e209..90910ee 100644 --- a/Projects/Features/HomeFeature/Sources/HomeViewController.swift +++ b/Projects/Features/HomeFeature/Sources/HomeViewController.swift @@ -276,7 +276,9 @@ extension HomeViewController: HomePresentable { } func update(with sections: [HomeSectionModel]) { - applySnapshot(with: sections) + DispatchQueue.main.async { [weak self] in + self?.applySnapshot(with: sections) + } } func setLoading(_ isLoading: Bool) { diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift index e878541..b3f4931 100644 --- a/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift @@ -101,9 +101,9 @@ final class PopularTravelInteractor: PresentableInteractor(_ target: Target) async throws -> T { try await withCheckedThrowingContinuation { continuation in + NetworkLogger.logRequest(target) + request(target) { result in - NetworkLogger.logRequest(target) - switch result { case .success(let response): NetworkLogger.logResponse(response) From 1140041909f38c0e09f43ef1c507fe6152114ee1 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Fri, 13 Feb 2026 18:01:52 +0900 Subject: [PATCH 43/47] =?UTF-8?q?refactor:=20#18=20-=20Network=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A7=A4=ED=95=91=20=EB=B0=8F=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/Home/HomeRepository.swift | 18 +++++++-- Projects/Data/Sources/Transform/Error+.swift | 29 ++++++++++++++ Projects/Domain/Sources/Error/NDGLError.swift | 15 +++++++ .../Networks/Sources/Base/NetworkError.swift | 3 ++ .../Extensions/MoyaProvider+Async.swift | 39 ++++++++----------- 5 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 Projects/Data/Sources/Transform/Error+.swift create mode 100644 Projects/Domain/Sources/Error/NDGLError.swift diff --git a/Projects/Data/Sources/Repository/Home/HomeRepository.swift b/Projects/Data/Sources/Repository/Home/HomeRepository.swift index 3215f8f..9076d22 100644 --- a/Projects/Data/Sources/Repository/Home/HomeRepository.swift +++ b/Projects/Data/Sources/Repository/Home/HomeRepository.swift @@ -23,14 +23,26 @@ public final class HomeRepository: HomeRepositoryInterface { } public func fetchCategoryList() async throws -> [TripCategory] { - try await homeService.getCategoryList().map { $0.toDomain() } + do { + return try await homeService.getCategoryList().map { $0.toDomain() } + } catch { + throw error.toNDGLError() + } } public func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] { - try await homeService.getPopularTripList(id: id, page: page, size: size).toDomain() + do { + return try await homeService.getPopularTripList(id: id, page: page, size: size).toDomain() + } catch { + throw error.toNDGLError() + } } public func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] { - try await homeService.getRecommendTripList(page: page, size: size).toDomain() + do { + return try await homeService.getRecommendTripList(page: page, size: size).toDomain() + } catch { + throw error.toNDGLError() + } } } diff --git a/Projects/Data/Sources/Transform/Error+.swift b/Projects/Data/Sources/Transform/Error+.swift new file mode 100644 index 0000000..999a768 --- /dev/null +++ b/Projects/Data/Sources/Transform/Error+.swift @@ -0,0 +1,29 @@ +// +// Error+.swift +// Data +// +// Created by 최안용 on 2/13/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension Error { + func toNDGLError() -> NDGLError { + if let networkError = self as? NetworkError { + switch networkError { + case .connectionFailed, .decodingFailed, .noData: + return .unknown("\(networkError.message)") + case .unknown(let string): + return .serverError(string) + case .serverError(let errorResponse): + return .serverError(errorResponse.message ?? "알 수 없는 오류가 발생했습니다.") + } + } + + return .unknown("알 수 없는 오류가 발생했습니다.") + } +} diff --git a/Projects/Domain/Sources/Error/NDGLError.swift b/Projects/Domain/Sources/Error/NDGLError.swift new file mode 100644 index 0000000..b7f68ca --- /dev/null +++ b/Projects/Domain/Sources/Error/NDGLError.swift @@ -0,0 +1,15 @@ +// +// NDGLError.swift +// Domain +// +// Created by 최안용 on 2/13/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public enum NDGLError: Error { + case serverError(String) + case unknown(String) + case authenticationFailed +} diff --git a/Projects/Modules/Networks/Sources/Base/NetworkError.swift b/Projects/Modules/Networks/Sources/Base/NetworkError.swift index 757f07d..7e8b09c 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkError.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkError.swift @@ -13,6 +13,7 @@ public enum NetworkError: Error, Sendable { case decodingFailed case noData case unknown(String) + case serverError(ErrorResponse) public var message: String { switch self { @@ -24,6 +25,8 @@ public enum NetworkError: Error, Sendable { return "응답 데이터가 없습니다." case .unknown(let description): return "알 수 없는 오류: \(description)" + case .serverError(let error): + return error.message ?? "알 수 없는 오류가 발생했습니다." } } } diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift index 2a7b661..a3520ed 100644 --- a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -114,29 +114,24 @@ extension MoyaProvider { switch result { case .success(let response): NetworkLogger.logResponse(response) - if (200...299).contains(response.statusCode) { - do { - let decodedData = try response.map(BaseResponse.self) - - if let data = decodedData.data { - continuation.resume(returning: data) - } else { - continuation.resume(throwing: NetworkError.noData) - } - } catch { - continuation.resume(throwing: NetworkError.decodingFailed) - } - } else { - do { - let errorResponse = try response.map(ErrorResponse.self) - continuation.resume( - throwing: NetworkError.unknown( - errorResponse.message ?? "알 수 없는 오류가 발생했습니다." - ) - ) - } catch { - continuation.resume(throwing: NetworkError.unknown("알 수 없는 오류가 발생했습니다.")) + + guard (200...299).contains(response.statusCode) else { + let error = (try? response.map(ErrorResponse.self)) + .map { NetworkError.serverError($0) } + ?? NetworkError.unknown("Status Code: \(response.statusCode)") + continuation.resume(throwing: error) + return + } + + do { + let baseResponse = try response.map(BaseResponse.self) + if let data = baseResponse.data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: NetworkError.noData) } + } catch { + continuation.resume(throwing: NetworkError.decodingFailed) } case .failure(let error): NetworkLogger.logError(error) From 8dcfcfb6beb74259fce35e2a6f6bc6a06c11cb5a Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Fri, 13 Feb 2026 18:20:54 +0900 Subject: [PATCH 44/47] =?UTF-8?q?fix:=20#18=20-=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopularTravelFeature/Sources/PopularTravelInteractor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift index b3f4931..3cd1b6a 100644 --- a/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift +++ b/Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift @@ -100,7 +100,7 @@ final class PopularTravelInteractor: PresentableInteractor Date: Fri, 13 Feb 2026 20:56:44 +0900 Subject: [PATCH 45/47] =?UTF-8?q?feat:=20#18=20-=20upcoming=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Adapter/TokenProviderAdapter.swift | 2 +- .../Data/Sources/DI/HomeServiceFactory.swift | 2 +- .../Repository/Home/HomeRepository.swift | 8 ++- .../Sources/Transform/ProgramResponse+.swift | 2 +- .../Sources/Transform/TripResponse+.swift | 2 +- .../Sources/Transform/UpcomingResponse+.swift | 40 ++++++++++++ .../Home/HomeRepositoryInterface.swift | 2 +- .../Sources/Model/Home/MyTripSummary.swift | 7 ++- .../Domain/Sources/UseCase/HomeUsecase.swift | 4 +- .../HomeFeature/Sources/HomeInteractor.swift | 17 ++++-- .../Models/HomePresentationModel.swift | 61 +++++++++++++++---- .../Sources/DTO/Home/UpcomingResponse.swift | 38 ++++++++++++ .../Sources/Service/HomeService.swift | 8 +-- .../Networks/Sources/TargetType/HomeAPI.swift | 10 +-- 14 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 Projects/Data/Sources/Transform/UpcomingResponse+.swift create mode 100644 Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift diff --git a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift index 75032b5..d622da6 100644 --- a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift +++ b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift @@ -19,6 +19,6 @@ public final class TokenProviderAdapter: TokenProviding, @unchecked Sendable { public func accessToken() -> String? { // tokenRepository.get(.accessToken) - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiYmE3ODIwYS0wMDUzLTQxZDctODdhYi00Zjk2ZWM3ZDI1MTMiLCJpYXQiOjE3NzA3OTUwMjYsImV4cCI6MTc3MDg4MTQyNn0.oNCkotV0uA-3kCtTwGhTUwA9fUUhuO85p1k3952oTfRaULOw2Ix3RpXq_ta82ynmUK7F3i8F0Jb1d4_Rl-zcgA" + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiYmE3ODIwYS0wMDUzLTQxZDctODdhYi00Zjk2ZWM3ZDI1MTMiLCJpYXQiOjE3NzA5NjQzMDUsImV4cCI6MTc3MTA1MDcwNX0.Sn8wNhZ1Ac-ETZDsOiSMMHHaALJNXxNKrbN_-4xD5REcVa2tJ0NiafhTKlbIuYafL1Acd9dDMIHjx3H33c5w8w" } } diff --git a/Projects/Data/Sources/DI/HomeServiceFactory.swift b/Projects/Data/Sources/DI/HomeServiceFactory.swift index 81f6021..9c1947d 100644 --- a/Projects/Data/Sources/DI/HomeServiceFactory.swift +++ b/Projects/Data/Sources/DI/HomeServiceFactory.swift @@ -6,8 +6,8 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Networks import Domain +import Networks import Moya diff --git a/Projects/Data/Sources/Repository/Home/HomeRepository.swift b/Projects/Data/Sources/Repository/Home/HomeRepository.swift index 9076d22..cb14917 100644 --- a/Projects/Data/Sources/Repository/Home/HomeRepository.swift +++ b/Projects/Data/Sources/Repository/Home/HomeRepository.swift @@ -18,8 +18,12 @@ public final class HomeRepository: HomeRepositoryInterface { self.homeService = homeService } - public func fetchMyTripInfo() async throws -> MyTripSummary? { - return MyTripSummary(title: "임시", startDay: .now, endDay: .now, tripSchedule: [Schedule(id: 1, day: 1, placeName: "임시", thumbnailUrl: "", transport: "", estimatedDuration: 2)]) + public func fetchMyTripInfo() async throws -> MyTripSummary { + do { + return try await homeService.getUpcoming().toDomain() + } catch { + throw error.toNDGLError() + } } public func fetchCategoryList() async throws -> [TripCategory] { diff --git a/Projects/Data/Sources/Transform/ProgramResponse+.swift b/Projects/Data/Sources/Transform/ProgramResponse+.swift index 80aefd9..1f91606 100644 --- a/Projects/Data/Sources/Transform/ProgramResponse+.swift +++ b/Projects/Data/Sources/Transform/ProgramResponse+.swift @@ -12,7 +12,7 @@ import Domain import Networks extension ProgramResponse { - public func toDomain() -> TripCategory { + func toDomain() -> TripCategory { .init(id: self.id, creator: self.name, viedoType: VideoType(rawValue: self.type)) } } diff --git a/Projects/Data/Sources/Transform/TripResponse+.swift b/Projects/Data/Sources/Transform/TripResponse+.swift index a8912db..84301ff 100644 --- a/Projects/Data/Sources/Transform/TripResponse+.swift +++ b/Projects/Data/Sources/Transform/TripResponse+.swift @@ -12,7 +12,7 @@ import Domain import Networks extension TripResponse { - public func toDomain() -> [TripInfo] { + func toDomain() -> [TripInfo] { self.content.map { .init( id: $0.travelId, diff --git a/Projects/Data/Sources/Transform/UpcomingResponse+.swift b/Projects/Data/Sources/Transform/UpcomingResponse+.swift new file mode 100644 index 0000000..18f65bf --- /dev/null +++ b/Projects/Data/Sources/Transform/UpcomingResponse+.swift @@ -0,0 +1,40 @@ +// +// UpcomingResponse+.swift +// Data +// +// Created by 최안용 on 2/13/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension UpcomingResponse { + func toDomain() -> MyTripSummary { + return .init( + id: self.userTravelId, + title: self.title, + startDay: self.startDate.toDate() ?? .now, + endDay: self.endDate.toDate() ?? .now, + tripSchedule: .init( + id: self.upcomingUserTravelPlace.id, + day: 1, // 서버에서 첫 일정만 보내주고 있음 + placeName: self.upcomingUserTravelPlace.place.name, + thumbnailUrl: self.upcomingUserTravelPlace.place.thumbnail ?? "", + transport: self.upcomingUserTravelPlace.place.category, + estimatedDuration: self.upcomingUserTravelPlace.estimatedDuration + ) + ) + } +} + +extension String { + func toDate() -> Date? { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + return formatter.date(from: self) + } +} diff --git a/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift b/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift index 9d7c377..b051de7 100644 --- a/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift +++ b/Projects/Domain/Sources/Interface/Home/HomeRepositoryInterface.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - API 나오기 전 임시 public protocol HomeRepositoryInterface { - func fetchMyTripInfo() async throws -> MyTripSummary? + func fetchMyTripInfo() async throws -> MyTripSummary func fetchCategoryList() async throws -> [TripCategory] func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] diff --git a/Projects/Domain/Sources/Model/Home/MyTripSummary.swift b/Projects/Domain/Sources/Model/Home/MyTripSummary.swift index bc5c14b..894937f 100644 --- a/Projects/Domain/Sources/Model/Home/MyTripSummary.swift +++ b/Projects/Domain/Sources/Model/Home/MyTripSummary.swift @@ -10,12 +10,14 @@ import Foundation // MARK: - API 나오기 전 임시 public struct MyTripSummary { + public let id: Int public let title: String public let startDay: Date public let endDay: Date - public let tripSchedule: [Schedule] + public let tripSchedule: Schedule - public init(title: String, startDay: Date, endDay: Date, tripSchedule: [Schedule]) { + public init(id: Int, title: String, startDay: Date, endDay: Date, tripSchedule: Schedule) { + self.id = id self.title = title self.startDay = startDay self.endDay = endDay @@ -25,7 +27,6 @@ public struct MyTripSummary { // MARK: - API 나오기 전 임시 public struct Schedule { - // 시작 시간이 있어야 홈 상단에 현재 갈 장소 카드로 보여줄 수 있음 public let id: Int public let day: Int public let placeName: String diff --git a/Projects/Domain/Sources/UseCase/HomeUsecase.swift b/Projects/Domain/Sources/UseCase/HomeUsecase.swift index 55de0ed..9944dec 100644 --- a/Projects/Domain/Sources/UseCase/HomeUsecase.swift +++ b/Projects/Domain/Sources/UseCase/HomeUsecase.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - API 나오기 전 임시 public protocol HomeUsecaseProtocol { - func fetchMyTripInfo() async throws -> MyTripSummary? + func fetchMyTripInfo() async throws -> MyTripSummary func fetchCategoryList() async throws -> [TripCategory] func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo] func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] @@ -42,7 +42,7 @@ public final class HomeUsecase { } extension HomeUsecase: HomeUsecaseProtocol { - public func fetchMyTripInfo() async throws -> MyTripSummary? { + public func fetchMyTripInfo() async throws -> MyTripSummary { try await repository.fetchMyTripInfo() } diff --git a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift index fa58e25..769c1b1 100644 --- a/Projects/Features/HomeFeature/Sources/HomeInteractor.swift +++ b/Projects/Features/HomeFeature/Sources/HomeInteractor.swift @@ -114,13 +114,21 @@ final class HomeInteractor: PresentableInteractor, HomeInteract guard let self, !Task.isCancelled else { return } do { - async let myTrip = self.usecase.fetchMyTripInfo()?.toPresention() + let myTripBanner: HomePresentationModel.Banner = await { + do { + return try await self.usecase.fetchMyTripInfo().toPresention() + } catch { + + return .empty + } + }() + async let categories = self.usecase.fetchCategoryList().map { $0.toHomeModel() } async let populars = self.usecase.fetchPopularTripList().map { $0.toPopularHomeModel() } async let recommended = self.usecase.fetchRecommendTripList().map { $0.toRecommendHomeModel() } let model = try await HomePresentationModel( - banner: myTrip ?? .init(title: "", startDay: .now, endDay: .now, tripSchedule: []), + banner: myTripBanner, category: categories, popularTrip: populars, recommendedTrip: recommended @@ -135,6 +143,7 @@ final class HomeInteractor: PresentableInteractor, HomeInteract homeDataRelay.accept(model) presenter.setLoading(false) } catch let error { + print(error) presenter.setLoading(false) presenter.showErrorView(true) } @@ -161,9 +170,9 @@ extension HomeInteractor: HomePresentableListener { case .category(let category, _): selectedCategoryRelay.accept(category.id) case .popularTrip(let trip): - listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 0) + listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 1) case .recommendedTrip(let trip): - listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 0) + listener?.homeDidTapFollowDetail(with: Int(trip.id) ?? 1) default: break } } diff --git a/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift index 7db026b..7d0832a 100644 --- a/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift +++ b/Projects/Features/HomeFeature/Sources/Models/HomePresentationModel.swift @@ -17,11 +17,12 @@ struct HomePresentationModel { let recommendedTrip: [HomePresentationModel.RecommendedTrip] struct Banner: Hashable { - let id = UUID() + let id: Int let title: String let startDay: Date let endDay: Date - let tripSchedule: [Schedule] + let duration: String + let tripSchedule: Schedule } struct Schedule: Hashable { @@ -60,22 +61,49 @@ struct HomePresentationModel { } } +extension HomePresentationModel.Banner { + static var empty: Self { + return .init( + id: 0, // 0으로 설정하여 empty 배너임을 구분 + title: "다가오는 여행이 없습니다.", + startDay: Date(), + endDay: Date(), + duration: "", + tripSchedule: .empty + ) + } +} + +extension HomePresentationModel.Schedule { + static var empty: Self { + return .init( + id: 0, + day: 0, + placeName: "", + thumbnailUrl: "", + transport: "", + estimatedDuration: 0 + ) + } +} + extension MyTripSummary { func toPresention() -> HomePresentationModel.Banner { return HomePresentationModel.Banner( + id: self.id, title: self.title, startDay: self.startDay, endDay: self.endDay, - tripSchedule: self.tripSchedule.map { - HomePresentationModel.Schedule( - id: $0.id, - day: $0.day, - placeName: $0.placeName, - thumbnailUrl: $0.thumbnailUrl, - transport: $0.transport, - estimatedDuration: $0.estimatedDuration - ) - } + duration: "\(self.startDay.toKoreanMMdd())~\(self.endDay.toKoreanMMdd())", + tripSchedule: + .init( + id: self.tripSchedule.id, + day: self.tripSchedule.day, + placeName: self.tripSchedule.placeName, + thumbnailUrl: self.tripSchedule.thumbnailUrl, + transport: self.tripSchedule.transport, + estimatedDuration: self.tripSchedule.estimatedDuration + ) ) } } @@ -115,3 +143,12 @@ extension TripInfo { ) } } + +extension Date { + func toKoreanMMdd() -> String { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "M월 d일" + return formatter.string(from: self) + } +} diff --git a/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift b/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift new file mode 100644 index 0000000..59af683 --- /dev/null +++ b/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift @@ -0,0 +1,38 @@ +// +// UpcomingResponse.swift +// Networks +// +// Created by 최안용 on 2/13/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct UpcomingResponse: Decodable { + public let userTravelId: Int + public let title: String + public let country: String + public let city: String + public let startDate: String + public let endDate: String + public let nights: Int + public let days: Int + public let upcomingUserTravelPlace: UpcomingPlaceResponse +} + +public struct UpcomingPlaceResponse: Decodable { + public let id: Int + public let estimatedDuration: Int + public let place: PlaceDetailResponse +} + +public struct PlaceDetailResponse: Decodable { + public let googlePlaceId: String + public let thumbnail: String? + public let latitude: Double + public let longitude: Double + public let name: String + public let regularOpeningHours: String? + public let googleMapsUri: String + public let category: String +} diff --git a/Projects/Modules/Networks/Sources/Service/HomeService.swift b/Projects/Modules/Networks/Sources/Service/HomeService.swift index 6a886f0..711be42 100644 --- a/Projects/Modules/Networks/Sources/Service/HomeService.swift +++ b/Projects/Modules/Networks/Sources/Service/HomeService.swift @@ -12,9 +12,7 @@ import Moya // MARK: - API 나오기 전 임시 public protocol HomeServiceProtocol { - //임시 - func getMyTrip() async throws -> Int - + func getUpcoming() async throws -> UpcomingResponse func getCategoryList() async throws -> [ProgramResponse] func getPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> TripResponse func getRecommendTripList(page: Int?, size: Int?) async throws -> TripResponse @@ -27,8 +25,8 @@ public final class HomeService: HomeServiceProtocol { self.provider = provider } - public func getMyTrip() async throws -> Int { - try await provider.asyncThowsRequest(.getMyTrip) + public func getUpcoming() async throws -> UpcomingResponse { + try await provider.asyncThowsRequest(.getUpcoming) } public func getCategoryList() async throws -> [ProgramResponse] { diff --git a/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift b/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift index 33b37cf..7239c26 100644 --- a/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift +++ b/Projects/Modules/Networks/Sources/TargetType/HomeAPI.swift @@ -12,7 +12,7 @@ import Moya // MARK: - API 나오기 전 임시 public enum HomeAPI { - case getMyTrip + case getUpcoming case getCategoryList case getPopularTripList(id: Int?, page: Int?, size: Int?) case getRecommendTripList(page: Int?, size: Int?) @@ -25,8 +25,8 @@ extension HomeAPI: TargetType { public var path: String { switch self { - case .getMyTrip: - return "" + case .getUpcoming: + return "/api/v1/travels/upcoming" case .getCategoryList: return "/api/v1/travel-programs" case .getPopularTripList: @@ -38,14 +38,14 @@ extension HomeAPI: TargetType { public var method: Moya.Method { switch self { - case .getMyTrip, .getCategoryList, .getPopularTripList, .getRecommendTripList: + case .getUpcoming, .getCategoryList, .getPopularTripList, .getRecommendTripList: return .get } } public var task: Moya.Task { switch self { - case .getMyTrip, .getCategoryList: + case .getUpcoming, .getCategoryList: return .requestPlain case .getPopularTripList(let id, let page, let size): var params: [String: Any] = [:] From ea28b573460f8362617c368349a2f9ddc2ebdaa6 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Fri, 13 Feb 2026 20:57:00 +0900 Subject: [PATCH 46/47] =?UTF-8?q?fix:=20#18=20-=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=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 --- .../Extensions/Foundation+/String+.swift | 2 +- .../Sources/Views/Cells/HomeBannerCell.swift | 34 ++++++------------- .../Views/Cells/RecommendInfoCell.swift | 13 +++++++ 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Projects/Core/Sources/Extensions/Foundation+/String+.swift b/Projects/Core/Sources/Extensions/Foundation+/String+.swift index 72ac364..8286cdd 100644 --- a/Projects/Core/Sources/Extensions/Foundation+/String+.swift +++ b/Projects/Core/Sources/Extensions/Foundation+/String+.swift @@ -26,7 +26,7 @@ public extension String { } func toKoreanCountryName() -> String { - guard self.count == 2 else { return "알 수 없음"} + guard self.count == 2 else { return "알 수 없음" } let locale = Locale(identifier: "ko_KR") return locale.localizedString(forRegionCode: self) ?? "알 수 없음" diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift index 2c51376..2ff02ec 100644 --- a/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift @@ -43,31 +43,30 @@ final class HomeBannerCell: UICollectionViewCell { func configure(_ model: HomePresentationModel.Banner) { [emptyView, upCommingView, onGoingView].forEach { $0.isHidden = true } + + let now = Date() + let calendar = Calendar.current - if model.tripSchedule.isEmpty { + if model.id == 0 { self.type = .empty emptyView.isHidden = false return } - - let now = Date() - let calendar = Calendar.current let startOfToday = calendar.startOfDay(for: now) let startOfTravel = calendar.startOfDay(for: model.startDay) let startOfEnd = calendar.startOfDay(for: model.endDay) - let dateRangeString = formatDateRange(start: model.startDay, end: model.endDay) if startOfToday >= startOfTravel && startOfToday <= startOfEnd { - let schedule = model.tripSchedule.first + let schedule = model.tripSchedule self.type = .onGoing( title: model.title, - date: dateRangeString, + date: model.duration, transportIcon: DSKitAsset.Assets.icBus2.image, - duration: "\(schedule?.estimatedDuration ?? 0)분", - place: schedule?.placeName ?? "", - imageUrl: schedule?.thumbnailUrl ?? "" + duration: "\(schedule.estimatedDuration)분", + place: schedule.placeName, + imageUrl: schedule.thumbnailUrl ) onGoingView.isHidden = false @@ -78,9 +77,9 @@ final class HomeBannerCell: UICollectionViewCell { self.type = .upComming( title: model.title, - date: dateRangeString, + date: model.duration, dDay: dDayValue, - imageUrl: model.tripSchedule.first?.thumbnailUrl ?? "" + imageUrl: model.tripSchedule.thumbnailUrl ) upCommingView.isHidden = false } @@ -95,17 +94,6 @@ final class HomeBannerCell: UICollectionViewCell { } private extension HomeBannerCell { - func formatDateRange(start: Date, end: Date) -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "ko_KR") - formatter.dateFormat = "M월 d일" - - let startStr = formatter.string(from: start) - let endStr = formatter.string(from: end) - - return "\(startStr) ~ \(endStr)" - } - func updateViewWithCurrentType() { switch type { case .upComming(let title, let date, let dDay, let imageUrl): diff --git a/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift b/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift index a75ab9e..48714d3 100644 --- a/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift +++ b/Projects/Features/HomeFeature/Sources/Views/Cells/RecommendInfoCell.swift @@ -41,6 +41,19 @@ final class RecommendInfoCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + override public func prepareForReuse() { + super.prepareForReuse() + + thumbnailView.kf.cancelDownloadTask() + thumbnailView.image = nil + nationalFlagLabel.text = nil + cityLabel.text = nil + nameLabel.text = nil + titleLabel.text = nil + nationLabel.text = nil + scheduleLabel.text = nil + } + // MARK: - Configure func configure(_ model: HomePresentationModel.RecommendedTrip) { if let url = URL(string: model.thumbnailUrl) { From 7612920b40d427f7ddf6da3e6a5879b09a1530e6 Mon Sep 17 00:00:00 2001 From: ChoiAnYong Date: Fri, 13 Feb 2026 21:17:25 +0900 Subject: [PATCH 47/47] =?UTF-8?q?fix:=20#18=20-=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift b/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift index 59af683..0287522 100644 --- a/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift +++ b/Projects/Modules/Networks/Sources/DTO/Home/UpcomingResponse.swift @@ -23,10 +23,10 @@ public struct UpcomingResponse: Decodable { public struct UpcomingPlaceResponse: Decodable { public let id: Int public let estimatedDuration: Int - public let place: PlaceDetailResponse + public let place: UpcomingPlaceDetailResponse } -public struct PlaceDetailResponse: Decodable { +public struct UpcomingPlaceDetailResponse: Decodable { public let googlePlaceId: String public let thumbnail: String? public let latitude: Double