From 4a6bee28f6a04a1b89212285b09cb4eb2e31a6f0 Mon Sep 17 00:00:00 2001 From: JANGHYUNn Date: Thu, 7 Dec 2023 11:17:52 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | Bin 4838 -> 4836 bytes .../화면구성도_다이어그램.png | Bin 27031 -> 0 bytes .../images/화면구성도다이어그램.png | Bin 0 -> 34178 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/assets/images/화면구성도_다이어그램.png create mode 100644 src/assets/images/화면구성도다이어그램.png diff --git a/README.md b/README.md index 70358ebd5d45d43518e9a82714ccdd0d27effb57..be77ae4d273a915978dd9018525ebbc23fbc7671 100644 GIT binary patch delta 11 ScmaE+`b2fZ8KKGNgd_kXZv`#@ delta 14 VcmaE&`b>4h86l>4hRJ7yBmgj$1&sgz diff --git a/src/assets/images/화면구성도_다이어그램.png b/src/assets/images/화면구성도_다이어그램.png deleted file mode 100644 index 9783ca09b7678e109661eafeaf6b4b0b09113b79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27031 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 diff --git a/src/assets/images/화면구성도다이어그램.png b/src/assets/images/화면구성도다이어그램.png new file mode 100644 index 0000000000000000000000000000000000000000..45db3125ec668119c4576da04c873b28b0e07860 GIT binary patch literal 34178 zcmeFZbyQVh^d<}lQi=is0us_vS3p1-MMN4wI<9nsbV-RyNp~YD-Q7w^Np~vU0vC{; z_h5eC{AT8x_5IeYnYHE*SofZL;yv$q_ukKb_Op+#oQ&92ED|gvB&4h2Paer5A)y?C zzq=Th!IoR3pu{f)-~0)rPR?k zG`0|++Nx=wqBPbMpi<_PzAODgMBm8xiLba`jlL(cUkXJ31U(5Tk9F{%0GJi z>tOIrfXc|$_608t=HTGK>cGKjX>AB&=rh^ZXZkp4EhDe#(Ecau{-pQ zSI*c`-%RC^vAMp54LGL&6*m{#|La%Pe}3iUx=RIv?g#ekhQQ3g{!9({@@I1OEx?Rg zgQ z?r+t9LgTHrYqe(gs9pHptno-XKQAYdp5c2VHnDECcF7C=dSSws2m1TMF&^<_e(lG>+^8;iR}!I@&~70jL+|GB84X1ub;cyB&tJ^Dc9`%s{3*jS|-U$ z)dObwlxFVcy2nbg_(FKc3M|!Q9}g$Xw<8{;DYFlEghq>S`ViB+zx^8XLv-c@mCpH( zwzV?`tmmVb+UTV8zV_DKy>o>}&qufL`STlYI#-dilSg;APC3g?G48rbz{xnv6g!l7 zE?@4DHvfUNEQHRg74u~;DNjOiY#PJls?z=5y~S#nh{qDv2dfcRSd~S9{pcZ*hNUaY z4zf-OlIXqbNJzp+;*TCYb3|UB!i>H#RP}w^AvuQl`E#U@7K~RO7_a`&HTy zaB4*Th<2QcBJDOzp`$a|44|X4aO(O-!pY_)NM8fII6#b>{M*NQ5{Cn0=qE|Nt(O~e~V6=q4)+#!+X-pp7R!r~bb@#Ne`(V1Eu zg^4NT3^xbyxExhUiRzSEX(hl6GdK6qQJbw2OTTwADhdXr2hy*9uf;uRgrxFBHF1TJ zkWsH+{QG|UdY{6neXrHAVW$4E(cE~O+1#>q{^~q?WL{X#Mv2`(o5p4J0u7x}ea)6) ztHR>qc8Vei3w$B;9LJFPp8w8A`UJ&hExrQm&9M@(?3|pIma;99wxQ%A2?+_Q;v`e4 zfQ6|=vpMYrI1W>8Ey}u=R>0t#O8WlxmzIv45a(MtgM!mlDo&j8LAnv+ZG07SO1N85 zW_x50IwM&UUTzQ9KYg*?qW-4uK34`2ROhd|MAp_<@S?~;i;(lB7#*2Kv2v{d+gu1@h`XlSSrnl+gfvEc+{R1ai?i|`{`trjsB;%SADr(W$|E^T1=_E#UW$7bot8XJAn?` zQLat;%z3G6j?)`o3?}tmHCjJ1mJHjPCykAhM%k-UL=}A;Eufk%dF1>y^DU)DR+e5QAktBlwsHYh`KV<&|oCr z5nSUzuAr2wa=XN^Co+b^Fjn@?(caq9e3}1T#QA;`7E^(6(X9#1mwu`YInf-4)2%wU zAmgRvL$2dDI6|!S*SR=U{rKiDzFDFxRUKacd4FY~?L<0}Uv;r7hB_i5qW`^!cUtG& zOW-I5F3&F3*UVgGayg8RH8D&s>Qy=&V zPXm~6=*)Ut^wmd5o)58ewaUgdG6rfKwjLH#FArxL?xEw{VlW-aXQ?(f^n&((hC*|g zWgG1coQ~e@Lem3Q9GtGZ+<5HnFMkEDphG~}4jYa%toiH4V)KB}mYAz?UHR?Tgp*a!p6vLS zEsR2RUiBLCw}6lA2X#1=~PXH3nbBb@ywEjLPBG^`FPM z7K&mE((IKxy>I0yKMrGk>lz&21cy^L>@20Fe>B=#APpTYnaiI}AFbT)6VjavWl=A` zhgsjL5-<{acq8{DPWw#+LE&+UYhtIa+;K!Avo=GmhyMsho*npRk>qdA{MPk4llyVf z1~wuGp`~5=%~s*aKK2SaDr-53YPlUQEIT(G)NRG+Z`N!HR?P+Sb!~*V1yNY)#Jg-(E$&;D7q+?|tz?SM z+|YQ@gtmCqQ%3h^a0)~+-0>P997KgK$NFOMsaU&t#S~o}w`C+IKHcYUE&koa2NRYJ zQ#6|j8B!s!u_R7k-w<`t-mkIW2#vF>SGA~GAl&`X2?t}ap7liM%8qLtn<{61j+5F_rqrN5V<$OH->nRI*0R}g-+lSSlO6{;qb{~eg9n5lE>v1Q;ZeU&N(p! zQ712V+Ufg&Q;6Q#Z~q)ZEj{M4Ri~U^yCcCDBL+QlPb7@sdGuu54=CVq%Q1NB1_=zu zmyN6~&p5BKl&IAixc4SM=+BTQC1%rkZM!wCiFkK*m3xvBn~?b~{L7G}yHbIcdHzy` z-O$?h-uTIu+d)mSUOV1@boN`WGmHHxhY?hUqdv9d=d{wz2 zR95Kp8EYLvp5uJXFo|bp+G?SdQ->8)7Gtuj`Jfj+B{D7$H&_?;FpE{17RV8C!E@k}B{i1+aMDc?z<<&o+R*z0NG%1EC{ckY1j!_HlcQ(!k z>t6^L1GX7xh5rpL!j#`ZZ1J?DV)Xe9kin!IfNe2OPe~v{_!%TT8-b@nulM~x`5XO! zd5pmfo;O?+{r@*O;ZanzG85V%#Ubq+d!sdVMMYYZFBcE#I{=|)%7pKrhbV(uZNI^2 zI-DOO9mmDvD;7M60w8eMmju2{)@BVb%;e9gz**)tje#$p8-&l#PP-+5$BaCWAUGs3 z=#Hgb8!3pYcRePoLX8Uox7v*LNGECMB2rQ+d)jY|Lnf;azyjT1G@CAjPO9GOI|T`w zo9AsOu<5>Cj?12|b8%T@_k-q1I8>GbnwcHSyLz9m<16ByzQHDvIIx&X&#h&?X3AI} z?6SWO`A>0I*Saet_ntfsA_o!0?2aHtIpk2D+`gWbljHwo*y@-lA$q|lz?7ooiY}pz zBK_4x#>jvwBprVJbO7hwkxnJnE+pZ1EJUmhNk+12GOi8h zg~i6kKAs3Md1awX6M9?~{r2tUu%w$fH;U?VnGa-X5m!Z>oT}1Xc5xLvE9v(gwaiW#18=8hsvItT z#A0GfcD)T5VM@x!;Tf7f=fXW1|1VuHniF07FIDZ3*Y98RmVl9`Qd%jRWTL=aJ z!jBzHa?5iOk|jyF`ME`1{M%Pyje}Qi(_KkNwc$Q?^^OJ1f2W$IRTolHHw?c=pH2kWpi$ z0QzfRR%QFg%nosZg9~DM`j2rNv4bnnr5d*U;{)c!Kz=5a|LDRI-zhX6$dDV3leidG zGk}?G&fovyLL|>#V9(z5m}5xb;*>6)1WZbw3$;J#t&bM5TTb)iKWX#6M#0)6aG$<}n8e$y*72s|qglfjsn!T`$VGG9>PgN83W4BUw7 zKoR6JdihcFE5Bi_jF*{qwZ=QIX7vN0A8pKk0aGx7fLn4L_lt#Gl@sp$&B{4F5DNQ7 zFQMav+w=%tL%rU!tANMAKyhsezqwMVLl+MASA#ppqvPV=Q%cW*wwN@no+ED|%E3+wD zB%PvDg~fSc|Ii7#(YYY-vXF9HgpZGpBS;tTyl>0i*|F{kUw^!|wkBF9At|YF`9~{v zy>|V}``+{AN8TUDfVk$l$4N2A^2?940102HOB|zf+G`)yyQw%SU=#?`V&>}($!~q z6Wt@4X*Gx2^qC7H=nia_zC>Il<)WGP@sJjn4;GOMSkJq6NvZDfknvP?S=FUpfayrO zJMCX}*3Ys>S_G{*j**NlXh4VOnqeQh*qU!y=Cw;!>Bu$S3Hx3+{__RV5=Y%&hRQ{WA5GFmnFwGJ&QE<$E!k$_Z{QCG}MOI@}= z$S#5tJhaF;T#sy5rt4}nUT(M09E`EiX-dHLju9)mXLPcO%r=W%AsrnOE4}YU`t_qd z!BIEjgbj!u62{?7_=9gC-2#}%#{9U_YVOJRlJA{*Aqp})EjMAB)%U_$7@%G5xd7ua zm}xen1P7}<-^(6aWKWoepM^4uxS=6(>!(NDH^$2jHF5i(MHLLKx!k4?tN>lZpO0*6 z_Jb%gt6Um0D3|B3H9aL&{kBgE1f3uaDy9S!-%0JX2N{OIl*rnKSCr@J`@!@N4ybL{;?`FJOTR zN9imM1tPav3a$|`=aCT+Jf1_`6s)VSBhGO=36!)vbw<(EOJ_yoE#rtx;&p3;R=|dK z;*&kJ9VT}fHO6;J@cP@n0(_gRntmX-o^o)pjuq=mhRxQz8**fck!|W|AXZXQMXnK= zSgPW`Tn4wwl3~q@JuPO{qPE?5u%TS-%35A@bN@l^$%a@tthb=w_$+DK=fAa^XBQfRWLMDj==1tm2?zYgdE_X_C8T2S-C0{u zy?)_o3xVH!vu9uq`OqdbgRbb?1Ox;-g9@^uBd6PK6rI&}Yx89zT4CLB+;J zAI!KnXt?eT6~FlUkY|5XSATP|D%2=8uXBH6V&N^RMez8z8Zn=JE|mIdIV}+I#r;fW zJTRN5g*cu^r1D-|rwY@^I^S^lcyEnN_pWd3BgCko0_$lXu2 zAFnNs7Nv$qMz%`H-jl{IH6CcKy&Vla%Q7jF+Z6ZP0D5S?o`aZrY2e1Qi`)?Qy%}HD zhwcbg`~6idqF-{SPmBWMoxA*|-OpVQ=fh;@!Ip^axiX(?e*KFR3C&!h(hN_wI* zlBvMOJk&h_b#}OQezp~zY#49#*6D0iH^?_JV8nE%PUNH%q_&!iJxMTpdYN!b*EMzE zkz6i-lKf+szI;v9M2%{}w(PAC3_6+ee? zeU0k}GRXZBc@naaxYIKAmDc9QCtuvQ6Oqx&@SbBvRpfsh8^*i>SocZQ&Hp44$(Uf~ z*|IIgKJ$=rs-;*K4q!IKzYpbj4X}J`8!{)rBNpd~By;0a;wJF@w0!ZElQ#W$R zg}>%z;ZA|iFZXX;Zrc^S=1yez`XbQGo4o@9pu&`){&RQmA-Y zCUD~>qoSe=NvWzeXz*|YQiE7811_BO+68Mw08*=i2g+^Pj@*fvghrdEej`nT8f!WnaH|{MQ*6!3?LE zSa1>kwtC^@n@|GsK4R=KG_Mz%JyQVX1Iu1C(EaNk{2l;+cT+9K959Kmu*08-H;uGo z{rnj+d!<*9eKo!p_WItU?j8RcdV<$K%YoSzx$E~2#Jp|`29R8FeeW-cCrl{*SmkblQ1m9SWi}>}D zRyg!L0!Q}vv;!_T-EyX0l(lIQx``r`kkL&3i=ls3OxCQiXSbez0)Yff zPJ^zSD}z}I`P!<})N<fOcK3Z5T8htMED#uZ}r=0`)1+`oQ& z(_g3VaEA%j`6<>9HlHZqqBoSSC=Z9jye?l2?MoE{OHAGG+A9`l!OCMi0u*FQiQVLx zD7^uX4k-69e~NC=7!voY`jzHzmo)L5lgF1jKPRq#1=(A6YO`f>vJMYE{vEVe#AlZ{D;|;s-zCUJY4d^y{mQ3F=92T{C zx3WySHImGW*&rL@^8x80nmz{}$cGcFh0XA9Gx>#WWWYqcZj zY_%$LEw|X7NL;Z|FOZ6od^WAfOp2cKsH140&~NX_9&>`?qb-T&=mN7!%F29BPy6oY zUk*rzp|INHTojnre15YO6M`QcT>DD8kD^uz?beh49n@u8^mxIJVsK6iI6l(uT^Yya z_uliq^ohX`gT}{WFiAeq*&o=Y=4X_fz5J5InpO^0_omGJ> z3d;zNq0UN@_2EO|9=CWYxmn#n3J1rnBEuvE(xy)DeJ%smZA8E;|BZqBDuP_RIPq!xOSRx#6MHG<+}e^j}sG1~42qu>jLQ?ut?f9C~O)%lMC5lLjox zZ_4kt37-HTu+$du;h)RigIJWXz7m~3h7MLeaNkK3$|?UiKQOouI(3ep7sYrjEFvNz zB~aV5XE;fG;UGTJ1CL+NmD&oK7>DOVXH;jiFMhh~@qV$}sWbk;YkZktkTiUj=AMFJ z^^0;=lnXlRhKxt29hPBtEMxQY#LzP*T-xQzNURy~A9UQL(!5gNG*vOpaC=~HbK8^W177%q4cph5!jFtzwS zXt>BXKpwUqN_I-cdI_3p;Yu_rMMcFvmJb(|02ALw)(tCx+!nTIs)0rcwlAK-0v1v% zR1PhrF4o4JV5Re($ylXT>frFO^PT>{wIYcIq_{OXVA|Cbk&%%ZS2#@u8F;?=P|xr7 z2ywgnVwv6|)3_C6YGNW4PsqZ~HDqiZbQ_9+4N)tdwftPKy-h7e1#+j1 z2WHb2u9MUUN`aTew{PU!xNlWb==bO2j{e^wnU(34#evPK8vio@zF^S=?{X9Y|I$t7 zarw&4>)iA=Wp{@PUKQC{n0iLI%Yr*#y1t!ATWn}lJ8*JYLg|Yrb6!-d)%IpPltG1- zInJ%9aB_)rq2=hY2V|m8nE>vcUBBZ7@PJ`eBQF7oB^oCGb`q_0L;1fhjVS~rA24a5 z4;)aKi&(!!$f%(JDOZ53Lim~pvd1(DF?@* zcn;+##dX>`A8eAn8h=~k=d9#D+vr&gxV0CS7-i7zx4^>SyG#pR>Q_-r;5r(f+}c!) z1Cfz=?q#qDF}#6s-5>W=`^S=I)jeotxCzkyvGV_%-8^4lNhR=duKBH9a~XqsN)yd{ zT@wuM5jyABWV;Ad7&nALxJLS%ivouBg7(Fzws*^G-ks0X-Ma&_LITF@ZUfTXEzEOE z;-F9q;pqMXxDZs*gen%rx^02s{xTpl24F3z_zK6x__2VefZsG6eg$moKCX{!Dd6dJ zzD>IwZ}fu%h2!k2H-%qfo%`wDNUGs*h4}{y8zU@?{p4oqsH0F4c+?fnohGg_$aW%R1SVv3goehv~@$qd;fa&SU13RfB?<@|eV z{)9R|&D|21yHhIdUGO+&wS>b?f`rcQ9~q9kSV@nH)jKU}BM(bDKUp$3t7tei@0-_x zRX|0WM_~tSx~kU6U#nblTCY`u-6+9TXh^ zIila6CZ5u{Jj;^VUiSr%pfO!B9Bo}AdS?Kfhqw3>b$P&3uOUHNbO^K2&s5@C?KEek9dPP0 zwEZau_+bmo4fkrU;bh$>Fm+nQKZp(EaTOjXEoWkZ6gn`b-eic;=JYI<({vu>ew>y_h<>umP|WfC zi20&Q=4-AhKA4zb2T#B~$F6*n{2aBMo)xq<{trz63B z+<39Oxj$V}w5J(Vobs#}V+>ov`{97t+7Pi55Kt-NV=tD!`N66pv-OoHVE~C4brBV1jQ7Gu!KN3 zpl{f=S*zypc28<|zu;>Yxbf^SM8PA=%_lknxTlTkPqIgbC%(qboMe<5^?mFbFkRoO zKepgmjMnWks~euiaVAPiIzwbG4j>6koQEZy&}dgd#XGmdPI$+sYeLMaj{`yhC*PU; zz$+~+t(oQ=oy;~Eu`!|iIPjJ+>(%1z-R)&pfle;L|Dbd%QGas3tcfdhN_(&6_Q?C} z?2gwAFjx;~IKx|TezaUSeoatDwn-@5k^P;Hz+LwgZMupvD;vB_n8Fzr=EG8zA{(HYq&GA_Kox58pWVn z%UW{7w0;m9>ycN$f@OD>WM6^&vE!1?!q5|e#122x9fgStO#U0d^g{eqe)|W=%74Nk zB$O*^1wH$axsy<0iRWcx2RGZlxiHeb!?Q0UDi55)wPfB80cL3%-lc`rOM$H zX0*0oM9)#90oqqL^C+f+!WFod%0&RqW<%f zHa(o}2cQ@PvD-VDLZ|JMOJ_gkg1FoFE9&=Hb}xTDJKl=QDX1T>#qEW60Nb5IiYpIIAArbLz~~tQkHz${RL4*qkcw;J=7g^Qx5lXE9D% z#9wh4>^)r%rYc4;{dA5wSB75mQg|hbj?5f=`0&98vq0#T-H@Vk%>4O>)WFcpBsVp; zAD<}v0zpZChTHImVNW8<2;9ByuN9x`0L9Wk*;IDplLl%$W>+j{XLCQ%2R*OE^dU)FwF--bT%WBK5;5=oe)*YB8m141 z&p899Ie14NxJTR*e9%p25S%A?yk26))jq-wJO$BdAF+VV45;ldvme#EpPwx{MX_qm z`@`yezW7u0!D|C3cmW1;9v0BNbxewsqg+5($G_A`@hlur7-=69X4T|2j& zy(SAr(OXU)mXJ$|@0{)%br6&QM8H-dI5ACwlBE@ou()$%+Q%CDJgR{=&$sH#xy+v; z3UGV=5FucAOsT%YwP@2@vh5tx4R{W-FSVu*-TTGq+aZsEPVSh@|Gn{&^%tI{AK4#| zw;Eta8M6=tL;-}Ci&Aw-_kmNvJH3$%sOm)pByFyWubZQb|C5wgc1@AK`C6 z;nW&P8ahGLGODL^88^<1*%&A}b6$CeBX-N`we80E(7QESeu|ghJrAwdgKvv;(Q;;e zTkfe|i*n<%8j-X`bbHFg-M30PMD57$i5!TSv>Vafnf1EX0r)^BkUDU#El@y)F6;(O ze0NRK7Q~CjDi1A-RCwZ$&{nc<-Pza!*hbZQac9>6@}6+nk;UIQFj7*3QH5^P3kb3l;$W@cX92uR&Nqm6)A&+3G%y(!NyI^bCS- z3{(&cU}DAv)xExB(Hx=VI|$BQ*y&dSqnG|~zFS~(LNM^+Tu>Z?fkCcZhK9?5-%HWi z4=-UEOs+6>T`y|GD&rmYf}+o?2mpWRc%sn$g&4VNWgREgdDHpwTaGr9R&DLt-ed+A z4p|3fJ$jDYEkv#aBgYlzYw&YCq64t;A=X98lt$p=xV4m9AwA$?y5{=Ih73qCr7>Iz zHZ{Jj6x=c4ClYEd0ktBHY&=cE90t*G+}wSWe9)2s^azREqdYh!M85e5K7bX%RL1__2Xd7=BC zeFrPFv#VfL%VElH@u_9rSiUBoYN=5O56(mlq850=MZwdfIgN@*YyT^xTwRl2O0voY zhUyn8WFBlP*W&oK%a+13O-!vXH`Hl%ncJ@@pVeOI?kLg$FxxV}RZ8)#k7CwpkD78k zeA;|}q%=TMduWtq>_+Z>$LJ}e;`t{=RhS9Cz-J{^nJ4$Ab=(yUzQh~jSy&po;?9(r zMjgJR_g#ThKuOgOX`zWVE=&)UOzGmh$f0|2aBlY{7C4tzimBNAiJ5Q>XQ~aRTLMeHM}FPr*A_mSIBFP^LqHU@AM1w#=?g=(Iagmxn5k7`2T60;K^k z-RJCfJTVh_3Fm%eeA5X0(~zV|e)TG&&Ec0IHw3j4FsxMlI_jfOs#Dc=VV$ss$=Vld z)<<6I^hqy^`FFcG`ch>1w_nHPIT?3E=7}Q4&acr$zK8;rEnO!5rsJkcDb3{*qsDA0z#}#q~Dhzvmgp%Ls zcupuHIN|rg#_M;#@Ud<0_1}`-+>1Q{q_;Q_?##s0Z2k5`(QD8TkgM0ou_Uq>$Ov$< zNp8@}iJDv9#if4y1Vk9~kmXb@yi&jm^l{@{xiBedNRo&!7rqKvlOf88hD|`#uvUXG zsI6wkuFZo&;(QZ2Mff$LlU@xq8vyA$dfd-!h=ts0mx-*In3xuhXU^+EXcWk()e24h zr0FoFAE^#1dHleyrvx3f3Kzr-Jt%0e@M6t@*Z)KZ7;ljp97*5VCli7Z09FuyuQ*uu zYRuu*%O$?PXi%-aaXRo7p!$36eKGJrP}68XamoxJ;gmr1;EZUw`j0gmH&>7_CVgT!Nwa%_J=$-qo=SghRXU>XS5;kxi>x6vv!GK0r+ zvHx)(D1eU*kDEb8#R7s2IFg-M$@MX@@pXgeG#d8m4Gk7?w+7B8W$lI$Hj8a#I(Z+!mi|S z0UQS0Hj6bT7X=uG55a6(p^kv#TQJah=3d|=hdPorS^QHsi|q35;)PY+{`Ryrg0ptf3qYyhu8Qn zM1v|%7_J#`-S>7c*W!8Y!@3FtAPVbBvC+%H9mqLG2aFUVhoR&D^I>8_MHb;~=#$N_6-ra!r6{tKqy}17z{j?QC=VqgqOl$C-G14x8aX zI3RwoNS%RVCA)ssTqJ-)*D`QdeI-t+8_leSeq3rw(E9BoKn7iS@9?{7qKu4;DHExD zf&U?&YD=nVMxk`3j+|4cl&4<7MGXxR0V=BRgOtA!)l?LqF!P_=e-leqt<=Ov%zN={VgTeuO3UPdNd}^M9>o<(DA>;pKlpT*<_o%pm337cTnvClAIRMQiVD%P zPTKv_!`nc()(+Gma8wjConEwi4_2pYDpsNZ5z9F=B&epOLc&e@3dF%AO~+3QAF$gb4RfWB+aRXbh-X*f9tR4@HAd-2%K4Z_>s z=HsJ1c(9g9zd*Ea>Xlpf<-Q0UOfK6zzHVgV`6G-rp22vPQ2SK-Iw&iPFg}7YWYV&FE`U_?K_s;rJ(Bk7&P3!;vxW41p)g;N>X_}5)(uDmkqdNTXSY75c!uUK zLX|U=qukjVK$;HREj;*~X(1DSh`C$=heBH5{cjkDATRBPr;Ju1a(+0DZF*Jr07V4b z(Mojd!YFw`+=&xa4n*rOcE@!mG={>6`PwWS{4^Q2IT}tSg!KDmMZ?HMIGxK+8J~6+ zPob*=86*fDk*E6;J+Fs06b{S++qUpu329RTvn=E_EYx<5zHT?CUMX`F7dPKBRikKnjN|FkjBQHDUx z3Vr>sNrw-Z94VJKLIY8n68Pxi^W9aKd)7>y7EtT7YYe1dEt4cJX{(A$! zO(~WcfXrzZTn-y1FHp+YxC80AK@uF}(tWTsBXrd6(<667k}A2?+1=eWOvq+Y%4V?u z24yF5nk$}#j_M2u&ZVKZj40=BGwWgt_QJCF$elC8ZCem`m9 z9`43KX3$|Azf-aJGXTV#QnvGl+$SSPw|N zUBPsl1jQKDy5S~IH&%3jhbeO1<@329Aub{I^brI0d0AoDP#;m&H6e}AhKPHgJ_Gii zt)Z32V)8!T34cXF;_ac=5SZ#b_##|TGIC%Y;HT{>E;MLZSK3Aeff zYDMy4DrmU9u(8$TsO7u!aN&l#xee@DmiaeNFn{*Gq&CoW2E&mBataRiDjJ64X398| zmX8)p?B;gjPb#v=(GkT0$Kw&_oW++>_ks9u0RJKV^DX8^&L3}#dF!q`^e%kKR&bXw zYoz#BZ_Mh$E<3v9VMI^Y{kOYODa(i(qJW+Q!mVwpiN>E2D|&VE$j%3iqUW}*wf)_F(N#G4aCDoz zYn_Iyplr4^e^bJ3VWxgG4{D!z$|7(*;=gL237gj-ofHM+=ge&l^(x1(#fJUxQWO9v zWcCw&3$G?Du?{~MO(m5}zdVV^2xmeTV69wEEf-|f34yD2BZ9O`kJ#aUfbzstJt;-R zO;^4S(54`b`Yx2N-x--9!>W4-ci9P5p{?*un(OyNWz?#;+q2;oy*94V(&t>cu1wN1 z#P(P=^6m|-M$u#AJg@Nr+hYy0)J?A|($uplr)F1MuW69L>ez1NYMrg?5m7{dI}`ZGT?>6 zs3$tbOF6gQxS2=Fb%39b0bC3K^wQ*xv!2VgAWSkMZvptt*MQK#>({&pDhvs3+32?@JNg0sF)* zF)8H+gP0b*4fF=F6E&;&n&m)ZAhkq+l8-7QbZYAay{c(KdA#0DpX9~eF@4ODHSq9U#A>qID?L8VF_P6JWM^IZkduELDH+o-a;o$h;EP# zDjyweS!QhgDjRJ?SvH)O$7lgjzVpkWuYk}C1_3S3>g={$7sxhRCXdW&IFRe&{1S_j zfIss+1njDK{b9Fd2c#c6S}WilxHskwGSd0_&B{-;cPh`%j=T9;jz40BbjW$Sm8vl7XGpt9~_Yx@j}{PO@ua??xH7TJya;8M?n@p8EzUA8^> zvCR&tZF%jouiU)bSWDcy(njH4>+rM{mqEgFG;W@V7MV1_gVYm9PNL!Oh99|u`T15y zRCcDe0P@z9sHI6z7h04jcYQh#lm^uu0q%8~A5w;M`bNZ&;G%h|cyfHX|B2@5k#C>- z*^+x}mj+O>9ny%yZm*4%WQu1X)=z`nPjppl?DLj6Pmi*~&tzI6l(mRUm}NovH2G9s z_SG`&JE##&_v0!i?x~{gMk)kTt6paU^t*U9ODi$;RAB~VnI0MRY_fIkpnB@un3Zy4 zE9aev=k^5aPX($d^_3h=KPc!P;Yf3?dY$OI3e>_ed4}&dYjfttP6 zpFPln16led;+VSO{B*0EUVOT79DUE)$L;i}9arqu?F3#sJXic|S7q2p-%2e1%C$-( zAp>Db3KZwdXJFhRteVw6bl;~R1VuTwj{?cDv^xO(b+=p*eT(?d1oBslK~%NC)@CH_6>C7Q9jPw7bDBBT8uT~w2gh3`8IHtPT7kAgGO&1FYN4!y%5*)OoN;!J?IgT_MU@7j? zP2{?Eu?~~!Z>>=Mo&vRsU(wK*!JmlhX}|)gtK7J#fWO%$!($LgbWL3bV zajz}v&;5;%;A}uWr=DQ`SIJ{EjDmaWNzMnQq-vtA8&M`rtgNhU7)XMid3l47G zJ*$)}X9KnL$c+@XPHlouAv?aNOhm4X`fp-lO~zJ7}) zXdY=Y3-Gt&*-&RaVmORACx5mMn(%|9K@UO9F|~{P>ZFjqnYfD_6A&&ZgtTzfS?0GE z9oT)LdelWVZ5|jC(uALTqFYsN-IsYXk{GHa?0&Por9O3AquFYzpj3p?5zUibUU4oUP1RBG-P2yhFvW<(aB*m1; z85?Jfy7HCK-bo1XAiuVpI`+9piWS^=)pC(p7>4cUOIpS>CDZaVxc zK`yzDu@0;(MSSobF~{qY!$b2zz}G&4(jiFg5gr~s@1Wn!Q0=^O*L=Lxpj5GGGW;Li zV^i*PVN=fMjYxADZdVQo3fqv6jNB4@r&YjeF2YoJNO@PM8N((L0fBv>6u5`Vi32BC z0w_dQU@sh~7zC|>oZ+qIrP#t&a06i>)M6cg*INLDE{7pvx0kT2tZWAa+fJr}TAk_F z`obyCgHW=Il#4&-FT02e`o3*ZpiK|BX=j|EeL{Z2%qIDLX%KK_WT=*-Jq z>BI4ndIQ#O$+9cECz#8eKY+8tIeP2 zBbimHa+LETHESG1*ES6w;iE}wX==PpnIQB{bL*)E-Abypjzw1}1o@5kAVTt6%1Gl9 zuMtX`NCsxLGoVZcPWHg-1DZ#RbiKzMy25UqkDUeUMlaW|SHr8aqgwlj_CYz&*UeoI z6dL3e6vCc53Y~Vy3WKa3e+h(XU#*T1v(M#lTED0b!4JpY<0j{Wrk&)C*UuWa4?fC)i74gdI_Q^cT(gsz00r$g$)@5h$Z>|zNAFMJZZQ`k(tQK%vGbg z|K@&Axz|C_e5WKC$vWy38)9M~474n)*ey4I+QrbfxOlQhV}-Z{(3A9rH-S>c1n0V{ z<>)yJP>w#x-B$x@H(3bR#lD8ckSn$8k;t}X7l{|&amyojUcMc}=aA12O6u(oG28aj z?gbb;AM&ADL2XbX(sFzcMN6m-O&=ioB{)2z?@BI72u;HE$2mR+By6tS$%apA2SP2( zV~O_@3h0(tF9+!AF5<~n^tJ43FEkJoj++jFmS~R9>~n3=2GD1Fhzv*`dO>vZf(Y5mNlfrJ^a)dzfjcpwSo}vTcl|$kH_sdV?@mxi){*5=Y;)x7DEV5*DzTD_U8%c?ko3^CB8j0eoh5AouvPVDo^{V+m=Ex# zG%=s%9g%atiG~WbF;+>R-1;AVj0c2McvKYVGngvEnKtf=Q4Os0N#E?RhU5S{|xnBm2g_SDcH+s5$c}iP_G1@deP}(!?05DE9!r4^PQ!ckKfYlXxklz@a2^)vo^$J#l z4rQpQzMwvHq{RAFmC!RD4eIuT;B~3_#BlS`9K|^UloB~nAAx|_0ibT|E-!|Mu+0t7 zma{mTZKm)X71UxDVomcyK%dcPP%@UPi-f^hAs%_)lEPC)`BD^!0F4K)X-GM>h6=ob z)iZ9#?XNNM{f6IMy%y-PFR~FdE@s#3mOTf$n5dh$sZK;FA!ngOltDEsHJcVD1L121 zx@n-sakBEm{VJ}2wLIp$rGM>+4pH36itifM;UTPmZ6tK^#i3qTCUCUc$h{5_h3g9B zxWbg4;8Z9#=x)!-H`WV-{3fQb_I}kMZ6r33P!|CHJK0iVulvp=ln1yzLaHBqLz7Fd z>|qpxgM!t)t2s%LT|HMpdOSw16(1{I!j*Hi4=Af8%bf|~1$~CXlnf~QdcMPA$9D|3 zz53Y)sXv3~^1y;@b=V9QfwuaNx1G7*1uny$B|o~ejpMpoN8Jv~ z3S0v^$>OAQifwrxwwL0nopoo#V`dJqK(ZbZk>PqACoH%5y@AVOew z!PLc@wxRC?!6a04vd^@UuC{j91Tz1nsq^qv_MSR2g_nm~V6JK!lG9Sum( zvD%l`o~Jk;ClI>V?$l_cSZ{;fh=m3UcT!6$qI9UP;HCb$~cOXgFtaZZ$(J_ZcK< zQy@ktr{{(sO%iC0=EAn$8lGw|YJ;;BGo>H2T>F&VO5yh7J+oG=%4P7fp&8I`+!co( zxN)Y|bIf}mFxaZadcHhG$;U1LAU9A-Kg$L*xOC}amy)`sA`3(`9YWkUB+Hmr7R20* z;P|28+OIRM>(c*F(9rSVL6ScWbL5xR&ak;6!!|q5AI6x}nK4GXqiy$E6ut~|q>;4g z=Y}&Oc<`B$q@RU zTgkthm>U9!Rr-~XsNL~9q-h$@r`Is(@61H*7_ehcqhzP2UxhkD;r|&8HX+xNPYn#eod=I z7gWTE4kz*u>Uq4)m)lJTqm?8>ub+Vm&06VRXdMhDU`J5R@a;nkgr-AhYb@NajjWX6@qSsKGP?A>tWM9akt%3AjR1gGTuR&OZ%~7CwX~? zkA~;P(FMGEg5jP(+u@^?lsDt@{5vc;iZ3hfh;3fEksN(F_0W#aSr>%+Ryrxn%0^It zlD>L;uto7bmMds=g|pC^=k=Kh%u5IvP!}V!6YBCu>VEQLSj(2OqjL|-+5F6T>gM~7 zFB!FzpT3#Y0>j+)&^5X}q#6Y=@z)?GUhbpn$;W#Mr|hYAur9Svb%bS%Of$!GhF_;c zUkqv;Gn{gilxJsUVV)2*aZIEAdzDL|D7TU|7q1MU)zLMtCy z(Ax~X&;T^2Km@-Uc+(_6LowL*o`@`B7yWv@QXt5M$sXWA8njY zl>DFH?K@U@_zMTJ0`#9Mwl@SmW@E*))vcg#1z=GGpa4WO_4xo(Z%#PmScL;6NdY53 z=%pdj4wXCu-y;|9^em5eh$ij8T_n_yr>@ntVe<(Lv;h*mtcL@lwjq*U9*w9im$Dv* zu>Q@by2rnX1~<8Nr4Sr4-bZEks-RaZ0Ll=0TfzE+yxLm+iR*m}K=N*M%hi_6v4Us9 zQROho#(W_~_OL*JHcuF|a<%-gCmGl=pe9L~`f&2s=I?R36@(iylTX8uJ(IBp2M1Bh zOa`$%4S<4hW<56isx?b8OhOs%S_WYZ8Qv9|sOY6>Ba#)5 z$w1Z}&hu9`z||tvHgy+K)<>}@1f_m-ImmkknePagp6yEuw0N0O3-Lt9BF_(iF%|Cl ze6FxUd9|_d&w_Ry2-i<1T6Z1Jqs*;G@Lx}xY2=kGg!~O^lxp4o?Zh7w_S@T>=&Xo4?L2?hC@fX{0uUur9e) z2pK~gf*9eg+4Sj>oh z{il~~nDTsKrR_nGq1d0{zS);c(BEE%I_ZKM;m2;Fx8dLhFdB&j@vf?$dQZqn(_8@| zb-bB}Y6CGi5S|JJ({g_UDXO9mU?6gcW`oRNL57@Ap*|5k?)dJ+Z0KWJn@WcTo>eHy zuQ7nyn|ccb|DUMTZoW!Sef0U(c6qh$;`&83FkjOcFD|Mu!{kqejM2EJ>;eTDz>V zdL8rJ4~oFJ`LR!+@%WMVq;uaBu&T*FZf)l@-NL+ru*hug`K;%;hY8cx{W&VLagU4;&4zJCP4dt@zIqIdL6o|o!W8P_UGiX_5sN`Zu?9C8&9yb+ zd&*dr@H`tJ7_jFG?p~ZBKjEq0loB&hWtZgfs;`6H)kDlbrL?z$4f|9kst9a*TRI`U zk}PS4hX=WzExO;r)-&SG90-v$O|SZ|rFgQ%Z)TbLZ}tE!8Dv0OX!sUmgI#vN9eE|A z5BJ&P=AFC_8#;_c=z+%%4m8T&-45{S%^;{WyXF(H6@7~2gw7}fu|0@2Ncg(ev9JLw ziHk=%tuKz{Fg`$o+pc2bs7ysja<;<}Y;Ov%H3_TPAbv!3PM(G=&hGUN8(v@2YSk`@ z1)LRrCQgs!VRDjBL(*cJA#I-^Dl$iYaxpkQIJKw^-CSqGag#B_*Yg|Kdq>FnOT3nj z=P71U@#L2#r_$d;Si#6uo8rcSB>~T|9G|lV{x9fUKJuh@?9Fid@)%z1qo_nET=Nuz z)(k_h{%OOG`7AQeS&c5+`3tIqOT#d0Y&-4eaoPCE>rmTX>nh4PHBxM0jq7*|OmRth zs;L7cys_}&$u~F86na%hHCgqWBpEapr0!a(W_inQM;ffVA(CA&f`oL~YrNSVn&P4& z{(<6>{0+Buot-UAzbTk~H#AfSh z@E^37+dezsYH|tEV!6EnFY)-|Hlt++&z&C|se867ELq-~UAv1%PmP@)`EI*6Adu=w zXry(A5BnxjU9ud85+&Yk`r(M3SEH@;#-*`TXPWln9n$ml8mKtKVQ(4);BvaFGrmXcu>Sg)h!D zLHFtUGbNQ`^_)@6wa%ptWCt40O6+0f7C_YyrgbkB%BF=R;hQQdIZ@2DRdPddg90Oo z6bdf|dA=$zG_NaZFKGrAbAMaCEAvlr2xa1tc$njFEXR|FVy1V zSt=_s9u_Prz^+gne@fB4tyXV+C(cRlgDOM{U^{8yHAjrk#WoF24aalfq0_>#c`XL` zhI;Bb_5|<;cz=|XGO2lPvKcnTs*?1_9RPSHA|DYxp{_);twccakoE9E-hYLBHG##fcOiRl2rgV(1qGQm)*CsRi z*ql>~B%Rx^&&A*!wBg?I42|~>>*W1QZex41z4?UwMSy$z@68nB}SzeU~oT5m7{tf14 zS$k@+*F%L6H{Epi8xr8_NZ(DbS2p!1;?&Peu7k*Aj%gYI#|Cc@#r_Vvq4!a8r;6?lWY4KQiP3LVXbHaR#7 z1vn-^aM-7j{(PVT@)pV~p(^A`FYL5a8>z5oM4~0H9Ff~TuWW;V?ZBm0qCkbWG&in| z$qt*HZR;Hd@wmm;R#k<|-JrQE-vpm0|NSdL78pdm000FNl(A;(`a-vEVsJAG(U-zj zlxV_qUWrv1GN}>sHc(nw9B8jFao8k7ejYG?l3OLk`dj)`J*R&_e$gHWY3uDvX()3* z{AR!h^^DW7=>}8fNee#kuy7#Wr^kOB4siJ+F=qhDV?&}6C-!#T5g8ykCS6cd#BD*{ zWVG3LZ*s`!uC5R)fzxwz0HCG%oEx@Lx_O!Y&Rf9X>%jZGTAeRIX(3d0ue%PyP7zj;sUF5v8)#k}=%U z1{|}dVeyTI(hmg=t}FU=u1+-pvvk(Ygw1RA3$I@+pme<5my1x1E8pw(7T2&yngQTZ z?i}=Ih(!-x5=AQ?g!lwN7c~*fi$lX&fKW!U1@6KqDI&k+1x9Wd_@C`BpKcg~!d0Fw z8IBbFE3fm?5jWFx4$>VWRh*f7Ju^Rhwg`{S=?N!OSY zs%*emKCk9Ez&Chn(60H^OU?mEm1VtV{xO`&zJ@{PG!Sh97JcBdj*49qd6yE?Vcy&m z7K=;NhqJvs)pnYpTVUzJXl?mR(7u-a8>N6J!nS@W&;s1uyzz&qQRKMB; zV(a&r_sf^H0*PUeDr=0a>)xP}ylaW~^`t2$uSPwr3)MA40=l(pwsdH-#WrM#cfJ5n z|K-F<+AVGNlT07f3RJ(m|0HXgH8^Lnk<|^yC0TIdMhS#@k|d(h=$g;1qRDW)Y1o=g zLL#v~bbh;0E>hs58nI|ZHeMHcJonHUTzrD+Ee`a|l+jqR=K}_3s%sVt*PMhn_XW?z z@vb$r5?|u_gBWWnQg2&#yAM45VVBKzj!3v#RNwiPbT{%^=;mj^i`NG4!vdKOJt{^g zqAY=E1-WFNPaz~Z*a^W&)eOY#GX8^ugGnBU44ommN!;osg!5-EZiu{d*clF4OUqMZQWOq165q;>d`qxMkr@tH@|Q_!?g_cAJ5P0IjJu(jbS-9bh?YBY$3smU z2mJob?(0h2!v@(Vvxi@=e^|?dr3Mzn*yy&NY{9Dfii`ZV@p@o2l)1w%*(`hSLRZLb zy5bx8io%ei6S2O&e)+}9kHl{eekbg$l$e>BDMq^3IZ^;o=KJiM4Gw|}&^&F_jEGh% z>TodxKyT<(yrK=)!{x&kCJXQH1!(-0=I6Vxi7mQEWX>eM&4a_rY+r=>@;lM^3e?3< zmn&jYhsi_ype3(&O(QY=hCo~KE2O9zZxsnVaiROWn#8tb2mUyI_FQ;Vkfl&1s_r`@>jw zfad!;5zmK|6|YXmi+X!nF33v$BorgzzgVm9pZurhfOkN>LS|O3VzsY^`5(XS{qCsU zy7R|d^bWAcyrav6{k;q%FvCFlHQ*QW5w$4d5k#D34vWksRFB=qJ?`>5j8mV;?s8ul zwc70zUMv8ms~_YPw1kP`+tnkzuh!Vbf}rl zQ_vCOV}5N)ABbenlASfV-Xp;{p3`F~F(I+l7yp2zRm9p@I@<9$>-doa@Bg@ZnfMaB zKnHVu2`RTyqr<1-YWHl5?0WO6{I4TN<(23#8mX`Izb2$44p+zQTU0NW_Ut}&$$V_P1yW`*DDAtCJvnyA9I{P@7c#&-#&B+Eo&-v)6h zeM~aZ2b};&JXxL7kjKTXysM-K{WkId*J@bzbF8Vv-g@TmUa)|@RXxR8`I@Yb?ww#2 zl)Eof*vf^s4AZ1fjfos+;8y1S7$jh2{~=s-PP2g*6Dt}o^C8&`d&}K;MpZ#(T|Y?E zZ;_o_8Hyqmn650hT(5SHQ{(`U=|VBlLMclZ&Hbk1ebV*s#L{%G+~&I%H-zUX{_;=< zC_l$<$w5-wAPr)C3IXH1<7X*ZfqmLe8Z94Z=;JX@)_IzdRGKc@mzSrXo!27xtT>(R z=|62qX56|jG9Mx|i+?Chl^3zTevyhw1C~qc!J=fHv-r}cCWBQD8Qp#vpBKeY{b*=v zN?nm%9;?@K1J}N@P?5GBpr-Im6%nqFM$}8txt>it7YMm|!`=CjY6pD%!4Ja4yEnfT z-`nW8Sn080?!7YUa2FB9BCC!y$cFGMZal?W*jpkz8?m?Pu+}v~N_4K*?6U{5mi%Yq z@6!f**paExl`EAxA69GfC>Vm!L}8SJrDeB_57byXba<6<&UPl0N52*1F4etMXUB8< z3=9k`q*{dBW2Gn%#m_M!oLBODL|ZG%nO?_y-#4(4X)Lk<^({aqiX|7~5AKN-gQK%} z&-@JpA!Cz{Ug%U;QHePC_Pm3?p3z`|PR2>v8##!TgL&eQ2-GGnNL2f!avw?X)IGH7 zynj}Im2ZJ6t|f<$`@7zC&jD&a;w8hgkD&sdKj;f)G;5ISN(xEUQm{LunRlfWLNA6K z7R8sAF;hcH4t&15USL<^=7anwg7=|(lC-OYU~%XjpbQXXawOG}*DsxeG|u7EG&)0` z_K;jF6D(Kui3t{+lds{`tcdr%I(0xtJDkX5opjRKWC}!cIiO9>g}?$arXP~W6OQBJ zf$ZB|Y=H2FD$h&+eb~1Ib$0_EjkZ{nanlDjdifdww`je!KBGw{PFI0WsJx0d(!9v3 z*-essM9qjfc$nl5vXAHnI8kpHJKHdBl@IY6tbhU8Er0UlfF_qC0gqFIkmU-^hZpW# zzT+kpMq3E_0Ea@oV`$E+Afvu%+h%@z-anx*xHMW+`#7dIR)&tVuzES;Tn5?aB9q$K zyOF7H+7abCZm#DJs8*Ou+}3PVL6C-lz^+0K`6hu&Q9GYcsjVIQ$c-KfVel^b;j}^f zSac5K=#U~hXYg@{uWF2hFtc&vC7kpGtF>1F)HRukdu}||PN|_N>I^cx=am-|%AUXC z>wV+L@B(QE9%F+OP+qYLObv$U4?dK8CMWfbHO_$V6%Sv z3?eUsG~|6w?>^3}4B@;Mzx6%%XFY$+1P5VOp1g{q|KJX31yJ>M2=83~mu906gxy6Z zgnd65X9F|1?4;N}mQ6Yxe$1H+*?n9b8sXyp-@aBR=;eBsBRw{_8?R1>unpb_;FMZ5 z&sS89FD#Q6s`_lPkWEGXmsoBW9F7>H-xS?HWSdMH8X9^(|BcV-gDJX2X)u? zl4KkUm`D|+9OpXuu$N@&)Kg2h5)4TewXT^=ux*T42DKU;m6^%Je5Xv@C7Ob=GF6Yb zEUoi`CVpyQhW?V0AQ0y)pO8@9?vPv2Begwgi^)5eQL-*VwPbI$*ieSCSgPXM%Br*< z`;4xWGepZGavc!It%8UNAz2Hb}j2o}VEAif{$6FK7`zT9Ie^%Qj&Z_viDo{aH;Z~(-_ z*$+yXe#qZCyJ$#`)B}*GkUIJ1e3EcCi3xd7_qkVO0ogV^H1iLypu1W~5z6~i$fuY( zuq$GKTPuW+^g_|S9Tzu0QH;O1E6;jvMpsS#rQuhejf!%lGlYlkUmkKYwj;%l}n>vti#ZaXb z_XEw;+jjYD0=QA~!9XC}CN;^L4RbzG!uV3QIexi%!u3Woh?JIDogyh$L44B86YgRJ z;#q6xhhQF8FtY^fgg1n&Ex@iO<4|wbalR6wiJDd?UdIeh*~c@2Z40I&Dy$T99{uzd*BKz? z5*$r=<{5K1Rz(73%)zW_EA<*%u}O-(gOXB_;Ax1jUQU#VN< zO14dL=@haC5%c32q$XQ{5He-tfGxTS*Ld<;?;P5jo;jdWo*xOvqHM0Q!ZM&QDbZXN2twP5@Xk*Y&-M_KF!kP7o6yOEEI^lDWnyhWEQ`mo6a zP^IKUDD&KuM|IQ`7$@`@rMk}MbAGP*x8tplGD z1{_scChQIa)9yRa5TR%gQKH=3`SuR4cG0P*u<+BpPDpu6>xg)-FFF<>V6q+VDh^difYba#=f>EX*t`+OCf9>9mty>4t7lIC#GK zd%~4O-cQIo503l3zPl?JD4$?!mcX>Qeknemy1bd`oAdF&zks+-q~(P`zN%|o9y?+O z@M+r?#NOSXC|k2uQEr;{Iqj8dMX%Q9bKTK@F2B6_u!{CiA>ik4Uhl0Z+d#n*p=7QX z{F%L$iu;|d>4VVG2t&|-?Gk^5aFzm7`Re0q;Ub5&r`uK1-x8JN<=OGHc zwAJfm&^INNlIIfPFk>H27-*z#+K;xTj&!Ow*>p@to!Xk7g5|9How=!A(`a%W(=N=P zzNtlKmfd;#zq9J;2qS@C-NHyEbLz>HWom7l>d=JvXG!Xf|BKFhT$3J;Ln!b5- z08jBKGiA0fAmjvTt#WHfhghx18Mnv{*_CeTWVZ3Y#6*BK^E!Wo3uE!t~(gLvFi>((lfLvMZD)Xm4n@Ni@}P-pQXHjr6!shUhJ_pAEN{aI(pXYD75UhtUzd zuvq6nkEYBK_9&o8oDx2|p%Wd(gmlrudwzbcl!{%ns~Qx$z`G5E^}MJDB&M0| zs8@NIuIpYJwAiv(bc0=0?7LpA?xek_`6qRbt+7__}GLH|Hr%9E90@v*kb*#j4h-%Ze8K4ynHmPHmDQg@joo$UP6 zN~Wj1-(}zGZIUpcxVP2PW{7N$H(S?!b}>C2{z^oCo!%)J0VnCdhIdEjC0>>{+lkKH4Hy>u2oX3lOE$;2GV;wm8~54F`)-#z{XZepV$bDX9WWm3elFIDDSt_&I_+Va^iI)d{CGGv3l>(_07WI1hG4jgyKREL9$e&UxNHP`) zVDYKy2<0;-)hwdyWC^%3R8S`D%44jp`AE{5Q^qI4NVjYV0F)fa-K#b|hh7*mzA{^t z4RMS5{UAECJtre{fEZF%du-s?Ojn|DKYz~usfXlPBHYyyv+u9TstTGzGe%bY`4=QE z$VN=Fe&d{jBoblAFOTlkI-3#eQWUeGp4x%BAP<5F2iRF;e5&oo`q8U_seJv9{6JD& zG`P8d;V{a7s|hH_Mo<}__thYS8=EkT8BgA~Q3(;Htm0Z4L}0GmyO&ZhP2Tw0 zr3D?hlq8ZShn|DJHdn}HHV1^c`D#i+80&L)Ur^`n4zGtL0w0WVW-G{j139I@gmfS_ zPIagNlZryV!N3QKAL-BgA2Rv?-4%`G|A?}v3>qfcC?HNcbR?5_pcH-oF(^*+ffCAv zB~c%V={NzK2sCIF8BPD8-dI|y0`@E!RB#`p6k%Y&#W*d$D8JVNqNh9vwao!xQBE`e zUREciQ@{c^#f)F((#(R`Z%)Z*ph!P|yA|%*^4TS}r=oK$B{$^gDvp09{?J3BOEfmg zAlE~3MYGyAI8ShsM-H#+4@$Vaq9Vlvo)#7LOzH$%rdXs(o5@t|nTP^LHV;xPB&`z1 z2M^`Hdq$QIF}RDSQVSiXT4}y}yRwn*z>I5jYUD+9(rtr3;kXDj4aoMwL**H~eT}o^B+&bxDrxLU&<+Bah&JRdq zwtTIn#dF?Q6~;^hM2DU*ev{G8mw(zzaxK@?;5-S3@25#eJBPr*Tg`VwyMn##x?Z15 zsABuG)96LQI*UPM4cd`tmACx<&|NqNMp|;9;vS9UMA>NGZ=$@H7gZAO(lJ}_T>GHj z*#IT<@a<97v$xG~D-{a)f+<>u2U;r`MgUu}jNaR}+-P9mgk#^2iJAnVB!PPQ1ne!w z2tuizAxU}gw1L{k4fgLnNe(JYi!nlzrAbo?&)!JC#&KVxqFA>?Uk!V@v^b%;QXwnE z-?2`|_|c_cT?;Z=8IADe9<(zN8|=O}3rcv2ks-HF=Z{L6lNN+XthA}>tSt)HnzSG} zX}{TGL5Rv;RjP=IBlG0*`&xQzbFBy( zXP>?ircn~+enrzD^ovJ;#&-t8<^ zHs+c3j8N3bY{|_17^F|#)}c9npNOTdQndY)p?{1a)3!@N*~pJUqZta+E_MDA|1Muv ziE+&f!(=2Z_xIfr1Sihw1EO5ff9@U;u@7uE%}2Wa#ehyZ>cP&kMx^S!4++?TJL+@# zR~y$~<9(@tyEkKu3m9I6Ry=PX{qe{Uei8Ru zBsTsx(nSr!yWe=Ej^x_?ez95n=S`KL{nU(o8-6WYZ-cUovN5SpSmu5ens;W%IyE=R z-tozveB-6F5#tz5oowoY-U=Oy60~fe?IF!i(q&w4*f|u+&uQ~9qUwn93$B0CG{S^1 zEfN=$^5(GCI&gN%&USz3k{2JC~vL67;W* z>)2bxQr~W9wup{bLYwvf;}NpUEl>r1aefLd;s;&FX7pnXbiRamWGMf4z)%UgOScNT z5Ec`8cmFrfVClN`!7h_?OH=x~>>RDr4>1Y*t}^`rxIagaTvGUbrQn*qK^$BdqkjQE zqQ?h8*3L@*{QA!%!oM$cF+3$&{vQ8cDWJi|Vj8$Y!a|o87l)mu$*CklLGiug`emt*zA#gNIdxoD^*T{^?S-LttIm&N?U5s`37}5t#uejl2%ZB!$?FWMp+I=LYjeV%C z{yYafhhDh_lu>llBm?{WoL>NoZgOGx*=ytXB6AStAIWyfzrBZu4t#~eG@0~2&7t5f z7aMw3xo`ClL>htUwf{#JcndrjEusZ~*YL<(eIo=b%GcXcPxn16M_|_=+q{2e-zw0F j>{JxI?E5=fmb}>`eg}}Ls$B#n5&V;pRFo*XZRGb~ppEA2 literal 0 HcmV?d00001 From bdca19c2dfc4813395c1ac67713adb687f64d653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=83=81=ED=98=84?= Date: Thu, 7 Dec 2023 11:25:48 +0900 Subject: [PATCH 2/7] =?UTF-8?q?LAANC=20=EC=A3=BC=EC=84=9D=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 28 +++------------------- src/components/laanc/list/LaancDetail.js | 5 ++-- src/components/laanc/list/LaancGrid.js | 4 ++++ src/components/laanc/list/LaancSearch.js | 3 +++ src/components/laanc/step/LaacnStep3.js | 15 ++++++------ src/components/laanc/step/LaancStep1.js | 2 ++ src/components/laanc/step/LaancStep2.js | 5 +++- src/containers/laanc/LaancContainer.js | 4 +++- src/containers/laanc/LaancPlanContainer.js | 3 +++ 9 files changed, 33 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3932cd61..8afcb53f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6324,9 +6324,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001242", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001242.tgz", - "integrity": "sha512-KvNuZ/duufelMB3w2xtf9gEWCSxJwUgoxOx5b6ScLXC4kPc9xsczUVCPrQU26j5kOsHM4pSUL54tAZt5THQKug==" + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==" }, "capture-exit": { "version": "2.0.0", @@ -8487,14 +8487,6 @@ } } }, - "dom7": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz", - "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", - "requires": { - "ssr-window": "^3.0.0-alpha.1" - } - }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -24397,11 +24389,6 @@ "tweetnacl": "~0.14.0" } }, - "ssr-window": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz", - "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==" - }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -24943,15 +24930,6 @@ "resolved": "https://registry.npmjs.org/sweetalert2-react-content/-/sweetalert2-react-content-3.0.1.tgz", "integrity": "sha512-VBybIRTIzY2bTkUddcp2wMJ3mp3gfGGX6+BfW2dDrEv6bXM2WtzJpFkM2imFpcPhpkOIf2/J8gLxEu0jBZq0DQ==" }, - "swiper": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-6.0.4.tgz", - "integrity": "sha512-D+DBxgg81+uocgsvhmdzrpr4GHzhAt2yImArqzunrC80y7+/yCEAq/EJw1VASD+CBFNacF4F8FEIqJMLyDFM0g==", - "requires": { - "dom7": "^3.0.0-alpha.7", - "ssr-window": "^3.0.0-alpha.4" - } - }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", diff --git a/src/components/laanc/list/LaancDetail.js b/src/components/laanc/list/LaancDetail.js index c933e7d9..904de477 100644 --- a/src/components/laanc/list/LaancDetail.js +++ b/src/components/laanc/list/LaancDetail.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Row, @@ -7,7 +7,6 @@ import { ModalHeader, ModalBody, ModalFooter, - Alert, FormGroup, Label, Input @@ -33,6 +32,8 @@ export default function LaancDetail({ data, handlerLaancClose }) { }; const { user } = useSelector(state => state.authState); const { termsList } = useSelector(state => state.accountState); + + // Laanc 약관 동의 useEffect(() => { dispatch( TermsActions.termsList.request({ diff --git a/src/components/laanc/list/LaancGrid.js b/src/components/laanc/list/LaancGrid.js index 0f3bfd93..5adcf7ed 100644 --- a/src/components/laanc/list/LaancGrid.js +++ b/src/components/laanc/list/LaancGrid.js @@ -26,10 +26,12 @@ export default function LaancGrid() { ); const { loading } = useSelector(state => state.loadingReducer); + // Laanc 승인 신청 목록 조회 useEffect(() => { return () => dispatch(LaancAction.LAANC_APPROVAL_DETAIL_INIT()); }, []); + // Laanc 승인 신청 목록 비행 구역 조회 useEffect(() => { if (laancDetail) { dispatch(FLIGHT_PLAN_AREA_BUFFER_LIST.request(laancDetail?.areaList)); @@ -43,6 +45,7 @@ export default function LaancGrid() { dispatch(LaancAction.LAANC_DETAIL.request(planSno)); }; + // Laanc 승인 신청 목록 닫기 const handlerLaancClose = () => { dispatch(drawTypeChangeAction('')); dispatch(AREA_DETAIL_INIT()); @@ -167,6 +170,7 @@ export default function LaancGrid() { } ]; + // PDF 다운로드 const handlerPdfDownload = pdf => { if (pdf) { let alink = document.createElement('a'); diff --git a/src/components/laanc/list/LaancSearch.js b/src/components/laanc/list/LaancSearch.js index 80a4e3ca..d61a03d6 100644 --- a/src/components/laanc/list/LaancSearch.js +++ b/src/components/laanc/list/LaancSearch.js @@ -14,10 +14,12 @@ function LaancSearch() { createEndDate: moment().subtract(-7, 'day').format('YYYY-MM-DD') }); + // Laanc 승인 신청 목록 조회 useEffect(() => { dispatch(LaancAction.LAANC_APRV_LIST.request({ ...date })); }, []); + // 날짜 변경 헨들러 const handlerChangeDate = selectedDates => { if (selectedDates.length === 2) { const createStDate = moment(selectedDates[0]).format('YYYY-MM-DD'); @@ -26,6 +28,7 @@ function LaancSearch() { } }; + // 검색 헨들러 const handlerClick = () => { dispatch(LaancAction.LAANC_APRV_LIST.request({ ...date })); }; diff --git a/src/components/laanc/step/LaacnStep3.js b/src/components/laanc/step/LaacnStep3.js index a08aee6a..aaeab817 100644 --- a/src/components/laanc/step/LaacnStep3.js +++ b/src/components/laanc/step/LaacnStep3.js @@ -40,12 +40,14 @@ export default function LaacnStep3({ 11: '25kg초과' } }; - const [centeredModal2, setCenteredModal2] = useState(false); + + const [confirmModal, setConfirmModal] = useState(false); const [formModal, setFormModal] = useState(false); const [numPages, setNumPages] = useState(null); // total const { user } = useSelector(state => state.authState); const { laancPdf } = useSelector(state => state.laancState); + // PDF 다운로드 const handlerPdfDownload = e => { if (laancPdf.pdfUrl) { let alink = document.createElement('a'); @@ -55,6 +57,7 @@ export default function LaacnStep3({ } }; + // PDF 페이지 로드 const onDocumentLoadSuccess = ({ numPages: nextNumPages }) => { setNumPages(nextNumPages); }; @@ -238,17 +241,16 @@ export default function LaacnStep3({ - {/* outline onClick={() => setCenteredModal2(!centeredModal2)} */} - setCenteredModal2(!centeredModal2)} + isOpen={confirmModal} + toggle={() => setConfirmModal(!confirmModal)} className='modal-dialog-centered' style={{ maxWidth: '650px', margin: '0 auto' }} > - setCenteredModal2(!centeredModal2)}> + setConfirmModal(!confirmModal)}> 비행 승인 완료 @@ -261,7 +263,6 @@ export default function LaacnStep3({ ))} - {/* */} diff --git a/src/components/laanc/step/LaancStep1.js b/src/components/laanc/step/LaancStep1.js index 73d23540..9dddaf6c 100644 --- a/src/components/laanc/step/LaancStep1.js +++ b/src/components/laanc/step/LaancStep1.js @@ -75,6 +75,7 @@ export default function LaancStep1({ if (!currentParm) setCenteredModal(mapParam != 'true' ? false : true); }, [location]); + // 일물 일출 데이터 호출 useEffect(() => { if (areaCoordList) { if (areaCoordList[0].coordList[0].lat !== 0) { @@ -263,6 +264,7 @@ export default function LaancStep1({ schFltEndDtRef.current.flatpickr.close(); }; + // 승인 유형, 비행 시작 일자, 비행 종료 일자 아이콘 팝오버 핸들러 const toggle = type => { switch (type) { case 'commercial': diff --git a/src/components/laanc/step/LaancStep2.js b/src/components/laanc/step/LaancStep2.js index 14f68de2..e6880e54 100644 --- a/src/components/laanc/step/LaancStep2.js +++ b/src/components/laanc/step/LaancStep2.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import '@styles/react/libs/flatpickr/flatpickr.scss'; -import { AlertCircle, CheckCircle } from 'react-feather'; +import { CheckCircle } from 'react-feather'; import FlightArea from '../map/FlightArea'; import { Row, @@ -48,6 +48,7 @@ export default function LaancStep2({ const { laancPdf } = useSelector(state => state.laancState); const dispatch = useDispatch(); + // 약관 동의 데이터 useEffect(() => { dispatch( TermsActions.termsList.request({ @@ -58,12 +59,14 @@ export default function LaancStep2({ ); }, []); + // 비행 승인 요청 useEffect(() => { if (flightData && Object.keys(flightData).length > 0) { dispatch(LaancAction.LAANC_FLIGHT_CREATE.request(flightData)); } }, [flightData]); + // 비행 승인 요청 성공 useEffect(() => { if (laancPdf && flightData && Object.keys(flightData).length > 0) { handlerStep(3); diff --git a/src/containers/laanc/LaancContainer.js b/src/containers/laanc/LaancContainer.js index 3655d5e1..afa2f65d 100644 --- a/src/containers/laanc/LaancContainer.js +++ b/src/containers/laanc/LaancContainer.js @@ -21,14 +21,16 @@ export default function LaancContainer() { const queryParams = new URLSearchParams(location.search); const mapParam = queryParams.get('map'); + // URL 쿼리 파라미터 중 'map' 값을 가져옵니다. useEffect(() => { - // URL 쿼리 파라미터 중 'map' 값을 가져옵니다. + // Redux Store 초기화 dispatch(drawTypeChangeAction('')); dispatch(LaancAction.LAANC_APPROVAL_INIT()); dispatch(AreaAction.AREA_DETAIL_INIT()); setDisabledAnimation(mapParam != 'true' ? false : true); }, [location]); + // LAANC 신청하기 버튼 클릭 헨들러 const handleApply = () => { dispatch(drawTypeChangeAction('')); dispatch(LaancAction.LAANC_APPROVAL_INIT()); diff --git a/src/containers/laanc/LaancPlanContainer.js b/src/containers/laanc/LaancPlanContainer.js index 958d2cd7..18dbbd94 100644 --- a/src/containers/laanc/LaancPlanContainer.js +++ b/src/containers/laanc/LaancPlanContainer.js @@ -44,6 +44,7 @@ export default function LaancPlanContainer({ url: '' }); + // 로그인 회원 정보 세팅 useEffect(() => { if (user) { setDetailData({ @@ -435,6 +436,7 @@ export default function LaancPlanContainer({ } }; + // 비행 구역 적용 버튼 핸들러 const handlerBufferApply = async () => { if (areaCoordList) { if (areaCoordList[0].coordList.length > 0) { @@ -485,6 +487,7 @@ export default function LaancPlanContainer({ } }; + // Laanc 승인 요청 취소 버튼 헨들러 const handlerLaancClose = () => { setStep(1); setDisabledAnimation(!disabledAnimation); From aac4a560c3a84b28422233a601acd7057b3ab84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=83=81=ED=98=84?= Date: Thu, 7 Dec 2023 14:25:23 +0900 Subject: [PATCH 3/7] =?UTF-8?q?laanc=20state=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/laanc/list/LaancDetail.js | 2 ++ src/components/laanc/list/LaancGrid.js | 4 +++- src/components/laanc/list/LaancSearch.js | 2 +- src/components/laanc/step/LaacnStep3.js | 7 ++++++- src/components/laanc/step/LaancStep1.js | 10 ++++++++++ src/components/laanc/step/LaancStep2.js | 6 ++++++ src/containers/laanc/LaancContainer.js | 6 ++++-- src/containers/laanc/LaancPlanContainer.js | 10 ++++++++-- 8 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/components/laanc/list/LaancDetail.js b/src/components/laanc/list/LaancDetail.js index 904de477..3837a1f1 100644 --- a/src/components/laanc/list/LaancDetail.js +++ b/src/components/laanc/list/LaancDetail.js @@ -30,7 +30,9 @@ export default function LaancDetail({ data, handlerLaancClose }) { 11: '25kg초과' } }; + // 로그인 정보 const { user } = useSelector(state => state.authState); + // 약관 정보 const { termsList } = useSelector(state => state.accountState); // Laanc 약관 동의 diff --git a/src/components/laanc/list/LaancGrid.js b/src/components/laanc/list/LaancGrid.js index 5adcf7ed..4a305bfe 100644 --- a/src/components/laanc/list/LaancGrid.js +++ b/src/components/laanc/list/LaancGrid.js @@ -19,11 +19,13 @@ pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/$ export default function LaancGrid() { const dispatch = useDispatch(); + // 상세보기 모달 const [isAnimation, setIsAnimation] = useState(false); - + // Laanc 승인 신청 목록 const { laancSearchData, laancDetail } = useSelector( state => state.laancState ); + // 로딩 상태 const { loading } = useSelector(state => state.loadingReducer); // Laanc 승인 신청 목록 조회 diff --git a/src/components/laanc/list/LaancSearch.js b/src/components/laanc/list/LaancSearch.js index d61a03d6..7a4edea6 100644 --- a/src/components/laanc/list/LaancSearch.js +++ b/src/components/laanc/list/LaancSearch.js @@ -8,7 +8,7 @@ import * as LaancAction from '../../../modules/laanc/actions/laancActions'; function LaancSearch() { const dispatch = useDispatch(); - + // 날짜 데이터 const [date, setDate] = useState({ createStDate: moment().subtract(0, 'day').format('YYYY-MM-DD'), createEndDate: moment().subtract(-7, 'day').format('YYYY-MM-DD') diff --git a/src/components/laanc/step/LaacnStep3.js b/src/components/laanc/step/LaacnStep3.js index aaeab817..3121f2fd 100644 --- a/src/components/laanc/step/LaacnStep3.js +++ b/src/components/laanc/step/LaacnStep3.js @@ -41,10 +41,15 @@ export default function LaacnStep3({ } }; + // 성공 모달 const [confirmModal, setConfirmModal] = useState(false); + // 공문 모달 const [formModal, setFormModal] = useState(false); - const [numPages, setNumPages] = useState(null); // total + // total + const [numPages, setNumPages] = useState(null); + // 로그인 정보 const { user } = useSelector(state => state.authState); + // pdf 데이터 const { laancPdf } = useSelector(state => state.laancState); // PDF 다운로드 diff --git a/src/components/laanc/step/LaancStep1.js b/src/components/laanc/step/LaancStep1.js index 9dddaf6c..fb1e45dd 100644 --- a/src/components/laanc/step/LaancStep1.js +++ b/src/components/laanc/step/LaancStep1.js @@ -36,22 +36,32 @@ export default function LaancStep1({ }) { const dispatch = useDispatch(); + // 로그인 정보 const { user } = useSelector(state => state.authState); + // 비행 구역 정보 const { areaCoordList } = useSelector(state => state.flightState); + // 일물 일출, 고도 정보, 관제권안 정보 const { laancSun, laancElev, laancArea } = useSelector( state => state.laancState ); + + // LAANC 폼 제어 const fltElevRef = useRef(null); const bufferZoneRef = useRef(null); const schFltStDtRef = useRef(null); const schFltEndDtRef = useRef(null); + + // 마운트 시 지도 표출 여부 const location = useLocation(); const queryParams = new URLSearchParams(location.search); const mapParam = queryParams.get('map'); + // 아이콘 팝오버 const [popoverCommercial, setPopoverCommercial] = useState(false); const [popoverSchFltStDt, setPopoverSchFltStDt] = useState(false); const [popoverSchFltEndDt, setPopoverSchFltEndDt] = useState(false); + + // 모달 const [isErrorModal, setIsErrorModal] = useState({ isOpen: false, title: '', diff --git a/src/components/laanc/step/LaancStep2.js b/src/components/laanc/step/LaancStep2.js index e6880e54..0939b3c0 100644 --- a/src/components/laanc/step/LaancStep2.js +++ b/src/components/laanc/step/LaancStep2.js @@ -40,11 +40,17 @@ export default function LaancStep2({ 11: '25kg초과' } }; + // 약관 동의 const [isterms, setIsterms] = useState(false); + // 약관 팝업 const [isPopUp, setIsPopUp] = useState(false); + // 비행 승인 요청 데이터 const [flightData, setFlightData] = useState({}); + // 로그인 정보 const { user } = useSelector(state => state.authState); + // 약관 동의 데이터 const { termsList } = useSelector(state => state.accountState); + // pdf 데이터 const { laancPdf } = useSelector(state => state.laancState); const dispatch = useDispatch(); diff --git a/src/containers/laanc/LaancContainer.js b/src/containers/laanc/LaancContainer.js index afa2f65d..202c6052 100644 --- a/src/containers/laanc/LaancContainer.js +++ b/src/containers/laanc/LaancContainer.js @@ -13,11 +13,13 @@ import LaancGrid from '../../components/laanc/list/LaancGrid'; export default function LaancContainer() { const dispatch = useDispatch(); - const location = useLocation(); - + // map 컴포넌트 표출 여부 const [currentParm, setCurrentParm] = useState(false); + //LAANC 신청하기 모달 const [disabledAnimation, setDisabledAnimation] = useState(false); + // 마운트 시 지도 표출 여부 + const location = useLocation(); const queryParams = new URLSearchParams(location.search); const mapParam = queryParams.get('map'); diff --git a/src/containers/laanc/LaancPlanContainer.js b/src/containers/laanc/LaancPlanContainer.js index 18dbbd94..2e77166d 100644 --- a/src/containers/laanc/LaancPlanContainer.js +++ b/src/containers/laanc/LaancPlanContainer.js @@ -23,14 +23,20 @@ export default function LaancPlanContainer({ setDisabledAnimation }) { const dispatch = useDispatch(); - + // 비행 구역 정보 const { areaCoordList } = useSelector(state => state.flightState); + // 로그인 정보 const { user } = useSelector(state => state.authState); + // 관제권안 정보,고도 정보 const { laancArea, laancElev } = useSelector(state => state.laancState); - + // laanc step const [step, setStep] = useState(1); + // laanc 초기값 const [detailData, setDetailData] = useState(initFlightBas.initDetail); + // 비행 구역 보달 const [centeredModal, setCenteredModal] = useState(false); + + // 모달 const [isErrorModal, setIsErrorModal] = useState({ isOpen: false, title: '', From 41aba97aab787e582faf6d5a16f73e25e74435cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?junh=5Feee=28=EC=9D=B4=EC=A4=80=ED=9D=AC=29?= Date: Thu, 7 Dec 2023 15:40:07 +0900 Subject: [PATCH 4/7] =?UTF-8?q?laanc=20=EB=B9=84=ED=96=89=EA=B5=AC?= =?UTF-8?q?=EC=97=AD=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/laanc/map/FlightArea.js | 237 ++++++------------- src/components/laanc/map/LaancAreaMap.js | 158 +++---------- src/components/laanc/map/LaancComn.js | 73 ++++++ src/components/laanc/map/LaancDrawControl.js | 64 ++--- src/utility/DrawUtil.js | 34 +-- 5 files changed, 222 insertions(+), 344 deletions(-) create mode 100644 src/components/laanc/map/LaancComn.js diff --git a/src/components/laanc/map/FlightArea.js b/src/components/laanc/map/FlightArea.js index 401dde88..c7bdfbff 100644 --- a/src/components/laanc/map/FlightArea.js +++ b/src/components/laanc/map/FlightArea.js @@ -1,6 +1,9 @@ +import { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +// mapbox import 'mapbox-gl/dist/mapbox-gl.css'; import mapboxgl from 'mapbox-gl'; -import threebox from 'threebox-plugin'; import MapboxLanguage from '@mapbox/mapbox-gl-language'; import MapboxDraw from '@mapbox/mapbox-gl-draw'; import { @@ -10,18 +13,6 @@ import { SimpleSelectMode } from 'mapbox-gl-draw-circle'; import { MAPBOX_TOKEN } from '../../../configs/constants'; -import { useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; -import { - AREA_COORDINATE_LIST_SAVE, - AREA_DETAIL_LIST_SAVE -} from '../../../modules/basis/flight/actions/basisFlightAction'; -import { - drawTypeChangeAction, - mapInitAction -} from '../../../modules/control/map/actions/controlMapActions'; -import LaancAreaMap from './LaancAreaMap'; import { InitFeature, handlerCreatePoint, @@ -32,20 +23,26 @@ import { layerPolyline, layerWayPoint } from '../../../utility/DrawUtil'; -import flatGimpo from '../../map/geojson/flatGimpoAirportAirArea.json'; +// actions +import { + AREA_COORDINATE_LIST_SAVE, + AREA_DETAIL_LIST_SAVE +} from '../../../modules/basis/flight/actions/basisFlightAction'; +import { + drawTypeChangeAction, + mapInitAction +} from '../../../modules/control/map/actions/controlMapActions'; +import * as LaancAction from '../../../modules/laanc/actions/laancActions'; +// geojson import gimpo from '../../map/geojson/gimpoAirportAirArea.json'; -import geoJson from '../../map/geojson/airArea.json'; + +import threebox from 'threebox-plugin'; import { FeatureAirZone } from '../../map/mapbox/feature/FeatureAirZone'; import { WeatherContainer } from '../../../containers/basis/flight/plan/WeatherContainer'; import { initFlightBas } from '../../../modules/laanc/models/laancModels'; -import * as LaancAction from '../../../modules/laanc/actions/laancActions'; +import LaancAreaMap from './LaancAreaMap'; import LaancDrawModal from './LaancDrawModal'; - -const initialAddData = { - isAddable: false, - isViewAdd: false, - overAdd: false -}; +import { handlerCreateAirSpace } from './LaancComn'; export default function FlightArea({ centeredModal, @@ -61,40 +58,65 @@ export default function FlightArea({ >; const dispatch = useDispatch(); - const { areaCoordList } = useSelector(state => state.flightState); + // 비행구역 타입 및 공역 타입 const mapControl = useSelector(state => state.controlMapReducer); - const mapContainer = useRef(null); + // 비행구역 정보 저장 + const { areaCoordList } = useSelector(state => state.flightState); + + // 지도 const [mapObject, setMapObject] = useState(); - const [drawObj, setDrawObj] = useState(); + const mapContainer = useRef(null); + // 지도 로드 여부 const [isMapLoad, setIsMapLoad] = useState(false); + + // 비행구역 그리기 + const [drawObj, setDrawObj] = useState(); + + // 미니맵 레이어 const [previewLayer, setPreviewLayer] = useState(); + + // 날씨 모달 const [formModal, setFormModal] = useState(false); + // 비행구역 설정 관련 모달 const [modal, setModal] = useState({ title: '', desc: '', isOpen: false }); + // 비행구역 저장 가능 여부 const [isSaveable, setIsSaveable] = useState(false); - const [addData, setAddData] = useState(initialAddData); + // 비행구역 추가 가능 여부 판단 + const [addData, setAddData] = useState({ + isAddable: false, + isViewAdd: false, + overAdd: false + }); + + // 저장된 비행구역 데이터 const [saveData, setSaveData] = useState(); + + // 비행구역 고도 const [saveElev, setSaveElev] = useState(); - //날씨 임시 데이터 + //날씨 위치 데이터 const [wheather, setWheather] = useState([]); + // 미니맵에 표출되는 비행구역 정보 const previewGeo = { type: 'FeatureCollection', features: [] }; + // 지도 초기 셋팅 useEffect(() => { handlerMapInit(); }, []); + // 미니맵에 비행구역 표출 및 날씨 정보 저장 useEffect(() => { if (areaCoordList) { const area = areaCoordList[0]; @@ -105,78 +127,12 @@ export default function FlightArea({ } }, [areaCoordList, centeredModal, previewLayer]); - const handlerCreateAirSpace = ( - map, - useGeoJson = { - ...geoJson, - ...flatGimpo, - features: [...geoJson.features, ...flatGimpo.features] - } - ) => { - if (map.getLayer('maine')) { - map.removeLayer('maine'); - map.removeSource('maine'); - } - let arrGeoJson = []; - useGeoJson.features.map(item => { - if (item.properties.type === '0001' && mapControl.area0001) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FF3648' } - }); - } else if (item.properties.type === '0002' && mapControl.area0002) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FFA1AA' } - }); - } else if (item.properties.type === '0003' && mapControl.area0003) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FFA800' } - }); - } else if (item.properties.type === '0004' && mapControl.area0004) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#A16B00' } - }); - } else if (item.properties.type === '0005' && mapControl.area0005) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#AB40FF' } - }); - } else if (item.properties.type === '0006' && mapControl.area0006) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#009cad' } - }); - } - }); - useGeoJson.features = arrGeoJson.filter(i => i.geometry.type === 'Polygon'); - - // 공역 생성 start - map.addSource('maine', { - type: 'geojson', - data: { - ...useGeoJson - } - }); - map.addLayer({ - id: 'maine', - type: 'fill', - source: 'maine', - layout: {}, - paint: { - 'fill-color': ['get', 'color'], - // 'fill-extrusion-height': 3000, - 'fill-opacity': 0.5 - } - }); - }; - + // 비행구역 설정 관련 모달 표출 const handlerModal = () => { setModal(!modal); }; + // 비행구역 타입 변경 시 그리기 모드 상태일 때 에러 표출 const handlerDrawType = val => { if (drawObj.getMode().includes('draw')) { setModal({ @@ -195,6 +151,7 @@ export default function FlightArea({ } }; + // laanc계획서 비행구역 저장버튼 클릭 시 비행구역 정보 저장 const handlerSave = async () => { if (areaCoordList) { console.log('save'); @@ -208,29 +165,28 @@ export default function FlightArea({ setCenteredModal(false); dispatch(AREA_DETAIL_LIST_SAVE(resultAreaDetail)); - } else { - alert('아무것도 작성 안함'); } }; - // 날씨 handler + // 날씨 모달 표출 const handlerWeather = () => { setFormModal(!formModal); }; + // 지도 초기 셋팅 const handlerMapInit = () => { mapboxgl.accessToken = MAPBOX_TOKEN; const map = new mapboxgl.Map({ - container: 'preview', // container ID - style: 'mapbox://styles/mapbox/streets-v12', // style URL - center: [126.612647, 37.519893], // starting position [lng, lat] - // zoom: !areaCoordList ? 14 : bufferZoom.bufferzoom, // starting zoom + container: 'preview', + style: 'mapbox://styles/mapbox/streets-v12', + center: [126.612647, 37.519893], zoom: 15, antialias: true, attributionControl: false }); + // 비행구역 상세맵 draw 정보 셋팅 const draw = new MapboxDraw({ displayControlsDefault: false, userProperties: true, @@ -243,16 +199,14 @@ export default function FlightArea({ simple_select: SimpleSelectMode }, styles: [ - // line stroke { + // polyline id: 'gl-draw-line', type: 'line', filter: [ 'all', ['==', '$type', 'LineString'], ['!=', 'mode', 'static'] - // ['==', 'meta', 'feature'], - // ['==', 'active', 'false'] ], layout: { 'line-cap': 'round', @@ -264,28 +218,8 @@ export default function FlightArea({ 'line-width': 2 } }, - // direct line stroke - // { - // id: 'gl-draw-line-active', - // type: 'line', - // filter: [ - // 'all', - // ['==', '$type', 'LineString'], - // ['==', 'meta', 'feature'], - // ['==', 'active', 'true'] - // ], - // layout: { - // 'line-cap': 'round', - // 'line-join': 'round' - // }, - // paint: { - // 'line-color': '#000000', - // 'line-dasharray': [0.2, 2], - // 'line-width': 2 - // } - // }, - // polygon fill { + // polygon fill id: 'gl-draw-polygon-fill', type: 'fill', filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], @@ -295,18 +229,7 @@ export default function FlightArea({ 'fill-opacity': 0.1 } }, - // polygon mid points - { - id: 'gl-draw-polygon-midpoint', - type: 'circle', - filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']], - paint: { - 'circle-radius': 4, - 'circle-color': '#8a1c05' - } - }, - // polygon outline stroke - // This doesn't style the first edge of the polygon, which uses the line stroke styling instead + // polygon outline { id: 'gl-draw-polygon-stroke-active', type: 'line', @@ -321,8 +244,8 @@ export default function FlightArea({ 'line-width': 2 } }, - // vertex point halos { + // vertex point halos id: 'gl-draw-polygon-and-line-vertex-halo-active', type: 'circle', filter: [ @@ -333,11 +256,11 @@ export default function FlightArea({ ], paint: { 'circle-radius': 8, - 'circle-color': '#fff' + 'circle-color': '#ffffff' } }, - // vertex points { + // vertex points id: 'gl-draw-polygon-and-line-vertex-active', type: 'circle', filter: [ @@ -359,7 +282,6 @@ export default function FlightArea({ const language = new MapboxLanguage(); map.addControl(language); - // map.addControl(draw); const tb = (window.tb = new threebox.Threebox( map, @@ -396,7 +318,7 @@ export default function FlightArea({ } }); - handlerCreateAirSpace(map); + handlerCreateAirSpace(map, mapControl); // 미니맵 표출 map.addSource('preview', { @@ -417,6 +339,7 @@ export default function FlightArea({ dispatch(mapInitAction(map)); }; + // 저장된 비행구역 미니맵에 표출 const handlerPreviewDraw = () => { if (areaCoordList) { const areas = areaCoordList[0]; @@ -427,7 +350,6 @@ export default function FlightArea({ let fitZoomPaths = []; - // 기존 if (areas.areaType) { if (areas.areaType === 'CIRCLE') { const radius = areas.bufferZone; @@ -478,12 +400,8 @@ export default function FlightArea({ //지도 줌 좌표 설정 fitZoomPaths = paths.concat(); - - // 마커 삭제 - // const ele = document.getElementById('mapboxgl-popup'); - // const eleArr = Array.from(ele); - // eleArr?.forEach(marker => marker.remove()); } + handlerFitBounds(mapObject, fitZoomPaths, 50, areas.areaType); mapObject.setPaintProperty('waypoint', 'circle-radius', 10); @@ -491,12 +409,13 @@ export default function FlightArea({ } const coordValue = []; - const coord = paths?.map(coords => { + paths?.map(coords => { coordValue.push({ lat: coords[1], lon: coords[0] }); }); + if (page === 1) { naver.maps.Service.reverseGeocode( { @@ -527,7 +446,6 @@ export default function FlightArea({ name: 'latlon', value: coordValue }); - //스텝1에 반경도 글씨가 바뀌어야 함...!! handleChange({ type: 'area', name: 'bufferZone', @@ -537,38 +455,33 @@ export default function FlightArea({ } }; + // 비행구역 추가 버튼 클릭 시 const handlerAddClick = () => { if (!addData.isAddable || !addData.overAdd) { handlerAddChange('isAddable', true); - const obj = drawObj - .getAll() - .features.filter(obj => obj.properties.id !== 'BUFFER'); - // handlerDrawType(obj[0].properties.id); } }; + // 비행구역 추가 관련 상태 변경 const handlerAddChange = (key, val) => { - // const [addData, setAddData] = useState({ - // isAddalbe: false, - // isViewAdd: false, - // overAdd: false - // }) - setAddData(prev => ({ ...prev, [key]: val })); }; + // 비행구역 저장 가능 유무 체크 const handlerSaveCheck = save => { setIsSaveable(save); }; + // 비행구역 데이터 초기화 const handlerInitCoordinates = () => { const init = initFlightBas.initDetail.areaList.concat(); dispatch(AREA_COORDINATE_LIST_SAVE(init)); }; + // 비행구역 고도 저장 const handlerSaveElev = elev => { setSaveElev(elev); }; @@ -604,7 +517,6 @@ export default function FlightArea({ - {/* 닫기 */} 저장 diff --git a/src/components/laanc/map/LaancAreaMap.js b/src/components/laanc/map/LaancAreaMap.js index 039f91e3..0901ba24 100644 --- a/src/components/laanc/map/LaancAreaMap.js +++ b/src/components/laanc/map/LaancAreaMap.js @@ -3,7 +3,7 @@ import mapboxgl from 'mapbox-gl'; import threebox from 'threebox-plugin'; import MapboxLanguage from '@mapbox/mapbox-gl-language'; import { MAPBOX_TOKEN } from '../../../configs/constants'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Card, CardBody } from 'reactstrap'; import { initFlightBas } from '../../../modules/basis/flight/models/basisFlightModel'; @@ -16,22 +16,15 @@ import { mapInitAction } from '../../../modules/control/map/actions/controlMapAc import { FormattingCoord, handlerFitBounds, - handlerGetCircleCoord, - layerBuffer, - layerGuideLine, - layerPolygon, - layerPolyline, - layerWayPoint + handlerGetCircleCoord } from '../../../utility/DrawUtil'; -import flatGimpo from '../../map/geojson/flatGimpoAirportAirArea.json'; import gimpo from '../../map/geojson/gimpoAirportAirArea.json'; -import geoJson from '../../map/geojson/airArea.json'; import { FeatureAirZone } from '../../map/mapbox/feature/FeatureAirZone'; import LaancMapSearch from './LaancMapSearch'; import { LaancDrawControl } from './LaancDrawControl'; +import { handlerCreateAirSpace } from './LaancComn'; export default function LaancAreaMap({ - centeredModal, mapContainer, drawObj, handlerSaveCheck, @@ -43,42 +36,38 @@ export default function LaancAreaMap({ setModal }) { const dispatch = useDispatch(); + // 비행구역 타입 및 공역 타입 const mapControl = useSelector(state => state.controlMapReducer); - const { areaCoordList } = useSelector(state => state.flightState); - const [mapObject, setMapObject] = useState(); - const [isMapLoad, setIsMapLoad] = useState(false); - const [mode, setMode] = useState(); + // 비행구역 정보 저장 + const { areaCoordList } = useSelector(state => state.flightState); + // 비행구역 초기값 포함 정보 저장 const [mapAreaCoordList, setMapAreaCoordList] = useState( initFlightBas.initDetail.areaList ); - const [number, setNumber] = useState(0); + // 지도 + const [mapObject, setMapObject] = useState(); + // 지도 로드 여부 + const [isMapLoad, setIsMapLoad] = useState(false); - // const [detailLayer, setDetailLayer] = useState(); + // 지도 렌더 횟수 + const [number, setNumber] = useState(0); + // 비행구역 좌표 카드에 표출될 좌표 정보 const [viewCoordObj, setViewCoordObj] = useState([]); - const detailGeo = useMemo(() => { - return { - type: 'FeatureCollection', - features: [] - }; - }, []); - // 좌표 정보 마우스 드래그 const scrollRef = useRef(null); const [isDrag, setIsDrag] = useState(false); const [startX, setStartX] = useState(); + // 지도 초기 셋팅 useEffect(() => { handlerMapInit(); }, []); - useEffect(() => { - setMode(mapControl.drawType); - }, [mapControl.drawType]); - + // 첫 비행구역 생성 or 저장했던 비행구역 다시 열기 시 비행구역에 화면 맞추어서 zoom useEffect(() => { if (areaCoordList && mapObject) { if ( @@ -111,75 +100,7 @@ export default function LaancAreaMap({ } }, [areaCoordList, mapObject, number]); - // 공역 생성 - const handlerCreateAirSpace = ( - map, - useGeoJson = { - ...geoJson, - ...flatGimpo, - features: [...geoJson.features, ...flatGimpo.features] - } - ) => { - if (map.getLayer('maine')) { - map.removeLayer('maine'); - map.removeSource('maine'); - } - let arrGeoJson = []; - useGeoJson.features.map(item => { - if (item.properties.type === '0001' && mapControl.area0001) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FF3648' } - }); - } else if (item.properties.type === '0002' && mapControl.area0002) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FFA1AA' } - }); - } else if (item.properties.type === '0003' && mapControl.area0003) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#FFA800' } - }); - } else if (item.properties.type === '0004' && mapControl.area0004) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#A16B00' } - }); - } else if (item.properties.type === '0005' && mapControl.area0005) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#AB40FF' } - }); - } else if (item.properties.type === '0006' && mapControl.area0006) { - arrGeoJson.push({ - ...item, - properties: { ...item.properties, color: '#009cad' } - }); - } - }); - useGeoJson.features = arrGeoJson.filter(i => i.geometry.type === 'Polygon'); - - // 공역 생성 start - map.addSource('maine', { - type: 'geojson', - data: { - ...useGeoJson - } - }); - map.addLayer({ - id: 'maine', - type: 'fill', - source: 'maine', - layout: {}, - paint: { - 'fill-color': ['get', 'color'], - // 'fill-extrusion-height': 3000, - 'fill-opacity': 0.5 - } - }); - }; - + // 맵박스 지도 초기 셋팅 const handlerMapInit = () => { mapboxgl.accessToken = MAPBOX_TOKEN; @@ -187,7 +108,6 @@ export default function LaancAreaMap({ container: 'detail', // container ID style: 'mapbox://styles/mapbox/streets-v12', // style URL center: [126.612647, 37.519893], // starting position [lng, lat] - // zoom: !areaCoordList ? 14 : bufferZoom.bufferzoom, // starting zoom zoom: 15, antialias: true, attributionControl: false @@ -238,31 +158,15 @@ export default function LaancAreaMap({ } }); - map.addSource('detail', { - type: 'geojson', - data: detailGeo - }); - map.addLayer(layerWayPoint('detail')); - map.addLayer(layerGuideLine('detail')); - map.addLayer(layerPolyline('detail')); - map.addLayer(layerPolygon('detail')); - map.addLayer(layerBuffer('detail')); - - handlerCreateAirSpace(map); + handlerCreateAirSpace(map, mapControl); setIsMapLoad(true); - - // const detail = map.getSource('detail'); - // if (detail) setDetailLayer(detail); }); + setMapObject(map); dispatch(mapInitAction(map)); }; - // const handlerInitCoordinates = () => { - // const init = initFlightBas.initDetail.areaList.concat(); - // dispatch(AREA_COORDINATE_LIST_SAVE(init)); - // }; - + // areaInfo를 areaList 형식으로 반환 const handlerAreaInfoToAreaList = areaInfo => { const initAreaList = initFlightBas.initDetail.areaList.concat(); const coordList = []; @@ -287,10 +191,10 @@ export default function LaancAreaMap({ return areaList; }; + // 비행관제구역 체크 및 버퍼 생성 const handlerCoordinates = areaInfo => { const areaList = handlerAreaInfoToAreaList(areaInfo); - // dispatch(LaancAction.LAANC_ALTITUDE.request(areaList)); dispatch(LaancAction.LAANC_VALID_AREA.request(areaList)); if (areaInfo.areaType === 'LINE' || areaInfo.areaType === 'POLYGON') { dispatch(FLIGHT_PLAN_AREA_BUFFER_LIST.request(areaList)); @@ -299,6 +203,7 @@ export default function LaancAreaMap({ } }; + // 비행구역 설정 후 저장 const handlerConfirm = areaList => { if (areaList === undefined) { alert('영역을 설정해 주세요.'); @@ -308,17 +213,14 @@ export default function LaancAreaMap({ dispatch(AREA_COORDINATE_LIST_SAVE(areaList)); }; - // const handlerModal = () => { - // setModal(!modal); - // }; - - // 좌표 정보 마우스 드래그 + // [좌표 정보] 마우스 다운 시 스크롤 준비 const onMouseDown = e => { e.preventDefault(); setIsDrag(true); setStartX(e.pageX + scrollRef.current.scrollLeft); }; + // [좌표 정보] 마우스 드래그로 스크롤 이동 const onMouseMove = e => { if (isDrag) { const { scrollWidth, clientWidth, scrollLeft } = scrollRef.current; @@ -333,6 +235,7 @@ export default function LaancAreaMap({ } }; + // [좌표 정보] onMouseMove 이벤트 빈도 조절 const throttle = (func, ms) => { let throttled = false; return (...args) => { @@ -346,11 +249,13 @@ export default function LaancAreaMap({ }; }; + // [좌표 정보] 마우스 업 시 스크롤 멈춤 const onMouseUp = e => { e.preventDefault(); setIsDrag(false); }; + // [좌표 정보] 마우스 벗어날 시 스크롤 멈춤 const onMouseLeave = () => { setIsDrag(false); }; @@ -455,21 +360,20 @@ export default function LaancAreaMap({ {isMapLoad && mapObject ? ( <> diff --git a/src/components/laanc/map/LaancComn.js b/src/components/laanc/map/LaancComn.js new file mode 100644 index 00000000..d8e30a8f --- /dev/null +++ b/src/components/laanc/map/LaancComn.js @@ -0,0 +1,73 @@ +import { useSelector } from 'react-redux'; +import geoJson from '../../map/geojson/airArea.json'; +import flatGimpo from '../../map/geojson/flatGimpoAirportAirArea.json'; + +// 공역 생성 +export const handlerCreateAirSpace = ( + map, + mapControl, + useGeoJson = { + ...geoJson, + ...flatGimpo, + features: [...geoJson.features, ...flatGimpo.features] + } +) => { + if (map.getLayer('maine')) { + map.removeLayer('maine'); + map.removeSource('maine'); + } + let arrGeoJson = []; + useGeoJson.features.map(item => { + if (item.properties.type === '0001' && mapControl.area0001) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#FF3648' } + }); + } else if (item.properties.type === '0002' && mapControl.area0002) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#FFA1AA' } + }); + } else if (item.properties.type === '0003' && mapControl.area0003) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#FFA800' } + }); + } else if (item.properties.type === '0004' && mapControl.area0004) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#A16B00' } + }); + } else if (item.properties.type === '0005' && mapControl.area0005) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#AB40FF' } + }); + } else if (item.properties.type === '0006' && mapControl.area0006) { + arrGeoJson.push({ + ...item, + properties: { ...item.properties, color: '#009cad' } + }); + } + }); + useGeoJson.features = arrGeoJson.filter(i => i.geometry.type === 'Polygon'); + + // 공역 생성 start + map.addSource('maine', { + type: 'geojson', + data: { + ...useGeoJson + } + }); + map.addLayer({ + id: 'maine', + type: 'fill', + source: 'maine', + layout: {}, + paint: { + 'fill-color': ['get', 'color'], + // 'fill-extrusion-height': 3000, + 'fill-opacity': 0.5 + } + }); +}; diff --git a/src/components/laanc/map/LaancDrawControl.js b/src/components/laanc/map/LaancDrawControl.js index 0dd2ef9d..58990f26 100644 --- a/src/components/laanc/map/LaancDrawControl.js +++ b/src/components/laanc/map/LaancDrawControl.js @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from 'react'; -import { InfoModal } from '../../modal/InfoModal'; import { ErrorModal } from '../../modal/ErrorModal'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -25,36 +24,42 @@ import Constants from 'mapbox-gl-draw-circle/node_modules/@mapbox/mapbox-gl-draw export const LaancDrawControl = props => { const dispatch = useDispatch(); - - const mapControl = useSelector(state => state.controlMapReducer); - const drawObj = props.drawObj; + + // 지도 const mapObject = props.mapObject; + // 비행구역 타입 + const { drawType } = useSelector(state => state.controlMapReducer); + + // mapbox 기본 onClick 함수 저장 + const originClickRef = useRef(null); + + // 비행구역이 WayPoint일 때 bufferId const [bufferId, setBufferId] = useState(); + // 그리기모드 상태인지 유무 확인 const [isDrawDone, setIsDrawDone] = useState(false); - const [alertModal, setAlertModal] = useState({ - isOpen: false, - title: '', - desc: '' - }); + // 지도 렌더 횟수 + const [number, setNumber] = useState(0); + + // 에러 모달창 정보 const [isErrorModal, setIsErrorModal] = useState({ isOpen: false, title: '', desc: '' }); - const [number, setNumber] = useState(0); - + // 비행구역 타입 변경에 따른 그리기모드 셋팅 useEffect(() => { - if (mapControl.drawType === 'DONE') { - // 구역 생성 후 바로 directMode + if (drawType === 'DONE') { if (number !== 0) { const obj = drawObj .getAll() .features.filter(o => o.properties.id !== 'BUFFER'); + + // 구역 생성 후 바로 directMode if (obj.length > 0) { drawObj.changeMode('direct_select', { featureId: obj[obj.length - 1]?.id @@ -64,10 +69,9 @@ export const LaancDrawControl = props => { } else { drawInit(); } - }, [mapControl.drawType]); - - const originClickRef = useRef(null); + }, [drawType]); + // mapbox 기본 함수 오버라이드 및 함수 중복 등록 방지 useEffect(() => { if (mapObject) { const DrawLineStringMode = MapboxDraw.modes.draw_line_string; @@ -75,6 +79,7 @@ export const LaancDrawControl = props => { const DrawCircleMode = CircleMode; const modeArr = [DrawLineStringMode, DrawPolygonMode, DrawCircleMode]; + // 등록 함수 제거 const cleanUp = () => { modeArr.forEach(m => { m.onStop = null; @@ -154,6 +159,7 @@ export const LaancDrawControl = props => { setNumber(number + 1); } + // 컴포넌트 언마운트 시 return () => { dispatch(drawTypeChangeAction('DONE')); mapObject.off('draw.update', handlerUpdateSetting); @@ -162,10 +168,12 @@ export const LaancDrawControl = props => { } }, [mapObject]); + // 비행구역 데이터 지도에 다시 그려주기 useEffect(() => { handlerPastDraw(); }, [props.areaCoordList]); + // 비행구역 설정을 올바르게 마쳤을 때 좌표 저장 useEffect(() => { if (isDrawDone) { props.handlerConfirm(props.areaCoordList); @@ -173,7 +181,7 @@ export const LaancDrawControl = props => { } }, [isDrawDone]); - // 클릭할 때마다 마커 찍어줌 + // 클릭할 때마다 마커 표출 const handlerCustomOnClick = (state, e) => { const mode = handlerReturnMode(drawObj.getMode()); const obj = state[mode?.toLowerCase()]; @@ -181,7 +189,7 @@ export const LaancDrawControl = props => { if (mode && obj) { const feature = drawObj.get(obj.id); if (!feature) { - console.log('2222222222'); + // console.log('simple_select'); drawObj.changeMode('simple_select'); return; } @@ -220,7 +228,7 @@ export const LaancDrawControl = props => { } }; - // 도형 그리기 완료 시 + // 비행구역 그리기 완료 시 const handlerFinishDraw = state => { if (drawObj.getAll().features.length === 0) return; @@ -293,8 +301,7 @@ export const LaancDrawControl = props => { }); } } else { - console.log('333333333'); - // 좌표가 찍히기도 전에 틀만 생성된 도형들 삭제 + // console.log('좌표가 찍히기도 전에 틀만 생성된 도형들 삭제'); // if (mode === 'CIRCLE') { // const obj = state.polygon; // drawObj.delete(obj.id); @@ -307,7 +314,7 @@ export const LaancDrawControl = props => { // 모든 비정상상황 체크 const handlerAbnormalityCheck = async data => { - // radius도 있음 + // radius 존재함 const { coords, mode, id } = data; const areaInfo = handlerSettingAreaInfo(coords, mode); @@ -411,7 +418,7 @@ export const LaancDrawControl = props => { } }; - // 도형 수정 시 + // 비행구역 수정 시 const handlerUpdateSetting = e => { if (e.features[0]) { const { geometry, properties, id } = e.features[0]; @@ -482,7 +489,7 @@ export const LaancDrawControl = props => { ); props.setViewCoordObj(viewCoordObj); - // 계속 20개 미만이라 overAdd false처리(임시) + // 계속 20개 미만이라 overAdd false처리 props.handlerAddChange('overAdd', false); }; @@ -565,12 +572,12 @@ export const LaancDrawControl = props => { return true; }; + // 저장된 비행구역 데이터를 기반으로 지도에 다시 그려주기 const handlerPastDraw = () => { if (props.areaCoordList) { const objs = drawObj?.getAll().features; const areas = props.areaCoordList; if (areas.length > 0 && objs.length > 0) { - // areas -> 현재는 1개이지만 추후에 데이터가 바뀌면 여러개의 도형도 처리 가능! areas.map((area, idx) => { const paths = []; area.coordList.forEach(coord => paths.push([coord.lon, coord.lat])); @@ -639,7 +646,7 @@ export const LaancDrawControl = props => { } objId = polygonId; } - // 마커를 삭제하고 다시 그려주기 + // 마커를 삭제하고 다시 그려줌 const data = { coord: area.areaType === 'LINE' ? paths : [paths], id: objId @@ -754,8 +761,9 @@ export const LaancDrawControl = props => { return newObjId[0]; }; + // 비행구역 기본셋팅 const drawInit = () => { - const mode = mapControl.drawType; + const mode = drawType; if (mode !== 'DONE') { if (!mode || mode === 'RESET') { handlerResetMode(); @@ -790,6 +798,7 @@ export const LaancDrawControl = props => { drawObj.changeMode('simple_select'); }; + // 그리기모드 셋팅 const handlerStartMode = mode => { if (mode === 'LINE') { drawObj.changeMode('draw_line_string'); @@ -802,7 +811,6 @@ export const LaancDrawControl = props => { return ( <> - ); diff --git a/src/utility/DrawUtil.js b/src/utility/DrawUtil.js index 87553f1d..19cb3677 100644 --- a/src/utility/DrawUtil.js +++ b/src/utility/DrawUtil.js @@ -2,6 +2,7 @@ import * as turf from '@turf/turf'; import mapboxgl from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; +// geojson Feature 형식으로 반환 export const InitFeature = (type, id) => { return { type: 'Feature', @@ -43,6 +44,13 @@ export const CalculateDistance = (mouse, center) => { return distance; }; +// 미터 반환(m붙여서) +export const fromMetersToText = meters => { + meters = meters || 0; + const text = parseFloat(meters.toFixed(1)) + 'm'; + return text; +}; + // 두 좌표 간의 중간 지점 좌표 반환 export const handlerGetMidPoint = (dis1, dis2) => { return [(dis1[0] + dis2[0]) / 2, (dis1[1] + dis2[1]) / 2]; @@ -63,13 +71,6 @@ export const handlerGetHtmlContent = (distance, id) => { ); }; -// 미터 반환(m붙여서) -export const fromMetersToText = meters => { - meters = meters || 0; - const text = parseFloat(meters.toFixed(1)) + 'm'; - return text; -}; - // circle 360도 좌표 반환 export const handlerGetCircleCoord = (center, distance) => { const options = { @@ -183,25 +184,6 @@ export const layerWayPoint = source => { }; }; -export const layerGuideLine = source => { - return { - id: 'guideline', - type: 'line', - source: source, - layout: { - 'line-cap': 'round', - 'line-join': 'round' - }, - paint: { - 'line-color': '#283046', - 'line-width': 2, - 'line-opacity': 0.5, - 'line-dasharray': [5, 5] - }, - filter: ['==', ['get', 'id'], 'guideline'] - }; -}; - export const layerPolyline = source => { return { id: 'polyline', From 4ed5949babe59fd5a27d8799fcdc1f66711f7b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?junh=5Feee=28=EC=9D=B4=EC=A4=80=ED=9D=AC=29?= Date: Thu, 7 Dec 2023 16:01:02 +0900 Subject: [PATCH 5/7] =?UTF-8?q?laanc=20=EB=B9=84=ED=96=89=EA=B5=AC?= =?UTF-8?q?=EC=97=AD=20=EA=B2=80=EC=83=89=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/laanc/map/LaancDrawModal.js | 1 + src/components/laanc/map/LaancMapSearch.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/components/laanc/map/LaancDrawModal.js b/src/components/laanc/map/LaancDrawModal.js index c97c27dd..c340ab33 100644 --- a/src/components/laanc/map/LaancDrawModal.js +++ b/src/components/laanc/map/LaancDrawModal.js @@ -1,6 +1,7 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; export default function LaancDrawModal({ modal, handler }) { + // 드론원스탑으로 새창 바로가기 const handlerDroneOneStop = () => { window.open('https://drone.onestop.go.kr/', '드론원스탑'); handler(); diff --git a/src/components/laanc/map/LaancMapSearch.js b/src/components/laanc/map/LaancMapSearch.js index 8ec4b9d7..46f02767 100644 --- a/src/components/laanc/map/LaancMapSearch.js +++ b/src/components/laanc/map/LaancMapSearch.js @@ -4,8 +4,13 @@ import { useState } from 'react'; import { flightPlanAPI } from '../../../modules/basis/flight/apis/basisFlightApi'; export default function LaancMapSearch({ mapObject }) { + // 검색어 const [query, setQuery] = useState(''); + + // 검색 결과 const [searchRes, setSearchRes] = useState([]); + + // 검색 여부 const [isSearch, setIsSearch] = useState(false); // 지역 검색 @@ -15,6 +20,7 @@ export default function LaancMapSearch({ mapObject }) { setSearchRes(res.data.items); }; + // 검색어 저장 const handlerSearchChange = e => { const { name, value } = e.target; @@ -23,12 +29,14 @@ export default function LaancMapSearch({ mapObject }) { } }; + // 지역 검색 후 엔터 키 const handlerSearchEnter = e => { if (e.key == 'Enter') { handlerSearchRes(); } }; + // 해당 좌표로 지도 이동 const handlerSearchCoord = (mapx, mapy) => { const numberString = [mapx, mapy]; const latlng = []; From 1354a255ed447c2df7177b614160523d62d09f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?junh=5Feee=28=EC=9D=B4=EC=A4=80=ED=9D=AC=29?= Date: Thu, 7 Dec 2023 17:25:19 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EB=93=9C=EB=A1=A0=EA=B5=90=ED=86=B5?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20control=20=EB=B6=80=EB=B6=84=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/control/alarm/ControlAlarmDetail.js | 106 +++---- src/views/control/alarm/ControlAlarmList.js | 276 ++++++++---------- src/views/control/alarm/ControlAlarmNotice.js | 45 --- src/views/control/main/ControlMain.js | 158 ++++------ .../control/report/ControlReportDetail.js | 83 ++---- src/views/control/report/ControlReportList.js | 45 +-- src/views/control/setting/ControlSetting.js | 113 +------ 7 files changed, 271 insertions(+), 555 deletions(-) delete mode 100644 src/views/control/alarm/ControlAlarmNotice.js diff --git a/src/views/control/alarm/ControlAlarmDetail.js b/src/views/control/alarm/ControlAlarmDetail.js index b7c1e684..ccf0753f 100644 --- a/src/views/control/alarm/ControlAlarmDetail.js +++ b/src/views/control/alarm/ControlAlarmDetail.js @@ -1,56 +1,56 @@ -import { useState, useEffect } from 'react'; -import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Card } from 'reactstrap' +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; -const ControlAlarmDetail = ({ historyModal, setHistoryModal, controlGpWarnLog }) => { - return ( - setHistoryModal(!historyModal)} - className='modal-dialog-centered historyModal' - > - setHistoryModal(!historyModal)}> -
- {controlGpWarnLog?.idntfNum} - 알림내역 -
-
+const ControlAlarmDetail = ({ + historyModal, + setHistoryModal, + controlGpWarnLog +}) => { + return ( + setHistoryModal(!historyModal)} + className='modal-dialog-centered historyModal' + > + setHistoryModal(!historyModal)}> +
+ {controlGpWarnLog?.idntfNum} + 알림내역 +
+
- - - - - - - - - {controlGpWarnLog ? - controlGpWarnLog.map((p, i) => { - return ( - - - - - - - ) - }) - : - - - - } -
번호식별번호날짜내용
{i + 1}{p.idntfNum}{p.createDt}{p.warnType}
데이터가 없습니다.
-
- - - -
- ) -} + + + + + + + + + {controlGpWarnLog ? ( + controlGpWarnLog.map((p, i) => { + return ( + + + + + + + ); + }) + ) : ( + + + + )} +
번호식별번호날짜내용
{i + 1}{p.idntfNum}{p.createDt}{p.warnType}
데이터가 없습니다.
+
+ + + +
+ ); +}; -export default ControlAlarmDetail; \ No newline at end of file +export default ControlAlarmDetail; diff --git a/src/views/control/alarm/ControlAlarmList.js b/src/views/control/alarm/ControlAlarmList.js index 8701e722..f57a796b 100644 --- a/src/views/control/alarm/ControlAlarmList.js +++ b/src/views/control/alarm/ControlAlarmList.js @@ -1,175 +1,151 @@ import { useEffect, useState } from 'react'; import { X } from 'react-feather'; import { useDispatch, useSelector } from 'react-redux'; -import { controlGpArcrftWarnAction, controlGpLogAction } from '../../../modules/control/gp/actions/controlGpAction'; +import { controlGpLogAction } from '../../../modules/control/gp/actions/controlGpAction'; import ControlAlarmDetail from './ControlAlarmDetail'; import { Badge } from 'reactstrap'; const ControlAlarmList = props => { - const dispatch = useDispatch(); - - const [historyModal, setHistoryModal] = useState(false); + const dispatch = useDispatch(); + // 비정상상황 상세 히스토리 모달 + const [historyModal, setHistoryModal] = useState(false); - const { controlGpList } = useSelector(state => state.controlGpState); - const { controlGpArcrftWarnList } = useSelector(state => state.controlGpLogState); - const { controlGpWarnLog } = useSelector(state => state.controlGpLogState); - const { objectId, isClickObject } = useSelector(state => state.controlMapReducer); + // 비정상상황 상세 히스토리 + const { controlGpWarnLog } = useSelector(state => state.controlGpLogState); - - const [total, setTotal] = useState({ - totalDroneCnt: 0, - totalWarnCnt: 0, - warnList: [] - }); + // 비정상상황 기체 목록 + const { controlGpArcrftWarnList } = useSelector( + state => state.controlGpLogState + ); - const handleWarnDetail = (cntrlId) => { - setHistoryModal(prev => !prev); + // 클릭한 기체 Id, 비행중인 기체 클릭 여부 + const { objectId, isClickObject } = useSelector( + state => state.controlMapReducer + ); - dispatch(controlGpLogAction.request({id : cntrlId})); - } + // 전체 드론, 비정상 드론 개수 + const [total, setTotal] = useState({ + totalDroneCnt: 0, + totalWarnCnt: 0, + warnList: [] + }); - useEffect(() => { - if(isClickObject) { - props.setOpenAlarmList(false); - } + // 비정상상황 상세 히스토리 모달 표출 + const handleWarnDetail = cntrlId => { + setHistoryModal(prev => !prev); + dispatch(controlGpLogAction.request({ id: cntrlId })); + }; - }, [objectId, isClickObject]) + // 비행중인 기체 클릭 시 비정상상황 사이드메뉴 닫힘 + useEffect(() => { + if (isClickObject) { + props.setOpenAlarmList(false); + } + }, [objectId, isClickObject]); - useEffect(() => { - if(controlGpArcrftWarnList) { - let totalWarnCnt = 0; + // 비정상상황 기체 개수 계산 + useEffect(() => { + if (controlGpArcrftWarnList) { + let totalWarnCnt = 0; - if(controlGpArcrftWarnList.length > 0) { - controlGpArcrftWarnList.forEach(warn => { - totalWarnCnt += warn.warnCount; - }); - } + if (controlGpArcrftWarnList.length > 0) { + controlGpArcrftWarnList.forEach(warn => { + totalWarnCnt += warn.warnCount; + }); + } - setTotal(total => { - return { - totalDroneCnt : controlGpArcrftWarnList.length, - totalWarnCnt : totalWarnCnt, - warnList : controlGpArcrftWarnList - } - }) - } + setTotal(total => { + return { + totalDroneCnt: controlGpArcrftWarnList.length, + totalWarnCnt: totalWarnCnt, + warnList: controlGpArcrftWarnList + }; + }); + } + }, [controlGpArcrftWarnList]); - }, [controlGpArcrftWarnList]); - - return ( -
-
-
-

실시간 비정상 알림 정보

- -
-
-
-
-
-
드론 현황
-
- - {total? total.totalDroneCnt: 0}대 비행 중 - -
-
-
-
-
-
비정상 알림 전체
-
- - {total? total.totalWarnCnt : 0}건 - -
-
-
-
+ return ( +
+
+
+

실시간 비정상 알림 정보

+ +
+
+
+
+
+
드론 현황
+
+ + {total ? total.totalDroneCnt : 0}대 비행 중 +
-
-
-
-

알림 목록

+
+
+
+
+
비정상 알림 전체
+
+ + {total ? total.totalWarnCnt : 0}건 +
- {total?.warnList.map((warn, i) => { - const warnContent = warn.warnType === 'PLAN' ? '비행 경로 이탈' : '비정상 상황 발생' +
+
+
+
+
+
+
+

알림 목록

+
+ {total?.warnList.map((warn, i) => { + const warnContent = + warn.warnType === 'PLAN' ? '비행 경로 이탈' : '비정상 상황 발생'; - return ( -
handleWarnDetail(warn.cntrlId)}> -
-
-
-
{warn.idntfNum}
-
{warn.occurDt ? warn.occurDt : '-'}
-
-
-
{warnContent ? warnContent : '-'}
-
{warn.warnCount ? warn.warnCount : '-'}건
-
-
-
-
- ) - })} - {/*
-
-
-
-
PAV-001
-
2022. 09. 02 10:00:30
-
-
-
비행 경로 이탈
-
22건
-
-
-
-
-
-
-
-
-
PAV-002
-
2022. 09. 02 11:23:52
-
-
-
비행 경로 이탈
-
10건
-
-
-
-
-
-
-
-
-
PAV-003
-
-
-
-
-
-
-
0건
-
-
-
-
*/} + return ( +
handleWarnDetail(warn.cntrlId)} + > +
+
+
+
{warn.idntfNum}
+
+ {warn.occurDt ? warn.occurDt : '-'} +
+
+
+
+ {warnContent ? warnContent : '-'} +
+
+ {warn.warnCount ? warn.warnCount : '-'}건 +
+
+
+
+ ); + })} +
- -
- ); + +
+ ); }; export default ControlAlarmList; diff --git a/src/views/control/alarm/ControlAlarmNotice.js b/src/views/control/alarm/ControlAlarmNotice.js deleted file mode 100644 index 9c01ad48..00000000 --- a/src/views/control/alarm/ControlAlarmNotice.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Bell, ChevronDown, ChevronUp } from "react-feather"; -import { ReactComponent as DroneMenuIcon } from '../../../assets/images/drone_menu_icon.svg'; - -const ControlAlarmNotice = () => { - {} - return( - -
- {/*
-
- -
-
-
-
- 2021-06-17 12:00:00AVSF123 장애물 - 지역에 접근하였습니다111. -
-
- 2021-06-30 13:00:00AVSF123 - 비행금지구역에 접근하였습니다. -
-
- 2021-08-20 14:00:00AVSF123 - 국립공원구역에 접근하였습니다. -
-
-
-
- - -
-
*/} - -
- - - ) -} - -export default ControlAlarmNotice; diff --git a/src/views/control/main/ControlMain.js b/src/views/control/main/ControlMain.js index 24fd6401..4a0fb6f7 100644 --- a/src/views/control/main/ControlMain.js +++ b/src/views/control/main/ControlMain.js @@ -11,7 +11,6 @@ import { Cloud, CloudRain, CloudSnow, - Moon, Grid } from 'react-feather'; @@ -19,7 +18,6 @@ import { AiOutlinePoweroff } from 'react-icons/ai'; import { IoAlertOutline } from 'react-icons/io5'; import { ReactComponent as DroneMenuIcon } from '../../../assets/images/drone_menu_icon.svg'; import { Card } from 'reactstrap'; -import ControlAlarmNotice from '../alarm/ControlAlarmNotice'; import ControlReportList from '../report/ControlReportList'; import ControlReportDetail from '../report/ControlReportDetail'; import ControlAlarmList from '../alarm/ControlAlarmList'; @@ -28,71 +26,51 @@ import WebsocketClient from '../../../components/websocket/WebsocketClient'; import { useDispatch, useSelector } from 'react-redux'; import { controlweatherAction } from '../../../modules/control/gp/actions/controlGpAction'; import * as Actions from '../../../modules/account/login/actions/authAction'; -import { - ctrlDrawTypeChangeAction, - objectUnClickAction -} from '../../../modules/control/map/actions/controlMapActions'; +import { objectUnClickAction } from '../../../modules/control/map/actions/controlMapActions'; const ControlMain = () => { const dispatch = useDispatch(); + const history = useHistory(); + // 비행중인 기체 클릭 여부 const { isClickObject } = useSelector(state => state.controlMapReducer); - const { controlGpList, controlGroupAuthInfo } = useSelector( - state => state.controlGpState - ); + + // 비행중인 기체 정보 + const { controlGpList } = useSelector(state => state.controlGpState); + + // 기체 상세 정보 const { controlDetail, controlWheather } = useSelector( state => state.controlGpDtlState ); + + // 드론 기체 갯수 (드론 + uam) const { controlGpCountDrone, controlGpCountFlight } = useSelector( state => state.controlGpCountState ); - // pav박람회 -> uam, 드론 구별을 위한 임시 코드 - // (이 작업으로 고도화 하려면 추후에 서버에서 uam타입을 새로 더 받아 작업해야 함) + // 드론, uam 기체 갯수 const [droneCount, setDroneCount] = useState(0); const [uamCount, setUamCount] = useState(0); + // 비정상상황 여부 const [alarm, setAlarm] = useState(false); - const { user } = useSelector(state => state.authState); - const [oepnReportList, setOpenReportList] = useState(false); - // const [openReportDetail, setOpenReportDetail] = useState(false); - // const [openWeatherList, setOpenWeatherList] = useState(false); - const [openAlarmList, setOpenAlarmList] = useState(false); + // 오른쪽 사이드 메뉴 표출 여부 const [openSetting, setOpenSetting] = useState(true); - const history = useHistory(); - const openMenu = val => { - if (val === 'reportList') { - setOpenReportList(true); - // setOpenReportDetail(false); - // setOpenWeatherList(false); - setOpenAlarmList(false); - } else if (val === 'weatherList') { - setOpenReportList(false); - // setOpenReportDetail(false); - // setOpenWeatherList(true); - setOpenAlarmList(false); - } else if (val === 'alarmList') { - dispatch(objectUnClickAction()); - - setOpenReportList(false); - // setOpenReportDetail(false); - // setOpenWeatherList(false); - setOpenAlarmList(true); - - setAlarm(false); - } - }; + // 왼쪽 드론 정보 사이드 메뉴 표출 여부 + const [oepnReportList, setOpenReportList] = useState(false); - // const openReportDetailParam = val => { - // setOpenReportDetail(true); - // }; + // 왼쪽 드론 비정상상황 알림 사이드 메뉴 표출 여부 + const [openAlarmList, setOpenAlarmList] = useState(false); - const handlerLogout = () => { - dispatch(Actions.logout.request()); + // 김포공항 좌표 + const rq = { + nx: 37.558522, + ny: 126.793722 }; + // 드론 비정상상황일 시 왼쪽 사이드 메뉴 알람 표시 아이콘 변경 useEffect(() => { if (controlGpList) { const warnGps = controlGpList.find(gps => { @@ -105,6 +83,7 @@ const ControlMain = () => { } }, [controlGpList]); + // 비행중인 기체 클릭 시 열려있는 사이드 메뉴 닫기 useEffect(() => { if (isClickObject) { setOpenReportList(false); @@ -112,6 +91,7 @@ const ControlMain = () => { } }, [isClickObject]); + // 드론, uam 기체 갯수 계산 useEffect(() => { if (controlGpCountDrone) { const uamCnt = controlGpCountDrone.filter(i => @@ -126,19 +106,43 @@ const ControlMain = () => { } }, [controlGpCountDrone]); + // 김포공항 날씨 API호출 + useEffect(() => { + dispatch(controlweatherAction.request(rq)); + }, []); + + // 화면 왼쪽 사이드 메뉴 오픈 시 다른 메뉴 닫기 + const openMenu = val => { + console.log(val, '--'); + if (val === 'reportList') { + setOpenReportList(true); + setOpenAlarmList(false); + } else if (val === 'weatherList') { + setOpenReportList(false); + setOpenAlarmList(false); + } else if (val === 'alarmList') { + dispatch(objectUnClickAction()); + + setOpenReportList(false); + setOpenAlarmList(true); + + setAlarm(false); + } + }; + + // 로그아웃 + const handlerLogout = () => { + dispatch(Actions.logout.request()); + }; + + // 드론 상세정보 창 닫기 const handlerClose = () => { setOpenReportList(true); dispatch(objectUnClickAction()); }; - //날씨 API - const rq = { - nx: 37.558522, - ny: 126.793722 - }; - useEffect(() => { - dispatch(controlweatherAction.request(rq)); - }, []); - function weathericon() { + + // 김포공항 날씨 아이콘 설정 + const weathericon = () => { if (controlWheather) { let wheatherDetail = controlWheather.items.item; let skyDetail = wheatherDetail[6].fcstValue; @@ -150,24 +154,10 @@ const ControlMain = () => { return ; } else return ; } - } - - const handlerDrawType = val => { - dispatch(ctrlDrawTypeChangeAction(val)); - }; - - const ThemeToggler = () => { - if (skin === 'dark') { - return setSkin('light')} />; - } else { - return setSkin('dark')} />; - } }; return ( <> - -

@@ -283,12 +273,6 @@ const ControlMain = () => {
드론 - {/* {controlGpList ? controlGpList.length : 0} */} - {/* - {controlGpCountDrone?.length > 0 - ? controlGpCountDrone?.length - : 0} - */} {droneCount}
@@ -297,7 +281,6 @@ const ControlMain = () => {
항공기 - {/* 2147대 */} {controlGpCountFlight?.length > 0 ? controlGpCountFlight?.length @@ -307,30 +290,10 @@ const ControlMain = () => {
- {/*
- -
- 화재경보 -
-
-
- handlerDrawType('CIRCLE')}> - 화재구역설정 - -
-
- handlerDrawType('RESET')}>초기화 -
-
-
-
*/}

{oepnReportList ? ( - + ) : (
)} @@ -339,11 +302,6 @@ const ControlMain = () => { ) : (
)} - {/* {openWeatherList ? ( - - ) : ( -
- )} */} {openAlarmList ? ( diff --git a/src/views/control/report/ControlReportDetail.js b/src/views/control/report/ControlReportDetail.js index 7ce80f79..732c48f0 100644 --- a/src/views/control/report/ControlReportDetail.js +++ b/src/views/control/report/ControlReportDetail.js @@ -1,45 +1,51 @@ import moment from 'moment'; -import React, { useMemo } from 'react'; import { useState, useEffect } from 'react'; import { X } from 'react-feather'; import { useDispatch, useSelector } from 'react-redux'; import drone_img from '../../../assets/images/drone.jpg'; import uam_img from '../../../assets/images/uam_img.jpg'; import drone_yellow from '../../../assets/images/drone_yellow.png'; -import { IMG_PATH } from '../../../configs/constants'; -import { objectUnClickAction } from '../../../modules/control/map/actions/controlMapActions'; import { GET_ARCTFT_TYPE_CD, GET_WGHT_TYPE_CD } from '../../../utility/CondeUtil'; -import { - controlGpLogAction, - controlweatherAction -} from '../../../modules/control/gp'; +import { controlGpLogAction } from '../../../modules/control/gp'; import ControlAlarmDetail from '../alarm/ControlAlarmDetail'; -import axios from '../../../modules/utils/customAxiosUtil'; import { Navigation2, - Search, Compass, Sun, Cloud, CloudRain, CloudSnow } from 'react-feather'; -import { WHEATHER_KEY } from '../../../configs/constants'; import { Table } from 'reactstrap'; + const ControlReportDetail = props => { const dispatch = useDispatch(); + + // 비정상상황 모달 const [historyModal, setHistoryModal] = useState(false); + // 기체 상세정보 const { controlGpDetail, controlDetail } = useSelector( state => state.controlGpDtlState ); - //const { controlWheather } = useSelector(state => state.ControlGpWeatherState); + + // 비정상상황 히스토리 내역 const { controlGpWarnLog } = useSelector(state => state.controlGpLogState); - function weathericon() { + // 해당 드론의 비정상상황 알림내역 불러옴 + useEffect(() => { + if (historyModal) { + if (controlGpDetail) { + dispatch(controlGpLogAction.request({ id: controlGpDetail.controlId })); + } + } + }, [historyModal]); + + // 날씨 아이콘 표출 + const weathericon = () => { if (controlDetail) { let wheatherDetail = controlDetail.items.item; let skyDetail = wheatherDetail[6].fcstValue; @@ -51,16 +57,9 @@ const ControlReportDetail = props => { return ; } else return ; } - } - - useEffect(() => { - if (historyModal) { - if (controlGpDetail) { - dispatch(controlGpLogAction.request({ id: controlGpDetail.controlId })); - } - } - }, [historyModal]); + }; + // 상세정보에 내역이 없으면 -로 표출 const nullMessage = val => { if (val) { return val; @@ -68,6 +67,7 @@ const ControlReportDetail = props => { return '-'; } }; + return (
@@ -76,7 +76,6 @@ const ControlReportDetail = props => {
- {/* {controlDetail?.imageUrl ? ( - - ) : ( - - )} */} {controlGpDetail?.objectId.includes('UAM') ? ( ) : ( @@ -138,18 +132,6 @@ const ControlReportDetail = props => { {GET_ARCTFT_TYPE_CD(controlDetail?.arcrftTypeCd)}
- {/*
-
배터리 잔량
-
- {controlGpDetail?.betteryLevel} % -
-
-
-
배터리 전압
-
- {controlGpDetail?.betteryVoltage} volt -
-
*/}
@@ -177,12 +159,6 @@ const ControlReportDetail = props => { : '-'}
- {/*
-
현재위치
-
- 인천광역시 부평구 안남로 272 -
-
*/}
속도
@@ -207,25 +183,12 @@ const ControlReportDetail = props => { : '-'}
- {/*
-
비행거리
-
- {nullMessage(controlGpDetail?.moveDistance)}{' '} - {controlGpDetail?.moveDistanceType} -
-
*/}
헤딩 방위각
{nullMessage(controlGpDetail?.heading)}
- {/*
-
상태
-
- {nullMessage(controlGpDetail?.dronStatus)} -
-
*/}
위치정보 수신 시간
@@ -248,10 +211,6 @@ const ControlReportDetail = props => {
- {/*
-
소속기관
-
팔네트웍스
-
*/}
담당자 이름
diff --git a/src/views/control/report/ControlReportList.js b/src/views/control/report/ControlReportList.js index 5dd5d63c..6fd94591 100644 --- a/src/views/control/report/ControlReportList.js +++ b/src/views/control/report/ControlReportList.js @@ -3,27 +3,24 @@ import { useState } from 'react'; import { Search, X } from 'react-feather'; import { useDispatch, useSelector } from 'react-redux'; import { Badge, Button, Input, InputGroup } from 'reactstrap'; -import { - controlGpDtlAction, - controlGpFlightPlanAction -} from '../../../modules/control/gp'; +import { controlGpDtlAction } from '../../../modules/control/gp'; import { objectClickAction } from '../../../modules/control/map/actions/controlMapActions'; -import { NavLink } from 'react-router-dom'; const ControlReportList = props => { + const dispatch = useDispatch(); + + // 비행중인 기체 정보 리스트 const { controlGpList } = useSelector(state => state.controlGpState); - const [filterId, setFilterId] = useState(''); - const dispatch = useDispatch(); + // 검색한 식별번호 + const [filterId, setFilterId] = useState(''); + // 기체 상세정보 표출 const handlerDetail = item => { dispatch(objectClickAction(item.controlId)); dispatch(controlGpDtlAction.request(item.controlId)); - // dispatch(controlGpFlightPlanAction.request(item.objectId)); }; - // useEffect(() => {}, [filterId]); - return (
@@ -97,34 +94,6 @@ const ControlReportList = props => { {item.dronStatus ? item.dronStatus : '-'}
- - {item.objectId.includes('NAMWON') ? ( -
-
- - 실시간 영상보기 - -
-
- ) : ( - <> - )} - - {/*
- -
*/}
); diff --git a/src/views/control/setting/ControlSetting.js b/src/views/control/setting/ControlSetting.js index 162ef1d3..60b1ef69 100644 --- a/src/views/control/setting/ControlSetting.js +++ b/src/views/control/setting/ControlSetting.js @@ -4,15 +4,17 @@ import { Button, ButtonGroup, CustomInput } from 'reactstrap'; import * as Actions from '../../../modules/menu/actions/menuAction'; import { areaClickAction, - mapTypeChangeAction, - sensorClickAction + mapTypeChangeAction } from '../../../modules/control/map/actions/controlMapActions'; const ControlSetting = props => { const dispatch = useDispatch(); const history = useHistory(); + + // 지도, 지도타입, 공역 타입 컨트롤 const mapControl = useSelector(state => state.controlMapReducer); + // 지도 유형 변경 const handlerMapType = val => { if (val === mapControl.mapType) return; if (val === 'TERRAIN') { @@ -35,24 +37,16 @@ const ControlSetting = props => { dispatch(mapTypeChangeAction(val)); }; + // 공역 표출 const handlerAreaClick = val => { dispatch(areaClickAction(val)); }; - const handlerSensorClick = (val, isChecked) => { - if (isChecked) { - dispatch(sensorClickAction(val)); - } else { - dispatch(sensorClickAction('')); - } - }; - return (
- {/*
-
-

환경지표

-
-
-
-
-
-
- 미세먼지(DUST) -
-
- handlerSensorClick('dust', e.target.checked)} - className='custom-control-primary' - type='switch' - id='sensorDust' - name='sensorDust' - inline - checked={mapControl.sensor === 'dust'} - // defaultChecked={mapControl.sensor === 'dust'} - /> -
-
-
-
- 오존(O3) -
-
- handlerSensorClick('o3', e.target.checked)} - className='custom-control-primary' - type='switch' - id='sensorO3' - name='sensorO3' - inline - checked={mapControl.sensor === 'o3'} - // defaultChecked={mapControl.sensor === 'o3'} - /> -
-
-
-
- 이산화질소(No2) -
-
- handlerSensorClick('no2', e.target.checked)} - className='custom-control-primary' - type='switch' - id='sensorNo2' - name='sensorNo2' - inline - checked={mapControl.sensor === 'no2'} - // defaultChecked={mapControl.sensor === 'no2'} - /> -
-
-
-
- 일산화탄소(Co) -
-
- handlerSensorClick('co', e.target.checked)} - className='custom-control-primary' - type='switch' - id='sensorCo' - name='sensorCo' - inline - checked={mapControl.sensor === 'co'} - // defaultChecked={mapControl.sensor === 'co'} - /> -
-
-
-
- 아황산가스(So2) -
-
- handlerSensorClick('so2', e.target.checked)} - className='custom-control-primary' - type='switch' - id='sensorSo2' - name='sensorSo2' - inline - checked={mapControl.sensor === 'so2'} - // defaultChecked={mapControl.sensor === 'so2'} - /> -
-
-
-
-
-
*/}
); }; + export default ControlSetting; From 1ed9d9e3865eef0d42bac8f02dddb2a221ff8d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=83=81=ED=98=84?= Date: Thu, 7 Dec 2023 18:01:55 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EB=B9=84=ED=96=89=20=EC=8B=9C=EB=AC=BC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simulation/AnalysisSimuationInfo.js | 3 ++ .../simulation/AnalysisSimulationDetail.js | 1 + .../simulation/AnalysisSimulationMap.js | 3 ++ .../simulation/AnalysisSimulationMarker.js | 4 +++ .../simulation/AnalysisSimulationPolyline.js | 3 ++ .../simulation/AnalysisSimulationReport.js | 1 + .../simulation/AnalysisSimulatorSlider.js | 2 +- .../simulator/AnalysisSimulationContainer.js | 32 ++++++++++++++++++- 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/analysis/simulation/AnalysisSimuationInfo.js b/src/components/analysis/simulation/AnalysisSimuationInfo.js index f7a3d724..9d73cf37 100644 --- a/src/components/analysis/simulation/AnalysisSimuationInfo.js +++ b/src/components/analysis/simulation/AnalysisSimuationInfo.js @@ -6,8 +6,10 @@ import { Spinner } from 'reactstrap'; export const AnalysisSimulationInfo = props => { const [target, setTarget] = useState(null); + // 로딩 상태 const { loading } = useSelector(state => state.loadingReducer); + // 스크롤 끝 감지 데이터 추가 const onIntersect = useCallback( ([entry], observer) => { if (entry.isIntersecting) { @@ -19,6 +21,7 @@ export const AnalysisSimulationInfo = props => { [props.handlerPageList] ); + // 무한 스크롤 useEffect(() => { let observer; if (target) { diff --git a/src/components/analysis/simulation/AnalysisSimulationDetail.js b/src/components/analysis/simulation/AnalysisSimulationDetail.js index 35aa6e8a..97f46a31 100644 --- a/src/components/analysis/simulation/AnalysisSimulationDetail.js +++ b/src/components/analysis/simulation/AnalysisSimulationDetail.js @@ -11,6 +11,7 @@ import { ReactComponent as Simulation_icon } from '../../../assets/images/simula import { IMG_PATH } from '../../../configs/constants'; import AnalysisSimulatorSlider from './AnalysisSimulatorSlider'; export const AnalysisSimulationDetail = props => { + // 슬라이드 진행 방향 const [isRtl, setIsRtl] = useRTL(); return ( diff --git a/src/components/analysis/simulation/AnalysisSimulationMap.js b/src/components/analysis/simulation/AnalysisSimulationMap.js index c3db44fb..d6fbd521 100644 --- a/src/components/analysis/simulation/AnalysisSimulationMap.js +++ b/src/components/analysis/simulation/AnalysisSimulationMap.js @@ -9,12 +9,15 @@ import { Threebox } from 'threebox-plugin'; import gimPo from '../../map/geojson/gimpoAirportAirArea.json'; export const AnalysisSimulationMap = props => { + // 지도 const mapContainer = useRef(null); + // mabboxgl 지도 초기화 useEffect(() => { mapBoxMapInit(); }, []); + // mabboxgl 지도 초기화 함수 const mapBoxMapInit = () => { mapboxgl.accessToken = MAPBOX_TOKEN; diff --git a/src/components/analysis/simulation/AnalysisSimulationMarker.js b/src/components/analysis/simulation/AnalysisSimulationMarker.js index 0a0e2018..715e7abc 100644 --- a/src/components/analysis/simulation/AnalysisSimulationMarker.js +++ b/src/components/analysis/simulation/AnalysisSimulationMarker.js @@ -3,6 +3,7 @@ import DronIcon from '../../../assets/images/drone-marker-icon.png'; import mapboxgl from 'mapbox-gl'; export const AnalysisSimulationMarker = props => { + // 마커 css const el = document.createElement('div'); el.className = 'marker'; el.style.width = '30px'; @@ -10,6 +11,7 @@ export const AnalysisSimulationMarker = props => { el.style.textAlign = 'center'; el.style.backgroundImage = `url(${DronIcon})`; + // 마커 경로 담기 useEffect(() => { if (props.selMarker && props.selMarker.setMap) { props.selMarker.setMap(null); @@ -30,6 +32,7 @@ export const AnalysisSimulationMarker = props => { } }, [props.data]); + // 매 초마다 경로 이동 useEffect(() => { if (props.isPlay) { if (props.marker) { @@ -47,6 +50,7 @@ export const AnalysisSimulationMarker = props => { } }, [props.info]); + // 지도 드론 표출 const addMarkers = (position, id) => { // 이미 지정된 마커 제거 if (props.marker) { diff --git a/src/components/analysis/simulation/AnalysisSimulationPolyline.js b/src/components/analysis/simulation/AnalysisSimulationPolyline.js index 305b4c13..7030c746 100644 --- a/src/components/analysis/simulation/AnalysisSimulationPolyline.js +++ b/src/components/analysis/simulation/AnalysisSimulationPolyline.js @@ -1,8 +1,10 @@ import { useEffect } from 'react'; export const AnalysisSimulationPolyline = props => { + // 폴리라인 경로 담는 변수 const polylinePath = []; + // 기존 저장된 경로 삭제 useEffect(() => { // 기존 폴리라인 삭제 처리 if (props.selPolyline && props.selPolyline.setMap) { @@ -19,6 +21,7 @@ export const AnalysisSimulationPolyline = props => { } }, [props.data]); + // 경로 그리기 const addPolyline = () => { if (props.data && props.map) { props.data.forEach(item => { diff --git a/src/components/analysis/simulation/AnalysisSimulationReport.js b/src/components/analysis/simulation/AnalysisSimulationReport.js index 4be4be63..6d118eba 100644 --- a/src/components/analysis/simulation/AnalysisSimulationReport.js +++ b/src/components/analysis/simulation/AnalysisSimulationReport.js @@ -5,6 +5,7 @@ import Flatpickr from 'react-flatpickr'; import { Button, Input, InputGroup } from 'reactstrap'; export const AnalysisSimulationReport = props => { + // 식별번호 const [filterId, setFilterId] = useState(''); return ( diff --git a/src/components/analysis/simulation/AnalysisSimulatorSlider.js b/src/components/analysis/simulation/AnalysisSimulatorSlider.js index 87b643a8..b26f7964 100644 --- a/src/components/analysis/simulation/AnalysisSimulatorSlider.js +++ b/src/components/analysis/simulation/AnalysisSimulatorSlider.js @@ -24,6 +24,7 @@ const AnalysisSimulatorSlider = ({ return timeString; } + // 슬라이더 값이 바뀔 때마다 실행되는 함수 const colorOptions = { start: [playCount ? playCount : 0], // connect: true, @@ -64,7 +65,6 @@ const AnalysisSimulatorSlider = ({ direction }; - useEffect(() => {}, [playCount]); return (
{/*
Default / Primary Color Slider
*/} diff --git a/src/containers/analysis/simulator/AnalysisSimulationContainer.js b/src/containers/analysis/simulator/AnalysisSimulationContainer.js index 5f37dae2..ae420112 100644 --- a/src/containers/analysis/simulator/AnalysisSimulationContainer.js +++ b/src/containers/analysis/simulator/AnalysisSimulationContainer.js @@ -15,33 +15,47 @@ let playCount = 0; let playCounts = 0; export const AnalysisSimulationContainer = props => { + // 슬라이드 모든 데이터 const { list, count, detail, searchParams, log, stcsList, stcsCount, page } = useSelector(state => state.analysisSimulatorState); + // 비행 시물레이션 데이터 const [oepnReportList, setOpenReportList] = useState(false); + // 지도 객체 const [mapObject, setMapObject] = useState(null); + // 비행 시물레이션 데이터 상세보기 const [openDetail, setOpenDetail] = useState(false); + // 선택 마커 const [selMarker, setSelMarker] = useState(null); + // 좌표 정보 const [selPolyline, setSelPolyline] = useState(null); + // 슬라이드 재생 여부 const [isPlay, setIsPlay] = useState(false); + // 드론 정보 const [info, setInfo] = useState(null); + // 슬라이드 시간 const [timeCd, setTimeCd] = useState(null); + // 슬라이드 카운터 const [sliderCount, setSliderCount] = useState(0); + // 검색 텍스트 const [searchText, setSearchText] = useState(''); + // 비행 pk 값 const [cntrlId, setCntrlId] = useState(''); + // 드론 마커 const [marker, setMarker] = useState(null); + // 카운터 초기값 const [sliderVal, setSliderVal] = useState({ maxVal: 0, minVal: 0 @@ -49,6 +63,7 @@ export const AnalysisSimulationContainer = props => { const dispatch = useDispatch(); + // 드론 갯수 const [dronLength, setDronLength] = useState(0); const [countArray, setCountArray] = useState([]); @@ -58,7 +73,7 @@ export const AnalysisSimulationContainer = props => { search1: '' }); - // 시뮬레이션 타이머 + // 시뮬레이션 타이머 로직 useEffect(() => { if (isPlay) { const countCheck = log.map(item => @@ -95,6 +110,7 @@ export const AnalysisSimulationContainer = props => { } }, [isPlay, log]); + // useEffect(() => { if (isPlay) { setInfo({ ...log[playCount], playCount, playCounts }); @@ -102,6 +118,7 @@ export const AnalysisSimulationContainer = props => { } }, [stcsList]); + // 검색 변경 헨들러 useEffect(() => { if (oepnReportList) { if (searchParams) { @@ -113,6 +130,8 @@ export const AnalysisSimulationContainer = props => { } } }, [oepnReportList]); + + // 슬라이드 카운터 로직 useEffect(() => { if (sliderCount && sliderCount > 0) { let benear = countArray[0]; @@ -138,6 +157,7 @@ export const AnalysisSimulationContainer = props => { } }, [sliderCount]); + // 슬라이드 카운터 초기화 useEffect(() => { playCount = 0; playCounts = 0; @@ -163,6 +183,7 @@ export const AnalysisSimulationContainer = props => { // let maxDate; }, [log]); + // 검색 헨들러 const handlerSearch = search1 => { setParams({ ...params, search1 }); dispatch( @@ -178,10 +199,12 @@ export const AnalysisSimulationContainer = props => { dispatch(Actions.log.request(id)); }; + // const handlerStcsSearch = id => { dispatch(Actions.stcs.request(id)); }; + // 검색 const handlerInput = (type, val) => { if (type === 'search1') { setParams({ ...params, search1: val }); @@ -197,6 +220,7 @@ export const AnalysisSimulationContainer = props => { } }; + // 상세보기 const handlerDetail = id => { // setOpenReportList(false); handlerDetailSearch(id); @@ -206,19 +230,25 @@ export const AnalysisSimulationContainer = props => { setCntrlId(id); }; + + // 로그아웃 const handlerLogout = () => { dispatch(Action.logout.request()); }; + + // 비행 시물레이션 데이터 닫기 const handlerDetailClose = () => { setOpenDetail(false); }; + // 비행 시물레이션 데이터 호출 const handlerPageList = useCallback(() => { dispatch( Actions.list.request({ searchParams: { ...params }, page: page + 1 }) ); }, [params, list, page]); + // 비행 시물레이션 데이터 모달 헨들러 const handlerOpenReportList = useCallback( val => { setOpenReportList(val);