From 52f45dba2b4bb8e61d255aacb1e2f89680f7712d Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Mon, 6 Jul 2026 00:56:00 +0900 Subject: [PATCH] Setting claude code and basic layout --- myApp/HaruDanim/CLAUDE.md | 22 ++ .../UserInterfaceState.xcuserstate | Bin 15741 -> 24322 bytes myApp/HaruDanim/IOS/ContentView.swift | 8 +- myApp/HaruDanim/IOS/HaruDanimApp.swift | 10 + myApp/HaruDanim/IOS/Models/Schema.swift | 265 ++++++++++++++++++ .../IOS/ViewModels/TrackingViewModel.swift | 85 ++++++ .../IOS/Views/MainDashboardView.swift | 207 ++++++++++++++ myApp/HaruDanim/IOS/Views/MainTabView.swift | 48 ++++ 8 files changed, 638 insertions(+), 7 deletions(-) create mode 100644 myApp/HaruDanim/CLAUDE.md create mode 100644 myApp/HaruDanim/IOS/Models/Schema.swift create mode 100644 myApp/HaruDanim/IOS/ViewModels/TrackingViewModel.swift create mode 100644 myApp/HaruDanim/IOS/Views/MainDashboardView.swift create mode 100644 myApp/HaruDanim/IOS/Views/MainTabView.swift diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md new file mode 100644 index 0000000..24d0275 --- /dev/null +++ b/myApp/HaruDanim/CLAUDE.md @@ -0,0 +1,22 @@ +# HaruDanim (하루 다님) Development Guidelines + +## Architecture & Stack +- **UI Framework**: SwiftUI +- **Data Storage**: SwiftData +- **Architecture**: MVVM (Model-View-ViewModel) + Observation (`@Observable`) +- **Minimum Target**: iOS 17.0+ (to fully utilize SwiftData and new Observation framework) + +## Coding Conventions +1. **Scope Limitation**: Never scan the entire project unnecessarily. Only read and edit files explicitly requested. +2. **Data Modeling**: + - Use `@Model` macro for all SwiftData models. + - Strictly implement relationships (e.g., Action <-> Tag many-to-many, Goal -> Quest one-to-many). + - Use raw value Enums for specific types (ActionType, QuestPeriod, Direction). +3. **UI/UX Guidelines**: + - Primary Green: Light `#2F6B4F`, Dark `#7FBF9E` + - Accent Yellow: Light `#D9A621`, Dark `#E8C558` + - Background: Light `#FAFAF6`, Dark `#111512` + - Use `SF Symbols` exclusively for iconography. +4. **Business Logic**: + - A day boundary starts at `AppSettings.dayStartHour` (default 00:00). + - Sessions crossing this boundary must be saved as one `TimeSession` but logically split when queried for statistics. diff --git a/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate index 4064445cd7980346f0d775a6106ba10c6b41ad32..9cb6840475193cbc094c0ad6612bfda97a2d7ec2 100644 GIT binary patch literal 24322 zcmeIac|cUv8#jK=ox6YtGb|(PFe|H!vJMKe3^NS-fPxz?IKYULzz_o}uJ@LW)p5ojbzL+NM~%0QVY3uPlC z%0VV%MpjgT#-d563Qa?`$c`N72Goe0$c0+a9Mp#9qubE!=nixzT8dVqb!a`>gf^pR z(Q{}!dI9Z3FQcRA7&?wlpm)%_=som4I*Cr9Pta%REINn2L|>yT=zH`d`U(Ave#Zn3 z!wMXZl~{$1E6B2L1|I0a|mOq_-Da6T@;g}4Nl;)%EtPr{S&G+cx0 z@C@w0ZFnx8hv(x3cp+YdS$rG59p8cP#7prqd_R5wKZqa2?YIMX;+OF*yc_Sqd+{rH zAAS|@$FJc7_;q{;AIBf!Q}{Ig0-wiU;tTj2d>Q|Yf5E@vKL`ooMGPi}5P?Jx5lkqE z2qKb*B8C(3L;{gQ7>HD26p=v~iCkg|VIyV`4&nx)nQ##;#C&1_v5>fpSW4VOJVHE5 zJWgySb`X1ry~HcT8^oK$A>st_4)HGW32~M-cLS2K1e=BK0$Vn>&U0bXUG@G9psDT%j6#Neexvv0r??$ ziabqzM1D+uLY^T%B|jt2lIO_J$uG$BSbyd zwVQgC+E2Yk9iTp@KB3M~pHiPuXQ^}4=hPR}dFo5*YwA1d2kI(K&=PtOEu(#DIUP)g z(4n-FR?#{-nvS9MbON17r_$+k7F|l0(WB`xbU9r?kEO@a<7pdRP1n%1^bC3~&C$2f zOX+*)2k1xW74+luO8NzQ2mK=b61|gtnchY3ruWc$=~w7|^a1(^{Vx3;{XYE(eTM#u zzDR#fe?woUuQD{lFdmF2Gl=nG1~UPSoEgG|Fe*mP#4xeUP$rIvXADd#Gm5b=c}zZ2 zz!Wk?%vfd|GoG2i)G~HvI#b8YU|N_~W)?G>nZqn)mN9oRcQf}e_cHe}_cISL%bCZR zRm=uvBl8rqjoHq;!0cdlGkciVnS*0>jji?d$B{SkK~f|`{%FW}Yx%I5_BK}~{O;02 z<8(FkHkYdd`654-U?m+W0LfXB!3HsJ$aW?Z0CmU;8Kwp>F%&<4uVXb3Y8_jlCi=)xj0{UD~)GeUFTbu3U94<$- zqu$ZdRwav=?rg5H=d?QNYoQ-weSPnWu7c@SyS>(43ku&okMWMiTIXz-ihpjSt-9V` zF{9aTtIf03*V~(+zYy`cvANmSSO*hnY%$xbTk8rMr#q`;zAz_~v$4fKr$t;MbQ2&> zyYFnPWbwr|SBuHn&{S`4vDcRIM#bcD`x|$WaFkg>g{R6hYug?f@uz~vA5c04tH;t z`*TBEjkDH1yrIq5)HFQb*4%2gH98uG7nGI{ALp>ob`3AFIT|Z$)%=HHuGx<1EyvI} zG#*Vr6DvhlZ?rYoCy0OAU1P1~K=TfGTsgs7UMkq;-V>fGOfIlWHs)VWt^{UX?;0*_ zyG@)}?{R8`ae}L4qyObN-CF|t(|?#5!Z4v#va)|UjKF=3Ej_#3doc~dD2ghX^X(KtsVFy~k)4#!&vdF}gPkh5-6BH9K$UJ;rQd4E4X`%)?%x7)>X+;#j3*3q%znc zPz~j+O}whOexN>es$>a&HAq3Dqs3vX?>klZ(wCt}kYNL;le7o*_k((Q5R{As zlu8V!mZ7Yk9S%w+0aT6u2J|RejvhlR(Bo(&dIGIN?WluIVhh+p_9k`}yM=w0eTCh} z9%plUQ};=My0KNV(m8sEI2*lPFctdRE=vc>sJhOIyLVXR^QS4Co*Fh=}8+q9g-gp|b8 zkqP?LkrvproWvac$Rv|RZ%j?hf$yY|@yRK@7xI!YNa8;mWJF?as>xtV(i;-vbM?t7 z3Ay@_DV7oXcvJGol;ot8l#!MsH-GIy2asVM+Ku+0z33IR550=^qu1DEHib2?sq6@L zB%8*juS2h+gXj(PCOU);qa)}ob`+b*X0h3X_~T z^EJsOh#OxD##vTtZ?QS*c@5C)GtMkd#xy##bB4mJ8D~IOmWrJJ8B$_ zb$T0EU1%%zq~{~et|BS8Y%LaGQr>z)r72E`FK2+S=^;J&6D3-pS9=7s#+` z6`LatbOC(@4o&w^W(T+y&PH2vTPM1JE}}}9MNgT#k8jW=WN1g3*dn%=En%%}DO<*lX2-DQ zYy~@ZBbK2Q@JjVK0L$?Z-b=;7;F*pC=d%*NCb9Kw1Mi@+i$r%cq(=jbYHygxJFr7l zvatRSXE0emSFaB1 z9gQ&cfmd&AZJFV0hB@_r%$qxlv%VI@1bpRwQ=Z*yYbu7GMaIF=sH7dou;W>cz(_a_ zm8=GO312`V@k=8^o*rHNfem;h8q$GN@d$P@JEa4s;dFK?Tg%5p5IWSi*u*6k_ualK znW_wSw659N+E_cr?t%crbzK$r0NL2Y4`9SOY!y4L1DkO!Yh$a~nyGH%Q*3XnYnd^L z-=G2zg*kSY>@9KnMW|#IzZBlDy@;(HykxoiEyERPNGBeR$KZ0-&Q53RI`LRM4v%MN zus5(X`2`H`w_Q!$ldf>KnH?_vbC1wh$s+%vg}BUeh_HZ;;r%MQ9JTgpTl28lylOj! zrywbwimN~m3~Oz5)K<5(*j=SE*>iE4<+prRq~T7y^YY_ub$eNdusUqR)#JcJA6sDV zz&2EQ4A(+?d;j*4agPt3$CSRa)Z?aaBeoyEk#%UZYgY-HO1noT{C2A>d;`7_8P?*N zxE?p)M(o5*Y$NMro7fxKX4bV9H-j9v;8r{f&t_XddRy7K>^ycMua^7-kEV;`UBfD! z&IVx4zJ=u=w!B-M!}9>}Zb*0aE}=Ks8fV#Dyu5(!>f5%=(bxz?V-ooC>-p1a}-of6+_2*OU#AnZ?c;A{9t z=x%lGHsom1rA(r98R zGIS6zL@c|W-OxeA5qfqbyNS&iFD}S6PTWIrNs0WDlGvwM&BX4r3%BV@>bnErfDj{y zkq|ZX*|Y0u&`G2b>Ab7{*VQ0$09XF{#sd!RURfrQ)tB3H2vgqz3o#mw68iY{19|mb zKK%w_jA${4aahjV3!)P2#dBTuVmsIi-k;yYTMKcsrt+Ir#XjGqw}@)cTSN^}%YU<5 z*=@qNM&={)bWeFE(FpQD)DsQt3+#>#!bvo-FS0NHXPza|O3dL0m<0m-5(qHS2A<{4 z0X$1ze8@egMa0d#x&xOHkLF!Q*4{}h#^Z@w*j=FPcC$6dh}(g&@93{b)_xrJ@NTBW zdf%iZ|FZG>GePAL%b@CA{j2`G`qaY6Q^v4%)#y{T8C#cis~qB9;sH@b+z%>ZFZcsQ z8_aj-pQ?!E#AAJlypmY|kBGdXi^!Y1i2N)N`PD8WzuraUqdi34#uIrvyWdUZ7kMJT zMC|0h+1J~e7e)R>(-Y?ue(pzN%j!=OX8wP|S*-Pw||I8njtdw-{`~~I#_zTPf@E35`YRMYluJ72( zJb$eNrdi*gY2Ix5aB2SjeEpNk^v@Stcl^%t*Hch+Q~#=E>n?kBuE^PTf7DwqT|Ain za1VbyBYBReUXJ8hb{hNr-x>(X4#`fD+AjgMul{}gS0r!#FXzFMLz2UiBfZXpudzR| zKleTl7MudfyIn`GaPKZT`9Gbjx(Rt&@(Is&_n~{?f@5fhe&`?!3=Eu{d%#a0{s?hjG}0!=4>vJek1ZK^*q} z&m@R6kZHUG^^PdXbaE6P&tWeP4;Jh%nT@2RvAYv7SWIkCW$Pih` zPUEo8-v(mjXcA5m;gp6fheH}H`}+o!FF7Sw&7GZ5;OFus??b zy4jelCGCA&IC2K*=vz=vHuNoMBAY?clQ(i0e!4gu$kt4OQ)nw(=a;uM!$ELQxFU!E zIS1GPKrIp;f*_a~az43`ziEK7c5)GiL)>hA6S-I@<#4EQ?nvH3-a57W7DKq2?IdsM zbvaAkPA&s-l6R1Il1n+P;BYvHm7U~Ww99D4{&?%8T_k_a~`=K>N@P!MIe^s)S zfhzEz4M0KE_C<5#w22Lcc98?M>sy~6z`gW`NVrlJWVfAsl*4LRI=KRP=y7r-kICR8 zR8R-MReF~^RLEiRaG6|1w!`tO8PFabrQeL`LqDo5DD*I;}-IHP+#P;4%#;%7T)ltUJ7|!}@>@(oV(%!5W*Wh%e!P&XHuxnvX zm$=n0kt@Y5uD+7XZ?UldyM)4DO~x^2Z-GMhQudOE(2xz}E95@%RdPT18hL zqJ9s?I{?#T=MPb2VvBNf(KuVP0}ht2>)U-i0~SN9*&8l3vAc2iV{t|nTZ5w>WJcb5 zJ_52vgf&R7=OkJGlFS}LURV2n;*YTcbHY)@b&V4Z!&1hHY*8uxN*EEB;3nq<@>{@i zkYAA($*;+8IGoAhEDmRL*tnX!M1DtJCa-Wfhr=cgn>ldzfm?@weD_6WRXAFB8U^)x zFiwOT5A>+CiNAmag2eZ0)ddQJ$K|aJ4RC8G3RAhgjz^oh#tK-raS5rGgoGs71yQB` zMlS9oe<%N-5Qi-sF6M9<=*+HFQWA;*13-}!MbRA2<8VHQ3pyze%9FgrVNfeYut)PTN!tJm&Tf7`WqgC$7`%yy% zm^^N#f~a7&g~KHrwz4@jNEJaK)iFweq*OSiOl*8w`Y3INv7oT1xTJjigvnK1_)c#hsm#|8z-wL06OnCj z!Deg$Wm(+{7i@s*3}Ph{%Uf$|1Qg;9D+U!Vimwj)O zv5W_xJOM{HWEcR`0EEy6D8z=b1^gH}ph1uO42{zd8xD91aonDMyE!=I`X{{>IK+>| zi%HglUs*y*XlG(lGAqDII}NEL1W2iz^%sx{{DAx-x*NllfwkOJ!IxX?HXhaqmA7Tg#x zrs7)pO&68|EhfUztnB*T?U*!XfKsSEWoj31bW9loAEyCC#jh8))rx%BQ4P%pD2Mz8 zUlan^-xz@MrUJaH7=WSW$cb7ZM_~zMCp-#BuEznzwH`eKnF;#3+E7&V-Vho4~l$8vZa$A=FS zgg?gddR6n+3Xo^8zpVt*f=W?;-4kD_yrqpdM}JeZ0vt=BrZ(F{+#VSgJRd5(%N;uF zrGw>ZoHAd?VR1tCiVgBrRPeI}e)53tH1KDVRl|`Hl?Yad$|4u9>ScCvs3M5oDHCO; zaw!XyN99um6j+-{9G=YKDIA{4;VKSK<1j=b)$5R+Dxs{rk3{A2KY6#PhQqa@gEU15 zEnzR^umd6hy1-=tAbmNwJod(^-KCahP;phVOuoR4kO$mxFW5wKvc-~^Y%=L{j24qV zIVp7nAktDSdb7oxoMTFgA8CwF2IwYq0E)B3Q3qDcUMrXDf%yO9H@U0xa zjU$4fjDXM$v=hvqhg(=}vmAAz3&0n3f8?QY0G5ZMu`N7I-(eT+li0GL(bXhq+}2J#!Qr`lB>ktYAo!#@snygPxY`F?7%wknJXjL)8Jg`b@MQ%s3y0@(cwVV5 z-gcdzrqTf+;p(@=2<{Q!%xih&Hqim@i7@Xe;x(=aMRZLCcL%_5Wlk5cle00251GF( z@5)}sNd4u3Az|UiJUj>aR5aOIW=wa$eHN$8tmP3I8hF{3S)Uba)Bnq;JZx~ zz>YlpP8d+=GvkLHfieW01^BglL9HQvLa}%Kl5AQ+x1 z{5re_L#a|MUE2EgnRYdlYy4KjM6|#={c{YFn3NeH1-a{_#p!QO)a2Pb(EnutxSlVai-GjLM7zX+R z>}lW3g#NWgw>oNO=G*I=y7|(*#Mj}5&aiI3RS>J)XF!}qh`SUt$$hdBH& zhc|E-!uFpz{0m0}@>`~I>)#&xP!925ivZOu1M#nt#pc-I_yhpD4G^I^T+T*8W5Tgs zbCLelcCjDeYq-o*VZ-xV6Pi!D{?-Z7WPqi;KE!tH-RI8h`w&aB+QtX zov9_ZIe-A13t)8K(*QFDD!OY%LS@lLs=DR?xK;-r#PJJ;V4a_d2y;D7U7!G*M2*CW z)I}J`THd`MBAgXeZLc!H+2Qa5qHKRdeG6$}-3>aZOQ^DUft%2msVm~iunfIR{YYJ- zeu5EyrhcJ*rGBG+r~aT3f`kj%^+1xTJyIyrN8W8t$3U>nv!7TFEgK+-tfzVvcuKr3 zFP-l0v*#IyJ2?yos%tsCp2Lr@698Y^&FG?$1C7MP*u`FZYS0 z;>(Wp1*ml`QZ7Bu%j!P}P%gCXy(LPM!pFb2DKlokAOc`+F6Edsha>T>uY;spLW) zNt@Vm1P%Br9**CK*?K75S1EMRY5ZdU=lUXVjv^Pg(-|Dz*b{Zp*>oO+thABNp-r@z z&ZR9Jeu~4JIJ}v|V6&d#@RqemPZ!XI@Kr3vS#e$ z8e_k)6=ELHn8Fd{BwlQa9F4W$efCm+Q+hOELJ|x=hlgPv{BsWN^#riF73mA>&pK!=1tQPI?MG73Xpo4CY0E3czIjphuU{l@G2Z z*gU>F(4}&>9e{^Dq6w$nB9^;PshEvG+u>e{h6rc}t2s$K=o{#nbUobwtky_7=_cBn zZl+yy3q6&dMbAbS8qT|eYr7KC`p}p^V*`sk9TXQI8uHFa*WF8Bn#0;%Lj2gBjRR*= zjY7=SrJAm2J0D8WQhPbPkHas*hI0684u8e^ zbNH1$Dx982&v&bE4)5XcOT9F!$cs1edh%v^F};KazhWncU*_;G4)4BBPj;(Cw_@t6 zw%nzC)YU&wRsKz_)sUqnX4vXoLax_8O~Z8a-xKskG-M;aif*Sn=uUbyy@p;(KS{5n z*VEv3yvpJI9Da?%2RQsXhYxc24GzD_;X@og%;6&&>8BtE363%7XXq_hz?RTk`8)Er zK*EG8pQ9W;B?3%1{3R4YDAhk? zwA4TSNsQ(MoiXhHMJ5vcDlhv$&$qjv3H0m0&i(ObZq|5J@^D^T_O{!ohk z$T&Auexw1KxR$;~|3v>x|3d#t|Hk3d9R7&IA9MH<4xi!hrvm7W!3@Dja4QdbK0QQYHCW4dLoxbz$Q;GgO73hCSB>JmFq^@N%-i!?Rgz;g(zdOs}bN>XU#{@D# zeUgBfP(}e~JWLpe&;JE*kI^s@0t$uE@hFrFf6km0)y`TlmAaI~oW$IOlw5tTB|b-= zY>ZFTk2IKb^tm~P)a2ZerqmIM@q*{X=$T<)r@?a)9fDqVy@MIfdr1R6bMHn1lLXN@ zlgQz3+nHnzU*f6i3l3X%P$hT>g3)3|Ksd~dWYReN9fyDH@uHXv#t5RsWHMPyHis{B z_zH)=?__dN3S;Il00KFD6>u)lZI5#?ng7&tQY(Xxd<<7&tAi}|)>`|-?j>onADGko z7=S5e$_5&4Gz?eHL81o?_Y*Ikc~I!#S8nez+T$nM^&?z!8`u2#%0&1i2dQUmU)MQo!I%0KFx=`Xy9=9e{fS`(&TZ z`!9aC5}dvveX0n(^WK+xu5ApQ^R8p& zGV_@E%mQX1vxs3Cjw5J}U^v2qBRn}`5Jz}%#9)r_h6@Sd3v9Uf1?%9y#4KX|FJLVG zBD_0btc6Pkm$Tl^Q>NHa-OL{Z2=9<_NALWd!hp5;+ZgZ#8=CsokNvayWd-I6c-E)$ zxIe300y*xr;E49E&Rg+;=K?#{x9XoxyS%m9C0rQytv~$Fx`x+O@JG44i`uuvpU&NV z3e~&1o170a5A%}#5JyPcnMXKCOZp2*U%^ZId4xV)d zwuQZscXv<>uMM)-=F~fDW`fu#CyeQS!rzm@(L8_A)qM;Dcf-L*12D}5xbw?`d*N|t zGMY-RfVY=DO>PG;)IstH`8MF1-h+3Sor1TP{Qz$ui-BuD3tagXQpJ>&Dx=0w74Yt{ z3Gm*r$?(pxHh9n20=RFv3GP^Kg*S~YrS66cj|bshW6P-(a2$GqIzxNV@essLpj+wN z!3jT3pQC?eD0pAj5GDxT*A>Qu!#le)jE+eF4W0{c>MDS@bggDK!MnEhG6$GL%sb4d z%vt7h=DbISN1;ctht;FZW3|T?kLNtLdTjR`>28*@ND#)=efXhktgSQ%=2T< zGoGJ$o*R@oC~r{dpo&4ZK{pJlAJjOgX;AZ^d4m=VS~Q3ow0O`hgSHJi;}z;v>NVNx zMlY9FtJiF=Hm`YJ3%nM2ab7ojE%93JwZdzq*D9|Luhm{_z1Dec@OsK?v)40TFMI9w z+UvD%uzav)ux@bV;ON2J;JXIjGx)y24-7sx`10WI2VWh0&AZThy!S-!N#0YupZ4DA zec1ag@1x$wz2EVE&-T= z$4D!r7~?*`vZzEAsZ@qN*EpYML(1HK1+-}F7~`nv-suvmH3tVjrJ?|tM+U3Yw~OJo9j2eh>St^jqcE;kVjvqu(aK zr~S70J?D4M-^YKr{|NtF{|Ww;{x$wL_&fdQ_%HE)#DBSer~gy_JN#esf8GBL|3m&q z{NMIJ=Krz(8UN4x&-s7h|E2#|{$B?W0qTIvfXM+11C|GL20Rn6HDG(dj(}YOdjehw zcs1Z?z()a}1zZgHI^gGk-va)S6LL~c%l+jFd8~Y>JWf7Lo-WUp=g7_S0(p_VL|!T% zE3cK`D8Eg9hkU90F8Mw3`{WPEACf;JUoKxEUn$=z-!9)Fe@Xtbe7AhBe4l*3{DAzR z{7v~``Gp~#L-a#Rhq#73I^@+M=L4C*$UswIabRiS=)m&8$$|F3xIfx1A~KtgM&kZql5Lq z!-Ero4Z$OV(}G6@7X?oUt_rpV*96;x>w_DEn}VBzTY@`-j|Kk{LWVFQo*`Z#VIh$r z@gbu^GDEUMaze}@4~DD^*%Ddgpl-601;j)c4y@RCd@O;D-6I|VahOdm^LgTEGjG} zY-resu#B**Fk_e{EI+I;tT@aOwlwU~u$5t}!aBm93|k+zF>F)V&amBKhr*7By%Y9c z*vYU@!afZ<8}@nF`LOT8epmP^{1t!zQ;bq%DvA_kiZO}`#YDv<#S}%AqERtVF<-Gj zu}HC0(XQxJtWi9vSg+Wq*ra$`u|@HmVyof}#UaHJ#oLNwiW7==74IuPP@GbHr1(Vf zrQ)LE8^tBXPvJy38BT|LgbxZI9PSe?3s;0|!gb-1;X}jq;lsld!mZ&qhA#}iIeba@ zt>H_PUU{(>&iEjhm`LqKT>|8{8V{X`ML7E@`CcB@+al*Dx@M* zjLK8xrSewAs!CLqsw$ODRim1rxAJ8jVg9sfpHPXtFd$jY*TM$h zxn{DaN@LU1XzDesn%SB*%{t@&PaRdY@A zvleS5T1v}kJ+*#Xr8ZHUtj*9CYNu$cwDsB>wJvR|cCL25cA=Kl-mP7sZP#{c*Jw9r zpVDsDKBL{OJ)}LY{aAZO`> zw(4Hcy{Ox%+oe08JE1$N`%rgU_o?oj?!4}T?uzcJ?waoB2x){aA|WC_qA;R3!WvN) zF(#rSVqC<8h{}k`5mO^tBj!ZRjhG*?FoKJ?Ibun~tr53JERDD;;+}{{BUVH_5z!v8 zA>z4+tr6QJc0{}s@p8l)5pP8ti+CsEgNV}+pG15bNkwWRhesMBM?|JYj*2uzS|SS~ ziy})R$3@me&WfB9$wl53*&f*$`Ap>2$nB9YM(&Q>8@VrXf8>eCGm)Q1o{#)0@>1mG z$nPVsMp03IQNdB6QHm&KRAf|4R9w{XsD!A@sH~{$sGO+MsIsWhQRPw7qHIysQMFMw zMzuuEj+z^_AZk(6vZ#lnRz-D2t%-Uv>iMYcQ9Gh`M(vK;8}ξi$KwjzxVKbs_4z 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 + predicate: #Predicate { $0.endTime == nil }, + sortBy: [SortDescriptor(\.startTime, order: .forward)] + ) + activeSessions = (try? context.fetch(descriptor)) ?? [] + } + + /// Returns the running session belonging to `action`, if any. + func runningSession(for action: Action, context: ModelContext) -> TimeSession? { + let actionID = action.id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.endTime == nil && $0.action?.id == actionID } + ) + return try? context.fetch(descriptor).first + } + + /// Whether the given action currently has a running session. + func isTracking(_ action: Action) -> Bool { + activeSessions.contains { $0.action?.id == action.id } + } + + // MARK: - Persistence + + private func save(_ context: ModelContext) { + do { + try context.save() + } catch { + // Persistence failures are non-fatal for the in-flight UI; surface in logs. + print("TrackingViewModel save failed: \(error)") + } + } +} diff --git a/myApp/HaruDanim/IOS/Views/MainDashboardView.swift b/myApp/HaruDanim/IOS/Views/MainDashboardView.swift new file mode 100644 index 0000000..db9c3ea --- /dev/null +++ b/myApp/HaruDanim/IOS/Views/MainDashboardView.swift @@ -0,0 +1,207 @@ +// +// MainDashboardView.swift +// HaruDanim +// +// The main dashboard: shows currently running time sessions and a grid of +// Actions the user can tap to start/stop tracking or add a count. +// + +import SwiftUI +import SwiftData + +struct MainDashboardView: View { + @Environment(\.modelContext) private var context + @Query(sort: \Action.createdAt, order: .forward) private var actions: [Action] + + @State private var viewModel = TrackingViewModel() + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12) + ] + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + activeSection + actionsSection + } + .padding() + } + .navigationTitle("메인") + .onAppear { viewModel.refreshActiveSessions(context: context) } + } + } + + // MARK: - Currently tracking + + @ViewBuilder + private var activeSection: some View { + if !viewModel.activeSessions.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("현재 진행 중") + .font(.headline) + + ForEach(viewModel.activeSessions) { session in + ActiveSessionRow(session: session) { + viewModel.stopSession(session, context: context) + } + } + } + } + } + + // MARK: - Actions grid + + @ViewBuilder + private var actionsSection: some View { + if actions.isEmpty { + Text("등록된 행동이 없습니다.") + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 40) + } else { + LazyVGrid(columns: columns, spacing: 12) { + ForEach(actions) { action in + ActionTile( + action: action, + isTracking: viewModel.isTracking(action) + ) { + tapped(action) + } + } + } + } + } + + private func tapped(_ action: Action) { + switch action.type { + case .time: + viewModel.toggleTimeTracking(for: action, context: context) + case .count: + viewModel.incrementCount(for: action, context: context) + } + } +} + +// MARK: - Active session row + +private struct ActiveSessionRow: View { + let session: TimeSession + let onStop: () -> Void + + var body: some View { + HStack(spacing: 12) { + Image(systemName: session.action?.iconName ?? "circle") + .font(.title3) + .foregroundStyle(Color.brandPrimary) + + VStack(alignment: .leading, spacing: 2) { + Text(session.action?.name ?? "행동") + .font(.subheadline.weight(.semibold)) + TimelineView(.periodic(from: session.startTime, by: 1)) { _ in + Text(elapsedString) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Button(action: onStop) { + Image(systemName: "stop.fill") + .font(.title3) + .foregroundStyle(.white) + .frame(width: 40, height: 40) + .background(Color.brandPrimary, in: Circle()) + } + .buttonStyle(.plain) + } + .padding(12) + .background(Color.brandPrimary.opacity(0.12), in: RoundedRectangle(cornerRadius: 14)) + } + + private var elapsedString: String { + let seconds = Int(Date.now.timeIntervalSince(session.startTime)) + let h = seconds / 3600 + let m = (seconds % 3600) / 60 + let s = seconds % 60 + return String(format: "%02d:%02d:%02d", h, m, s) + } +} + +// MARK: - Action tile + +private struct ActionTile: View { + let action: Action + let isTracking: Bool + let onTap: () -> Void + + private var tileColor: Color { + Color(hex: action.tags.first?.colorHex ?? "#2F6B4F") + } + + var body: some View { + Button(action: onTap) { + VStack(spacing: 10) { + Image(systemName: action.iconName) + .font(.system(size: 30)) + Text(action.name) + .font(.subheadline.weight(.semibold)) + .lineLimit(1) + if action.type == .time && isTracking { + Text("진행 중") + .font(.caption2.weight(.bold)) + } + } + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .frame(height: 120) + .background(tileColor, in: RoundedRectangle(cornerRadius: 18)) + .overlay { + if isTracking { + RoundedRectangle(cornerRadius: 18) + .strokeBorder(.white, lineWidth: 3) + } + } + } + .buttonStyle(.plain) + } +} + +// MARK: - Color helpers + +extension Color { + /// App primary green (adapts light/dark via asset-free literal fallback). + static let brandPrimary = Color(hex: "#2F6B4F") + + /// Builds a `Color` from a `#RRGGBB` (or `RRGGBB`) hex string. + init(hex: String) { + let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var value: UInt64 = 0 + Scanner(string: cleaned).scanHexInt64(&value) + + let r, g, b, a: Double + switch cleaned.count { + case 6: + r = Double((value & 0xFF0000) >> 16) / 255 + g = Double((value & 0x00FF00) >> 8) / 255 + b = Double(value & 0x0000FF) / 255 + a = 1 + case 8: + r = Double((value & 0xFF000000) >> 24) / 255 + g = Double((value & 0x00FF0000) >> 16) / 255 + b = Double((value & 0x0000FF00) >> 8) / 255 + a = Double(value & 0x000000FF) / 255 + default: + r = 0.18; g = 0.42; b = 0.31; a = 1 // fallback to primary green + } + self.init(.sRGB, red: r, green: g, blue: b, opacity: a) + } +} + +#Preview { + MainDashboardView() + .modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true) +} diff --git a/myApp/HaruDanim/IOS/Views/MainTabView.swift b/myApp/HaruDanim/IOS/Views/MainTabView.swift new file mode 100644 index 0000000..b9480c4 --- /dev/null +++ b/myApp/HaruDanim/IOS/Views/MainTabView.swift @@ -0,0 +1,48 @@ +// +// MainTabView.swift +// HaruDanim +// +// Root tab navigation hosting the app's six top-level sections. +// + +import SwiftUI + +struct MainTabView: View { + var body: some View { + TabView { + MainDashboardView() + .tabItem { + Label("메인", systemImage: "house") + } + + Text("행동") + .tabItem { + Label("행동", systemImage: "bolt") + } + + Text("꼬리표") + .tabItem { + Label("꼬리표", systemImage: "tag") + } + + Text("목표") + .tabItem { + Label("목표", systemImage: "flag") + } + + Text("기록") + .tabItem { + Label("기록", systemImage: "chart.bar") + } + + Text("설정") + .tabItem { + Label("설정", systemImage: "gearshape") + } + } + } +} + +#Preview { + MainTabView() +}