From 28a7a91ca22013355e39fdddfe45e9ab2834d8f4 Mon Sep 17 00:00:00 2001 From: sunmingLee <25thbam@gmail.com> Date: Fri, 24 Oct 2025 11:04:59 +0900 Subject: [PATCH] =?UTF-8?q?Swagger=20=EA=B4=80=EB=A0=A8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=ED=95=AD=20=EB=A1=A4=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/8.10/checksums/checksums.lock | Bin 17 -> 17 bytes .gradle/8.10/checksums/md5-checksums.bin | Bin 73965 -> 121833 bytes .gradle/8.10/checksums/sha1-checksums.bin | Bin 153107 -> 268979 bytes .../executionHistory/executionHistory.bin | Bin 85985 -> 275816 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 17 bytes .gradle/8.10/fileHashes/fileHashes.bin | Bin 20297 -> 25397 bytes .gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../8.10/fileHashes/resourceHashesCache.bin | Bin 19075 -> 21217 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/buildOutputCleanup/outputFiles.bin | Bin 18965 -> 19847 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .run/DistributionServiceApplication.run.xml | 20 ++ claude/make-run-profile.md | 175 ++++++++++ .../.run/distribution-service.run.xml | 51 +++ .../distribution/DistributionApplication.java | 23 ++ .../adapter/AbstractChannelAdapter.java | 86 +++++ .../distribution/adapter/ChannelAdapter.java | 30 ++ .../distribution/adapter/GiniTvAdapter.java | 45 +++ .../adapter/InstagramAdapter.java | 45 +++ .../kt/distribution/adapter/KakaoAdapter.java | 45 +++ .../kt/distribution/adapter/NaverAdapter.java | 45 +++ .../distribution/adapter/RingoBizAdapter.java | 45 +++ .../adapter/UriDongNeTvAdapter.java | 72 +++++ .../kt/distribution/config/KafkaConfig.java | 46 +++ .../com/kt/distribution/config/WebConfig.java | 32 ++ .../controller/DistributionController.java | 71 ++++ .../dto/ChannelDistributionResult.java | 49 +++ .../kt/distribution/dto/ChannelStatus.java | 100 ++++++ .../com/kt/distribution/dto/ChannelType.java | 26 ++ .../distribution/dto/DistributionRequest.java | 54 ++++ .../dto/DistributionResponse.java | 63 ++++ .../dto/DistributionStatusResponse.java | 52 +++ .../event/DistributionCompletedEvent.java | 48 +++ .../DistributionStatusRepository.java | 65 ++++ .../service/DistributionService.java | 261 +++++++++++++++ .../service/KafkaEventPublisher.java | 62 ++++ .../src/main/resources/application.yml | 102 ++++++ .../src/main/resources/mock-events.json | 134 ++++++++ distribution-service/test-distribution.sh | 34 ++ tools/run-intellij-service-profile.py | 303 ++++++++++++++++++ 40 files changed, 2184 insertions(+) create mode 100644 .run/DistributionServiceApplication.run.xml create mode 100644 claude/make-run-profile.md create mode 100644 distribution-service/.run/distribution-service.run.xml create mode 100644 distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java create mode 100644 distribution-service/src/main/resources/application.yml create mode 100644 distribution-service/src/main/resources/mock-events.json create mode 100644 distribution-service/test-distribution.sh create mode 100644 tools/run-intellij-service-profile.py diff --git a/.gradle/8.10/checksums/checksums.lock b/.gradle/8.10/checksums/checksums.lock index 837e5b9337bcfdbcf1aebd76ad9c0a8bfa0490ec..a1da254f90f45a01d64a9a5037595482128dd811 100644 GIT binary patch literal 17 VcmZQJJh3L%MKEUr0~qj!0RSw-1MC0* literal 17 VcmZQJJh3L%MKEUr0~l~D0RSxo1Tz2t diff --git a/.gradle/8.10/checksums/md5-checksums.bin b/.gradle/8.10/checksums/md5-checksums.bin index 04c6d0050987548d4ed9b0f3828b171e49d75fb8..589eb506a531946591ec1f98073f16c15ff73b30 100644 GIT binary patch delta 20671 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 LeJ7&W#^-+nrhwWc diff --git a/.gradle/8.10/checksums/sha1-checksums.bin b/.gradle/8.10/checksums/sha1-checksums.bin index 19a54106b689d3aaf682f717492529d1d26814ea..73ed738ef00ba4f482d387c458d98d3d467e884e 100644 GIT binary patch delta 49507 zcmbrnd0b6j`##=2=OhUy(sa^rBBzikQAmZ5BuNvRkR~aTsZt>#nU<`QBxFjGgd|Ba zRgyUrNhL`_MACQdeebiM=Xrmg-|zMLzP|q1dtK{V*L|<~-fQm;bXGnmSMmHUX?0sN=}hff=nzv-ImF9zqOHgb57Ud#Ey&RKx( z-^}4V55I8;=*R(`0~0xX*R6`-QD~xCoMfOzuNa$$s~Cnr#!G|vpBeHi8sp|guDrFuch%F zqbPU4s>4x)x~Z_tC%%4cOdOQWXh)gqCKj7MD>pCqR^0+%<4UPRo8_99=J7K30)Dub z!}}FaKCb#o74U@zQMJ0CWNVD|pv)m30P|eUapRBu4pwa3!{EHcd}Jmvm0Y~>^IFrK z65y(B=eT)WsQ#r7p=)@x{ZXJuM}PC1)hP-lUUPw5l*N%7$84zT`}`%~pIT6iNNm1r z?4E=EckTdoV!f2RW#z)*M-pmZ0RGmB!;_!q)Nj4}p1}ouGf|32Ed21~yY9WkYk=#1 z3+0NUg`%=ax_`gsF&Hl@3mIyd3I}gbA3XfaJmB4rLv9*k$wK+8dB!&v16ErowG$II zTqm|^`Axu|?M0y)Vqxq98Q0GTGXRt1p?HF&-n(_<^|M+A<1G(D=^D|(Q4M;F=ILAp zEU+Ka9GE0LsXeESQrp0mRYjQt?S;P{l{b}q84J8rE7VS~(Su74eO7%7ynH=mrYUxI z>+_tazdRvA;4B+lA9G+}Gt~zSaGY03m$#eLTaXU9!4)h1US|M`O zjCSg`@a=3Z`$WK6N~GLa1J9EdG5e=6xS)SMhZ}Ah-LWA-1+*V8;_wZkZ$tf~7+8iE zgvvDih4NF{Jf8nK57d|6k)f8o&^K46_sw4i7>xfs6@_Yvg>l)PcS>Vrf#-1t#S<*G zU0rGI-X(yAj6~^Le!{fY7mwRKMgX>T7?RbFmaO@6?c>YTUZ6LiK`)BhqQT486=}Bt zw={|4?r**s`Laz1xKH1s6dij>$&5W~FaJ&luE#Qt`{{;$$&{x4fEOE}dL2`t{pQCS zy6uyoyl)j!7TXI0FMK$2UFZzFkKV{lEH>YM{#IP>PIj~xay_zZua2WH>S7zGwq0s# zSHh5%pL-P!gKkVA3Kg3Q`HQ?B|9WT!SaLl|7mJ1clFa6Odc>B;k*GlIXYM#(_H^yL z?O@-tr&M$IQmaly>+yiSyj{?;?O!R=c;kk4duHIo5#Ha_jD%iNOaIs&#IG!FXp* zW7?;KS2kyk2EIl=4qp^G(&$<6OyD2zjQU~twd#GJETe8D&jOX!%ozU3HUA90;4p zfIQEqQUTP{{rJ2|FUJGVmaAADc;WG^ttTUdfx21lDO^qMok z_7gjzinu)uEy}&gxAET$HlsB-jhvFi%tKvkK|3V{dF$!uqw+B(_Dc`E1@PLx9Qgyw^?U`0dsanjzh#ktANZacVDnO)WgFlkYFoI-!7?Ff`yOg?|@=R z*|h#2`DY$OZwUC-D0hg7{(-+&SIA%3+6{sapGb}4IWxv+ygjo6@Pp4${SbS}VcYL_ zj@MEQ#@E;5xF<>%6V(w*4gy5z)WQ!OATRidX7Q{jxNI}VZqFoAjQwn$dr)I1<6&t~Dl zaE2!s;30J?X~S*(>*Aeb7@YU2jKjBkiP9!)Wm~(#8yV}1C6A}9bidcT32Zf==D5#$ z?^KiQkpqowLC98LEbQH#R3+0|1Kc0ak+;6RaM7g>r^kc&fL(7xnff}42VZ9e6@Sfw zpojM|7L|~~W&v;S+pDolUKd8y`liCO65pwA3D2SMF+0@9z(g^*&T~vbRX^AB_Dka4-N_jfxgJzMmoE^rJ6eo~fu{ zl)c2P!cpb)ESQD7{oE`(v!dsQXMKjB1g?KOk{#{qa^~sycZ2Wb`LLpC(i)ukbiFe? zwwMh_D|>PH_sR>8wpIK9{zf+`o*}B)SN>&h3*b>xk@skO;S}BPn=0a#1NP1dWfFrK zSG@n;wcW&e9I7l8IBO8?6qA>40puNA6Q7;!IPc;S!$*K09?$W2EFWtxI%WX)N^8_- zjH$5yu6Mfj2T}n$^bMJfnQM``_vSCO?CEU)_oYY$GP9E>9d*=#{eZWGt4Zd&J|+{t z+g=8~$KITnF$R>dNbl)2i#d}Skgu79(v5WVvpW9HZ4Rs62F!(nq{`>}hxNWT`KT@6 z&*tV77%5VU=RX}ucI`injXQ%UqLQ(GlFhxA3^|hH2C^!YR3W?goWnhwO}>8Re=UZ{~)6!Or>6R_v^k>NOd;aH8j z+Vq_^fCZmKq2t6tw_rPm$-DjnhBHz8I8$NPP{-m)`S2vl^D#gL1gp*cy8Xed3BcQk zPzxz5d+n-z-va{yTbhPs$NTE%HhQ>N_0WY4i}!Ydv^KfjE06E~$b+4q7hQn7$0rHz z9zzp=3)LuOqzFBQ3>VH^sqi@w+j@Em;45=;>P<-3CB>)oSupG>fJ+dPonR^)a@2Efhl&KSXCh=g z!A}@4XN_21tj}P49cPp-%EvD^RmSqs@bDFRngA(J*o95 z;r(dgFb&;dN5`>Y>iW&7;Jv9N^8AZF{8VUnewDhkkGgg$tQ*Vc`7(;U4SJ~My{}8x zCmX6~xYrs66v$Re$$w?mhI^Ft(*ouIAGN8SX>Y-@VuGB=39_&@7WM17Rv-}WykC2;XR@a(xeYOfbdMxH%l z)DV3Dh)?sR4s=g)vtKd&uQqU(eM5a3=2~=LiSLO@i>TJ5Xr^JGCpk6R+CkHfXE?Rn3(L<6@bQ|&t|8*LT9Ii5H7Ajvb{^WbUr zBACCt4GX14dE@l+dySg{`!8>&5{G+??SJc9r#Jx&KjH#T-r|iX%9a>r0{*=Xxiv;9 z@*bVLS=YCt2Y}BOqNk1a!jhA78rSuF!eG4Gk4W=_jz0fG&4fXg^Ffst$^};Zjx*2K z8O>V_-Ga(F{wMoj=wJSeOlgMTrQyC{;~*y#`aw}q(3Y*_o6@V05#`_4 zMiC$Eg~uLUP*Oiw3fSwZsN{o*zUQFK7d3rDi zFXBc7R9sH=bC{AgwFyMta}SPk+UCDP!y+IQ5cFS<%$mf`!~WhE zPJ0jWBrm?0)0ku%zhg)4FR;1Xl*6a3XmFocrUu#%-*EVn8q;EV+fd+pDImwDD1Esi zJHBrw{|b;7?BvLQ?XpK)zj_SF&#s~JCb7h_IBx8>H-mutl?%dpZfY6$JpU{NT>MiW zsJ_YGSvYuLm$GIea9?uA2lDE-yZk&>X#rl&4RLwRI~|kfJ!4=Q9txHEA+I-ogzuv9 z5Tx;IN|E75vHA3y(c2c>f=!iQ62@_Dd*18(QLr7fFGX{B_*QkZeX<7u|73%#KI&M= z-`lOcCeRi(OhNCx9JwQDVB)zkXMn8HB*lC6_@(AFv&|py?taMaqp5Ji=Umw}Pe53p zl8xd&MoR`*4_3XL44XUup_#P2!qR~|wD4JW;H#%2*=7?(h40;~K9wK=Bg#Xy$hcW7 z6sdn5BXj>bVAUUxEx`=FR65&RLuA1JU4^^}w(0r%LC@CyW_SWYF^Xt5(eM3PJ>jiU z6Wc{^8>QC!%nr)W8=Dou8p=SK&C!yJUOZD3{dIsv#Bki2bK+W~jT;%9f4m#%eG&_` zdqyq^%NPJ$KOr*vWD?nT0k3Y_ySHq-5-*kt^!2D8^Rg2i%Jtu$W z6${vB`3rQUPASI5Bzsq@MuD*b1{_|zzjD@xwXlct2Yf?rE!o0?JKNg^big#{Klp`Y zKbuNqlDA?Ta7_LyO|UC8J4i3MRgH4Mj3K=*ZJFTHVtHjOAS%PXR$NN z=Ox&_`5pGivDMUkY025N>m4qjxo|1RUG-HVB>($D2Ip-)#^GDhiF*#&onQAP67!*zt~F*21Xw}xs%0? zXiB*%#y!eZU0ObZc3%Z#+(t%!;fke?BVgs@cQzv1HnDlhl_`I2#a(B3{CaJ!{DW&M z=3)xY?f8eyIsD6M-GQ64W&=N+oN=_-OUz#>TuNH53VywLAmyrBF1m3}ulo~lzidXC zZT7;eHPP+vQ-1>1j!=7>pSfb8+z@Lm_5svYPFh~gRdYsW{Nw_LD^S|T;R~{kANh56 z7^`cG9KY&VsNK@s*Q(+f0%X~d9Qpf{s?=d~6d9a%z=*@U{}u;yZ$1kmd9^6;t60)E z!0K+Exf5{RH%qzd{nciBjz9bz@W2@;=4+Brb4{l~pN6pvCJ^Q$;zRF% zx1|w0I@B^>_Y_G$VF38%Qvy4EMOfvk=F(qr@0BE z@kndp1D{NGB0bkfvfurjZGWpZ4+w`R=j_E-+UkN zxi@$EH!z~xU$M?!0c_T#Bk%8~!pHAc6s>LF%3!?6G?eh&&wTalL`v>>E|ixklG@XZ zO2VnP$H9WYi%|a;eq6<6;-zfR{n-b#fA^EfUVBigJ+K?F_cNqit;K$?VhaAK0DdnA zIkuZhcJUl{eaK?_BrKKV9_$@AZtzZ7;GUR@V%q&A+NRSkUpT@>gKu9;x!T4bs-h;( zW(Tc%E2?kz6IPC&SheY41C-Z^M^-gM_Ep=^S+dQ=urT@@T)M*O)?s0u9yDHzbM+Jm9(s*V{ zU$w)GAn(x^)bb-*vZ&lCG<*|0F7tLqN{#C-`&3wJ5do{Dpl2uY{yDd|G*d=qkRSG= zC`aMz->uGrqF;hyGf|8<|P5RE)O$YX~mkQ`y2BH&IUdIA;JikH>)m>Ygwmt@jg`sdE8bG#VSq66U~D@-map!INrr2w4ST|*-QHm zya4c!J&uv1ehK$KNRC(^2~YaG*c=?%lQ>YGCwh43Zags8nd9`Hq)j~Iu4)yIh82|; zwH~+hqGBX@zz@o%gIfi>gkOnzT6>QC#%FdWj8$5@A=CXqaT^{!G zBFHEAkt$5FuoQ+?3fV`BU!3PtY?IB`wKaYPi!blt0}+%sa#`l%kPQJNK;$rY@;Jpe zr_Ren+SmtDIq?-o(k5)T`UWCT%sG5zUeBbJmh(U-jtiEiY@AcE!Kq^$@LOCt{>3vN zRn+&~z}F+Fe$J{bGLwA#V3Xm!Hj$F18g0~Y*lThRM148urfyj7yllAD5Wp8b=J?SQ z4W3L}44Vx<^A)}wL1jza4m{eGupEXB|8~2SJS}XVt7T>syE8r-gjZ~&OeOn=T)r5d zd>`Ds;=_>>Z)xt`)5b2PJCg9>jbxM`l3dT~*`Udg1%3SRrHz!1#k9633;!tfh3%Zb z7fU6qWc>zid=klq8o#;9SgE&~7Z+X-gUPyA_yH|FcZ$}OlZPTfxS6!5Fsq;5>FnCdK_n|ub`J6(mZM^bkqXY6R&G4YIO3!p>f zrR*6=&fU`w+p!~IGp9b|kTAf$D?A_gDpef+!_#NY57XhKk$=8|!+%R+ES9Ca0Kbjf z!eVI}RSV>RAHBn^s5s&bk(B2P*7H^|ODr;>Ps=p+Cau`GA1y$f7 z(L{F1vf?x9Y3!h=Nx+k%DIFK=aFznj6?X+3_I863E-Q1%6iW2wl_8QZ~BDzr%^( zy2K5)lLNr$vo5?fn#wkxXBVNs{|50m|0Eag*|;3i-uGh15{4`g{E~{;EQ(${WB%=R zAhMtXkJv;izviuHT*{ax0JDDL>6@r~lI(Po{s-JP0qFi)sxbRf_YuFH=n~+fblhVz zC6)|5ZnSfGe{%qjyGhBm)+andZg(GHaNZUn*4j+Qc-*YZfdjr60=dc#FWXGIuvp{t z*5v`23@A`U_y7&|zol?*vDZQFM!oBpneTAdH=#{ZmxCvTyWghuDr)<2AZ z-GjgIA$~xENj8~JhWyoq;;wJ7dknGuTluMH!6Mi{`R(2KL=1ITaqjEBukRaMyasC0 zJ-loysnzI`_&ciK`Z6Ft*+yC``>}JXUTyYfm(RVOc;i-L?D903S39hPK+f2X`);Fj z^zDDi44&B>m<76fIEi_EN3L2xycrCuPjdW(E>bJb#??G9hcn?3=M){c^Ui_@f%IXIwH*v$k z;o-=vuIj5+fDimBwdn99ji<+wE;9W%@cr5+ zlYkkLjMosRxxZ|gMQZhG1{BDrNM+{-_@~@BJ;e!dCGPZceyE=8WYY;SBSx&!4pz%-x%TuM?M=qeK zPX|c+D#hcrlWO$a-YGZlCA;qK-HqpLCn1<#`=f~+V^;vPpbnqgPK>Q?HZi7-wSnlG z5~(q#y@4u4vug$b`Sf^Pvz_$+Q12HDc3*_S#;=UVD|QfYgVN01rDs{UUW~(kcMu0q z?d~n_v_?Ylqn21Dj+$%X{KNP`mVd}#Fc!B`YRpA@{Mp!BhsUu_baJ?{MC<#ox+|b^ z*%iCTk!qA3RE~Xil?P;xE_^+X(ouA28y-D=Nl7iBE8_68I5N2B_y3aeeF_^}`xi=e zUH$G)^2)A*!OcH%2Y-&EOk7;==AJg<|2PKLey-=(&-Beq*W|*!$@hHA;cpkzrdw;m zrp#L%!r@=<&Yzp0;m+s?TY}Hy}UadT^m$T8z#rS$Lx2 zy*hx;?Ic~W@RdX6mZb+|f!(o9D!oWjwWBF7gPje&$2fdKrR~P48cF>%FOS- zf9TJ1?9REJn^KEB>Ta)c{xl23&*k90@nno|nK%EQX^}R7X?}Pz0VR1Qhvb_mI{*i2 zbLOr#xQ$j!Uj(?eHC_=<29x^QuOqY#+4mf2Pw`e7oN5^3T{UzDFb8+y!vvHB@73H} zbp!T8{=0IitXu4Gi)Y&!Vu4)Vj4#m&H+0__cB)=yK;AYIB*l|~M?2V~MJ-uQ|B^@9frMo~xbH5ipRhRHs`u2duv73aDPxsgWT_Y5 zio0s^iS?j01H11c&0rmV_Wrra5n!g8+c7;{!?cX_pRg;yX$r5|Ma|Wh8k3RHs+6IY zapT@EF8uG4E7t5B))C_q`>9`&NGhe%f3_AQ&3k=ZTG&5jY!m<7#6y51^UDVkDDr;X ztVjLt>(kj;xxV%Cr2|wGCAm>imekkZ2i();CQe|fY>T1mo`HZ5*oF@zl3r?ZUvYDG zzY74moWSQ2$y4v-gl?5&vsGYiQz_n>L^jRDMGp^4l6wJDy&V5dB2Q5v=1(gMKTihc z-V&@Sq{R5nLCRAYxBl~s5r1GY<83p=TFFF1-(pJRk3JVcWAGq+Aero(A>T$sY%v(f z?$MhJu+t%m9G-iIPO;PY2~@XB(v}GIQtH2Sz-2g*;jMDUmkyC`dZStSaMtw~;6_L# z{(FdYliuF$JwbQp1GtCV3)jpuKW$MxI|K0TMOY<;O4MKTX;X&N^5COjPp4RFZ%tQv z*IiAQ1i-acV#9u9e(jAuc;B;(o&2#gq~x`k*G~@amj&A_U$PlLOCgV?p%d@*k5Ys4 zKfb1+l)P?}B*k|?Bm~a9oz1wQAEkr09;QYMBmC~4x)&z~551@2bB9S!ohYjC9GM1# zo|lqv`SxMTk`ngZ8GA|d8;m)gdmjFqO57Xo?y&#GQ&x12oP*IBEr3UZD{=5Ut!K>MUpB*84 z)asF~2lo52;h44(UY17rDsFuJJA0`2_J<&lQi^YcGp&v+UaIF-mGJ@>wm? zI#eA5gUYOejq~Z4I5mKX8)6XMN0`Dz^^v zE>jEp4!?bVl{LJyNQG>j~T}yuwy|b9hv&+c1z`$367k$m-p%NVV+= z;QJnPCNDN`ntZwyI-CEOJ0~wvy1#OcZd?@bSNz0vmq|}q%T7CEe3hMJPR+QJfD*U& zH5$XO?qisIWe=%xQOJU^fy1=R0oU`vN>@nOT6EXiNjWJAPPh2#7%#j+s_468ysFuB zwzHF?r4l!{T={n3j)>j9bLDW{6%w!Ad|b3fV^$M8sec4;?B=zuns%_7$?!|8IQ+}I zhMqYyZ-66jvpKxeuzs6u;V1^@t*gcPS4b5nm+iI-@39%k+L8D%?ca`JGd%z9 zTdGjJuh&L3`8!^q5N(h7d1T-h^}L#1bRDJ%Z+|N`$|LbtaqC6j>>(F{D(J^KUHql; zyU}^iZ6G~JSt@<2$JDnn&nU!UdBnNVyse+wOd7ymQa45fl*I3|{ONR437Fs2 zIfbJ~@7Gy<^8tJxA7|uICi=HtE-IRB(}MyDY$;`zP)~We%P+GtNV5XJqN_6FbMWcL zoP3aQ8-NwAk}b`>v$MC`7uH;|1%3h!~c7xl|@mu3tCd&>%N@iodc zvP@;mZM};_0Sxix$Td@^EDfUAae6J4!{7Ojx56jcP%lZs;U5Zb>Fyo66m%vwbNE-! zOa9N=5qMazR*K)Jwv1Hp*>#H@DOwY`jNX=AsuTXO{rODqU$}qJwNJ`&U|@3HS{Ad7PfNZfe8Yss~Q7i(bt^PH48*{-gfE4pV_13T9YPrXj! z-=K-%mNg3oFd#3W9Oqvr`&8-2FX1t1zxxBafLoW#23<=tRb1By_(~2qvEK<5*ac9cK0pAW`va>K9eR?Y7Gyyx8vPJQ4uwX#PuPnWuzcrdu2_Z_Lz zWsf%YdssX8FIaS|#bJe%edH;#^Qjoff|DFxof7=6nyLo= zAg(&)zXSSKoPB>BaMzDg{sZNimGgTqX9HWTz~SP*n)B4!Kj7vq zj2jviv%sVXE_%2H>0IopTa0UgiDQoK_AuFxi21y3lVPqj53&mMQ39mWM* z!&Ht>)v8CsA4A!)lXz+onf8H2&2krdL7n+SYVpD%vR`P`m8N{z^A(t<5~NlhnJll~ zxX;S~aMTYUBMOpT{oj_A7efHc?~dTeuXD9p&o5^;@YHOaUqqRjQ}di=UyWm*fm3{> zSs?Wyw?MZZ^LkN5#?*~_)f*mkW(m!L3vBL1^n?tKdg81 z_bXuMFO;f0>FdNVi5M&Z{&q)fbCc9OMA>M&_g~nI1U=;NlAGih>$2aR*UzSZWS9bl zJY0N}boI?cx&a2C*xkNmH~vN!Z*f17G41IDVD_rT3dN-1ba%c!V?E|6I8bLQb>Qjf zjAyIR31}mpTLyL@WMNZkR-Ho?J0+cRadI))Xoo&}Ajp`_1`#VGr3%k%r?r%yUCVaK z#bgd&-kzeHc#nNLzh;DUh=?$%_Y_r+A?yj;6MtMvo71^597Q)rfp3FLu+lBkc^jQC zv=w;V1n>ygV$ZkFm_P37-c$zXJ$Z(Q-69GdD>EOwmHzEZRRa#WMYd^&QC0`O-uuij z1*(OZUqV`J?cDOuKmBw-wz35ql@PDOhYZxsXaZZj@H%W$LYB4{HY;L~gBT1QK8A~F z(EIH8!yW5jo#DNBfhXQ38}a3hPBZhLJOSnwE>Ns`Todg4>J+=}ICBqQRrOlkuhWdT z0e^Q0-h7+*@af2h)h?Hy83pREaPnal4wDr6NKG@{!^tr^Rg&8{%dv`?D^EEksSW2??;CV zSK;x3e}juPUw*Q**tsDcLM;9TZbx|4v(4_+&&X2nWOJZY=T+|-k5;Wy?RerH%5~7< zO3hM*s}Obbg1IMCDOpBFY=E^ZsKJVjuSI!VHh*KMrSEt=v4VVXz|+|y(p}{MHs0yr z&I-~5H+uewP_47NhJ@inyrJr!{;L_7)onfKnRj$Dr zmE@`2=NoT!wio-%yDeSn&8MBAe}@kowiN`Dr=s{NVq8{9lwA~-o)lkx2N}y>q4X)| z@v%pgx-+jcaHaG46v%kAMXJ;)I4x82%NcfO-i{?4J}lNEcI$Tt33xA#a`=WpPK}m} z6+p+$9NA9w)&KHqZ_RBtZI=9Upp^Wjo9cIY#;c7$eliwiPBj$<4rx%h zjx6}D15bQP988-sH{N^@Tbrlrkg}B+ zuYO9M5|(B?Gdw>QwoTq6H56*4WAWo>R^{lSTiA_vwVc%6Phpbx$BAMisOH-f96sXM z$OmhG{RO=89g4RSOD5gAx%Tp2b_N{grpZswEpHXBhpcDYZy}!gj129PUrA%PooBb* zLj|b7DhhvlMtKR3o`2$HZ^<4$tSG`Z&q-x&KDcWbmdGw#kLI9=>0-R)IpryF`ZCvb zr1D764)But_j`{+iJ7kgJ3(S2@T=z}kSxA&vcA5X?Ew#Nsr_B1r24e~ZFc=~sYKP& z#ln|C%O)(fJO{$gzfk*hQ@rQ}vAC({>IFfq5LEFNa?iS*Tj$DrH$OED3e;{yjx(Z# zw#r9m^*t2_*kU_WKEob|S5YGs|GZeOOAT&Hhnzji$Z)3a<%oLfKrphLDd8)mok3|c zMaXWZs_a2U2b=p$%FSA}Pg119vA0Yq8D1>5Z={rvyR{nM!S(@KZ*9a^IMo|vk*kBf z9=VE8x3xN7;j|^PpGDs@V)HgJsr5FhvPA661LR>t`12`p6`?E}!q0qysM+*A!*?9V zqzYy;{G59z{-g*M%vR?+IJPqPVRobp!OQJLD9ny2<1~m#X%Ln|&NUQB)HFzWQWd9h zOlssAM(yG#?4iUHp^;~ZoRc{uw$FvU*(hi(@yscdTt&#kmS|lZjk0a&d(b+@q{{6X z{-qd{V{fG3;5;0fM|BoTa~Q@~xbzcMI1nA@6mk_I$N8$V2Y#nG^hGP@i%^myq=HeT zqXOq?iVD9smfSvv*Z3*Eo980dGpM97iS8dDaQv3Nh+K>#n>pp4<4t{^6 zATswE#&__SL4m}Chc6NJw?WyyB4psph*qygLSG{4Z%3~G$q4jQS8(uO%P7eVoQPH? zD4`X8ME&2eJYUj{)u)iAp9qx_MN(1!?MzDOPun_&y!=I|$`5S4Lxp~{t;^&pLjTUt z44`ennjjB_s(r|COxX`DU>Je^-B!d3 zD0cx>tPr7~Kw2>ltqdZgtL6z_=|DzTToAah50Zw##Je|2C^A@t41*!7h)JcB(h4r4xThD+at}QWZ>@tw_95geurFKbe#^5hsJF<_Ve@BtqIj#Qt(Qa!=Zq@k4~F zS2Bz@>8N-mnS9G(5tXrtMOCW|WJ%2qpF?ILWTFL=wqFi$h>WH&tXv0!Nk{(+M2#WL zn0?cM-h>bn%Qup%2qmv(8sS1HHrFE+J{=0lqiA<1nWQUepW;wF@%`WLtyj_bJ2Y|? zEks{M$Yu>`fw$98${PBfX@S6DBsIDZne4>6dRvNY*NIRkDMwm6@C=jsC-dhzrk&=0 zV3wJ@4UCOi1(dphtc$B@6Wu6!wFu>HAonY%|ADuX5~Q7vd_$IDqv^XeO_UjCq@Yl{ z8phP9cvS17geHfQ#$GWMFg00JxmXGP4WqH?|GzwOIH^f(39<_(Ex96*Tt(}s7-X~JaQGGZJQY0aWxc3QUz?v83{KN?t4DY^dKYCV>503 z7bHqIGxpvON7;nqu#&!t(D*HYdLqp&H0n;SA{0na(gMdzQ6WJU-XFw!-6##D7(+U5 zWdfZbD`gCnyoThtn355`d$f_L2c)Hr3q?n#Qk%YbcNQsF%8QDfJB;HBOrjV-$Z?l20;NwCZXhRWvO&NV7 z!UnTRu%YmA7uK|e6eGJN7-!f}!xS7g z7z3cxfIF=jV7!!ps^W(!^ri+wx>o?MB_;R{8>V96Y~pouyeb`eY?vm^N2NPyK5Ic? z97!AWrWx!qqktaf48Q|zC>46@emjXNx;Dnvq&B@N3AVR}6jPfGwvg7ap#v+DqPb|~ zZn~j*Q(|PZn@-VLj`Teapj8w|Q+doHDkKFeu1p#w6DG+-Eu>U3(V(4LM5l0|Eu-&& z1DBC<<87HH?@c-2Y*Ix!O`GtBR2FGN+VhT~xRh8yr}xl~&qOJEMVRE#%;nM?u(6!v zjb=P{K%)suIES!kb+0)@{b44n7lNZQiQeVCv_v?=Y$Qz5IpNT@;{~|OmNH=c4(AHf z>&dQgPXh7>-g;&R#$$)&klxIFMKhRH>{_c68ln2Iv?k-cpnP2EA? zV@*58-HkZNj;MGr!>+>&htW+#@<8Pvmka4bkiVTS>#!jmdw7$oyEH-y5K9ohQk~E#052;~unbcEUu$0*NwTo1_Z4HVh-C78q zr{jKenH;cKCCy`#OfJTAi48LIHT`e|VPlu`oXyGS>3O%|9VRy-Y--OE4`Xe6k`9ul zi*b=XHJaAY>W>ZQQ6#Kbv}Tn&^qo#W{CpniI~t0>74wKcHl(8!+7ggCX>Xgoj5A?F z=oBtzY7}OGvz8D8+73+nKVSkpIt5g72>9gpGyT@~7>#rm;h%IaJvW+|m`{E`X{WGo zX%-|%Xo%xzO^2{(EOsOoD&{j5{&Fd>PyuN}I@clW9gdt&S62h7uYDKCX^`|pN4hLe z4ad_;B*l~s?*-|9FH71`YOtE7k){Yefqywdo9QfM!aE%41o}F=aitScjCTg=np`?= z!_92AxPXK^BK(WV9Whx~h~GF9_DB~ft!sg!T}T^O&0#jQxkFGbX;+&CjIT48kd01( zud5-Q>_XQwd;@MFCGmfzTo)29X;+>bHeX1OhVWzbjR>2$GKEgi;mdy}PP>9DgXHmU zSHgTFP3Pj#Bz(lXX-P6z1{>m9S}A1_S)1u}8jKjlQ+SCRJz@y-klA{1=R(H+U^)h* z6X5;}CTZ)=IP!$2H4z1BNk$L<9sG`!TEFssa$jf9lc<;B}^etW>?4E zOQ00LH`C}ONh?M&2?->2(p&yNiv8#84p_>HXk+`OjOmkf*n|6cv-mJN)}fQMcomcQ zj};GR5#~_RY9z_n(s5QDbXI|D07O?Nm0x&B1X)0_LtrKSFVWt4j z$r`m2Y{rbIF9tU#FqoiU8q(|V6H4@!sSG@Lgu(b1D^W}b`N&1}wBNFK3!v;PPn1Hi zjMXm`drgMd1^nwVD7V92SgZ8cD6;Yv@D4mhT^%MagS&##+UxC(fncqs^t?__yU~4~ zPxmCiUAR9F)ibhquyutx1IzG&t2w@DGYOy--Pt&=Mpl=BmRp_X4_q3Y3;+)&?E;Q3i0+25v;i1Mce zALYzIFNu4bKIE#xAWP$(0|CFSiHv`Xg%9gOXAUl~0L*nJvi+^&GUTm)&CK@1eGJIk z$$hvmr0tmEldZmRSk2eq-q%SKf(I;hlFMNDf*xkl%1V?hic`mafZpOuY?1fxXyN8{ zrM9-~*jM_CCZTHL#O}a5FUL>W2u@_CqxRoo$%ThGTdkehgV00V>rKgx*0Kp^pML=N zM;hwWsiQBsTX@Z_#)W-H`gkf- z$}p8L@B%@gB1D0mV&RtcmCmOwz&l4?~+h2AH{Mk=Bsw+u2den`N=+sZ}ry?1( zcSQ?Z6%yW>Jvt6p*Dw_L*VNpae@VgDsuZx7Zc^LB-RGR!9B;vP)S^`!zU)Tldxt#m zmH(j`rTi5Ow;eUwp}U{`A_mPsxdh97zNBYM&*Gp`lt_pre^^s~C z%3F=Pvc(Zj_4xxGQPZ{P03+YsTK{XCoejjqH3)WcFKI{pZ~yXIY;Mu|ec0INeLx{APO3cGesWZ8&-d`ki~pJ%HKW~b z`}dQp(gFU%)3}~??~=dAwVMj20N&K#L<)Q8p6_$667XBxOQO;L<22NGjg%y{^wfNo7dP64+e03Y4m3K%pWV?odNR4N}RwaUlba3 zcUYfstmgrGxl<}V_P_iwI{A@2t?voEkcU9dGso?G^4@Aq9#1S925rWx<9@JYY~=Af z_ecKT%8&*8W~?V5HTlP%vvcO|jeXj>zdMyBE75=EPJYI`d)SfVa`0-7|HlF63dlql zEa`o*?i7qH{<(g*UO-xHQRn#5(wrsW*sWu@i-3}_2mK!^Znpza(kiuOys_o=@H!cP zz>g!WEK3yr%b(QS3omZBjThZ$JCsbQ;|L%d%fZauYE31>pi&^6l@qTAt2KZ{7ev^|)_*$NjJ@ zH!ZVbb+ac*t1(`|rS|)r1+e@G)P`~RfBPK($)e7|v*pC>nk1vmk^jr@1BiuZS_YZD z`xp@O_x50!9u+^wD)``SW}@@i*tc+!S@>j#z; zGvWd{oBz)rA2883X)T@p-1{<|?eN>U#n`mRVVB${8<5~3{oO1M|MZJA$!wzoFV)+}qj-|F6GyU@AG`cDsN3 zQFg?&bDv30$T)gIp=kUtu&6!^=gN@*K1MJ}6p&^CVE$fQFGs$j`QQFxf{w++nx1od ze`WvFmwy&Z8-8N9Vqum^K?3N0<35j^)cf~mQSfqhNv<)#1@wOtMcl@VYv zEfweXA`A9^`SA<#3E7iz;*M+Yfw|zORM+BoX`ZR&9Cls%R)>`pD3kdXMb01J&$|8= z*s5HBV9~y-#|lXs8#wE6!T&#hSwpHWy^`N{5oe3P^u|H;RA2gGlsuoTr)e+FGUoxMQoNc;2Kutb3*m8|q>1ZMDv+v@ z=03uOzO?12$;`97(2tZNCjwii(5Lpacq?;GA4cQkL^&#iPNLj+qP2y59uO}=#k=5W z)g8UrMW^T-Z{|J)F|XI#A?TD*8j7N=mC#{C-pXz9gO*SliY@)gfxE)T0375`j@_m$-ESK;GHWQ#;7gFZprlEC6j4M~d5DJ36akatCP2a)C+5i%o3yAE3ppxxwX zm(C$AvE|VJ$PwDXaojHEU8ijV(=P2Vkx>Ha`z>$5r@n(w^8o|;HH83n3Yp%xPazHJEjJtT=-xzjX1oN=nz(Q|?OA1Hm2dtQ3I`?-unWgq*z^!%xG)qR~>KO$9ceGBlF6L7{|@_RV1 z%li!2GJ*}=dH&lu_8Y<1N6tb%xbcqrW!BekcSXC$Ok@uoy2_E;BzrvI9{I}6HTUq( z8LQMl{(8PtsK(#n(b#dO79?Gw7O@Xn%^{pdsYjK{DT@oBajzdvzDIs?2D3 zPduoMsNSA%rKh}l1vqdoLRzhNch!C^C^myr> zUB@50&u9Sqfmo{2IC8pu>gCDcBmYbze)WL#*otdu^?HvUfk?OlR;VBc82|iUT`KWG z;I^b{L6;Xuf5@f3WA_sVzQ1|xOgSw2{Jc<{TtT|`(Z0$%>sPXU-kpQYEX9(4eDE$+ zZR&sDIqk{(rHrb8T0(yaFE*bLy<6DpHk@?uZ*%Kflb_LiLiaIE$MoqZ0)P_2P+DA!U^y8C~Ap-=udq&G*)=FKbsfwNwyi&*aqsF&P5=%DJD3%l2ZS_`NUs1cHhlPmG^Hz z}Dh$(Ai7#x{*;l}*8HZ2j^2>a5?tH-TUU_25#NP)TYXc@QYVBmJT{FOTLg-l_jH+|5v*fLh`2^ zv}mO5!WF;H)WuWs-?uY(Yij+HdgRWrokCV^6fH$|^BVT@O<3}(&?DSUsH**S2L9>R z2c3NBfI7`eTI$J@x*OB}c1y(2|8Pr$QsJ9-6NWdPBVUM26>XJpHt{QMz2s4S&c+50 zo$RZ>*+OCC9K5^yyMdSXvVy(RJim(Bylh{S1^A#6|5UbjA^SSEIIl`#3U|*{$yfR4 zIGy>|f3*|Cw{HFshvLF@Y@$3}5q3D|b}Rid4d2l85#dNd@|;7J zybb!^z~AxTZ0D%2$=SR#>hjwdHoqC4tKPs-T%++%YKNrqBS>|<_BvnQe8%37sDHZm zLoxnK=)%GVJs-0tZ*E zdHQuU_Ab?9|4bUeGAF8~s_t6nd3S35={15of3>-vqW3x5Pi!iJiuuy8% z!&_6^YR5;%%?@1KZ@XY`9)3sWaE()NdyCw*r^V{3d-uO}|B6;)aKB+u)T~$zLwX8v zxY5Nqs6q3|F{6u^_)OV^_F)wSmTu`dpXn>TAv+)Mwlxp_oV~x9mWd_4l6_An+7)Bd z?89%~6_b-EKNlReM!A?}sxQ7zOs?dCrm7Mj)ZN6b_UZ@)Qx@9b~v z%=R7%{dUQ|3)j3HrTCr}w3nxs#-w4+7L5(3Pi@r$PT|g<&)VSQd;I6~gC-sb;HvH^ zKfm(N*!bi8$;Ka@yXC)M%c^KxHxXR2!y1&@KGnUc{X5 z>IbKvXGz;IorQ{jzH}>w!|XSU=e=nI+pw=aa?c{M{I77ouQ+ zObxe7vy$CuwPE2nLC@xO6+~vq?(}NuLLM0Cnf@qMq|i!-$-DW&-pv$Vm%!)GDfrHH zGObJ8kpffL+GSY=7Y7zzmL+)Je;H+LC6|pj>wkGwirh$L!C7P^Q0h+Plp0ATJ8h-Z zoy=)X2)rlonp&xmNTQxeZ*ZPKugfmtbPz%NwZ0`>8n2HlJ*7tAI04rbF3Kwoi#jn5 zn?NfLrQUFoxQMupV1Xs;-6Bn8Zc^1QT*Fwj5dYhPe<;0IwJ44&Hr@)kj{3AnX7Vmf zCT)WA>`MX@3>T0LIk#Mqs747ixf)u9;!78ctI zEM*DxAYwa+Yy#!g#%q?-ouA%1bU#0S!8$L6di5Al{LY$i|YJjElmF zTd@j!CLK)6TdgW|rY0=`!{g}nIiHAGKgB)o`A6j*At1)1;bN>N+z z_4X~y$`$DSW)U;;3O19uOZXq(bl=KU4@tX~sITxpZaqRx%+GrVsdzXc%yL|XC>Lzy zWpW-@=^2|!8U65jE_KHJhoXsC+KfX$j(fDF^!z9M@xAqze752H=|2g7mbMaTKO(FH zc~^hJMf#txj-_6rEVdvn?af;deKIhhw~cJC9-d7%Ge*Q^{R&mrmAwW6x_ml=#~AGXKs0WgK4joAzJF~ho{?I zZ0gMP-OA&u)H`STg{r$nF};829*>$DWB^F+OP#S{< zy4z?{`nH#sCyJECk2L=k3Ad!w*(@6$FD1HYeZO|z@^a9}CxWJWqzo|9rgvzx_h#Sg zOmC!gNNMY8bnTzIOwDv_jg;x)t=SWH=&`HIBSBM}rAY-Y9n4k9+W8>oY^T3u*3zkV z+K0o#j2^6h$-2YqNKIO5HReAUp3qdv%b8y_lzduhof`(6YEf{E=fehtYFz@>H`_VS zbJF;kTu=QlX-G?_X6wh^x)WvmjN2VmW96Np^@n4=xj16MdLeA^yMq+k62Ek;cl_ty zJ4LVo{mas$mR`*^JW2VcOXmY$Fwu0RuC|Sq>VS&n#e>+uuoS6YE3KwgvTDu-r{+wH zGn87j8mF1^u-(?+mu%D^`n43_%B%Lq_k~5jSuK3Uf>KTCSSzh>Z7;KHZRAZMstbm? ziqdNrJ?!{qCNEzal8bSZCpDd<)H&!x%KZm8SnsCUB^r+Mq13qUEQbU z@MTpUx-J>IYMpmKYdXwki@d^pM0trM{eY|GhdY0C7B*E*WsCGS_pp zi{yzke7fFZ!x0CTOE!`MT(wbFX}52$X?K=s4qJ78ZnD#NX?*=DUl!FyWfs4w(fPa4 zGp=#xYA{w=YunVUq29dGBh_1Qm}9-&N7H%-z`uk-Vvl}=poYZFCb z(zK;+@5gsydWA+Bodsfst6xlcGZ60*0C&nKv~S#^zcd5%X0_h zL1B(cigeZb>YKF)jJdr+$f|283nH5}E42eUygA7>5|s(}=C)RaCzrP3LAGkIR!YPS zekUDsF&)!V$Qs<=CuO>NYu;`=*e)-88tXqAA{Dqg)!zKkf1_`=2J4vk^04&Qb)54O z!4IAa=hIo2r-0`O*-hn++WHr#0e9K@eEPUa zuk<;1)p6w#f6ghvzBA8swJ^1lNBq!RmxG_;tCL^3evogsQEL_Ye?L_i7Xs@T2Rhu7 zr&X7RO4IOE;ZIH)*6D8>wqj&()7gzVz*KiUffyGTm#^L0=Pv)=RWID7;?~;FIF(qd z+34M6<*%C$3Y%*6P|4nX;Q!Z?ik&|>saO|WM&BXL-~8FdgM+(K6HhSKI)Ag|_CSlI z$4t8;DY8rad#Cl&m#_5t719~^fl+^V+HppHZ1B^J?MvCnx@kHie|rjYyNYkK>ZKlF z`La=p{Qv!QWUbufhAX$hj^z`6?^%C)TQ;=u!QUL4tkrZ{Fv5Gs^P4R9akDg}jaD<} zcF0cmW%Zes6)uIgaf-@Zyv{Xg^ZLb1tbD8U>_0trS+Vo*!?Vb-o&DMVDWy5w+1~O{ zM8SD!Rhv5I?R`=Q`%eiLB5E^fZJQ37Y2yZ1EO^Lwkm~1~(#1B>zSmE^&&YY-k;QB- z>T1||ugRCy<&|<@o^w~B7hYAhcrukc64knzQV)-DQ7#s3CAUbqDJu`v$?l4HP;mau z(Yfs9mPZPGdsMT+57ztyL3Mk+LeH4`tm==n+AN=|G>N-DIcHjtKR5YJ=zAC}?f0nt zkI$W!j(L=65}GZ~xO>8sgH}aJnQfh#eR>qNu9mF2K6US3GVaA*J&ngBsYPp$K`g}q(w z&kJN)afQyWy?=4^^{`KmzE*^94at4l*i~+k|JHvUj~Ubs*H-mXzq z$rVh$JN|Qy(Dn`fKabGX)wS>Y4*qLHj(#s}s%t4f?|qXNr9P}ThkZ~RH~LSH-0t#E zkKEQdcEGSGx$~;$7ud1--k)(4ce2Lf(4*^{H;my#U6~@KdTBMTU5~eYQ)$Aq)3>Gl zNc*2WvRkW}`5<@u`Se>Hp=Q36*`csgw#%|FR}?If7t9VSquo!B_tvFJ_g_!Xu|A%w zU7OdB4$AYVJ5s8cKi$Xw_=Zv@&8kdm+>`F(JSjmz8JS}3C*DG)`6YfG{tUDjNbXY3W88@Wiqd(rx`A^0EP zCECZ-FsWJ~l3z|`2({K1JGPA$h~(YNXj!qs6J9QR!Q9@oK!DjaItp%1H0e)hL z5x~UcojccA#|$n`6$=gS;69DTv6h7Xim=!ip3wi_#l_}j6cGwOvi*1NP)y4Th{?cs zM#a-a&5Brvr!o>yN7hCClAi{Z-ab1AwW~<7dt8ZZITc+xVZ{mEW-z+#*NKmu3kCC z#2MGYApzI*y+ZnYaj#~!AgT^2uS^3x|!qkvnY23Fm=AT5Y5BW}_=6nSS?GRW4geQXVdRYq3H*X@$!N zmaiSGv;M80e)9EGw|`@Lon7$N3Ow7O-Nx;M7OgnPMshDhjg|N~$zn&pknK-0WDCwZ z3*TA4NSHfq&HzC-m~Ri?t;E})ZH@5A9udo!I5ZySuEb26Y%ntaY5I3;HfOWW!dT7H zCciF>mB%NS;-RU8*DzpL!Rsf%&Mf>ur3eqXpZ3kp$c0QVZx5YOShKOq&+$_$n~$JdVHBO&P#qQerpD-Ls4J1#AX{i_B}21)%BFY(D&VzI2muqDP(#rUtN9Q zcTa3=)=n!AFYcwo0SVv7DD?Vu<%K?FLf9ZA6Fy4%KbW7@eAt#`ENH5-=J3@jywqP> zN8VU*>j)D|N5ZmI7}_W2U+AhDxAfB4v7WusSopWATbqlqdm?%Yp1Mch6 z*qK>{3_sk0iO*8tm!xpt+6=2J=;~TE;k!hA zZ{NQ=b0eXVV^(x+OEz-2refrivp8UNBJM{UW9mEpWxFYC+5CY{czpEchr7qkPhq-c zAegU4#}Cdpu5!9A|L9AmLDSXLJlGWNyzUgi)m^>{_NMyYkgys*rufM*AArYv1Zw8C z?4KLcA6R^S6?|NcyAK?{>9uEZa5NLsOJN!4rB%En&EB+^)0YgC#|6K>2diXMKa&%?GcH?hHMcGV*JMm=uX2Zm zHP^iq6{$@E;42~yesb~3;-*1dI~2h(Qva*dOf=G#Q~BdJSFz;vR5(tOYa^`3oWH{B zC93Qy$S0fLcA8Z$-3qy)n%AL53f|_^@85pd`M_*eU#Uz{CjZ6xDBjM=EgyJqA0y`^ zYpX8RlYi;#@r8MOdp7dRLg=4D3H+yrsc47VxI6TBT~LShS1D_rQ+wNerQI`@`zcjk ztggPPE8K4!Ju846GV0qeKkkPnsrr*O2V7L=HcoEC zbyf6$WvSSyea_i3c>2Jzfkw6W@U%~r768Xn@he-bc?Yl2`=e|+(yq?Z(gx=24^>^-)r_Lq!75ZNt!4vflkKj>^%zops(&Qp{^y+%) z3Vmg-d8JmBXE?}xV-)(nCL8OilM9)CC{>{!nDOeFRmm&pyk5Wg=N#=5s2Mz^bY=BZI-92X6|2|Tr_O%FCXf) za~?dFg8)lKI%ngD5x)lSVfr3rW$x3Hj&$--Cf)oi3WsdtTFqyC{s~A1PUo|6jTZV;RKGphvn1CBk#PS{=ov3#c`dzqWF*0#0TF+SUH>CMOf(qnIz*8L1tHG!+;1x zRjk1SJU!piJiv6GQtC{aBjXXKH-k2K;JJJhN3vu_*x;9k$%jPFX3_n z)Hj*K(Mwv!nS{~8%2<%*Q1lWv=X!RoP}H2C#aDEI2h9bK2Q7L={a|Pdnb#i%w4jKk z{Zz08JZ=k<{W=5s;HSh0iDXN^Gw!7O%GP`ZW9-s1MnCNoy2a3%zOjW!=&q{8__^*I zT+0=Vx~t3>wa%hIqD(4R^XwlnaXlF=+f9}$lc113poNGndoffqEI%TZwL=t zUQuY>kY3g)8xPS9QOd2MYEHDYafYC*%yCL z@NXjf@{GQog|Q}v@n^K`>tn3=7R|m^Ec>MqM3O9$_WTO>n_xXud<+3iu^C9&E`+za zvT*w`5Z9Cqm)k*XQ@Z=)l?1)+Z{R)gAo%OSH=qQy!I!=Oh1T|Vffde(1DbK3%Xu+C z&&q~AHVXZl(eEztBaCZC;g|P?G%5tG)>1+D_2kuK@$1PV39IA9CzE0MKF(+PBiOMI zigHD|dZNIfAp$`!VIHvKpeO5CmmZz;(UH6l+y| z&^}=#>Pd27oaw@usi*?Uh0G`M!jhhS4=`%U7qrq2s*$G(N?Jn&eEsMNnlv@WPwml~ zU%?ZR6~yeA*&t~C1T`_5qz8%!{veVbC<5)D3e%>>XTj|$nO@VB3i#^LQ(^i#T$PdV zcFiEdl$F=u^9;p`o^ra@EMaWsCc-eaEai_@x}lYzw5I~hl6iFtV3ZrxwceHrxKP?g zI3A<>ao0Hs0q)fHW^acqcNRdm(|MSr$7~T-`UDoRYmN29lud`w)+oMitB6bZ;6DonS z4&q`y=lU@sB0uGL)(G=we+6v^G>=Fa+@#Mqsv}2Vb_r6*_xBV%-8gUAIpm|QO-9Tu zA{2A%4)}CJ*%Op>%f;3wkfMFm?p(?wL>U8n$7MLJ!$qLOYD)A?rL*BvE&yCRA;m<3f`5vBSO zuR0w{$VyrdHlOnW2J}EYvOC3_b6z;8H=;QdbmMk-(-&+E6{w;w6>*M;7Yazh$6q99 zw1npfKyWYNNF40yg~rE`F1b}oRUwbFHCEv={+{{hDB}=zb!aEyYOGO1xJq|cHRD{M5}rgd8?(UXzU4fiIJ?f9X-J_*C!w&4?axPhs+inP zu)!a#BVzSp#_`W64>zp%L&BCr3>b+o#}3u;=u^E=Fj(+f^4D_9m>10B%qZ3i=8wGg zW;B_DS?vuG9Ob6Dtk$pSi2Txznbr!gj^ImX8?SGWAwpqfCmo&1==hXpy z?J6*O%iZ5eUWzi%yoB&lbSUQBPVjq$X6nBsGx?#y46(Ng_Rltu`HH)qQ#h$3$*Na! zSI|tzYWDz{661^f$!zR!Dd#kii~jR781R}JtNguGz`?o5O#UljM-_4$vO3`)vP8G~~{M4R4uCD291v zA6WMMF?ju+?!BAxz2_PrMll25iuN~F{~?Sg?E!2nnb9m+`%HHj^?`!Ad7TQj@PJYo zbgKqbQ!00T5U3{P63?!T(wu{@#g{P>X};B(ac;Tb27!GAau*8ObB91vj-PMFeB{g% zksE%O1lK>%t$ND`^vMqDv7N4prVgVn26n5vqB*jHZ?hbpX&Ba<2?6DF@h}TnaM~_}!-)zC{MHyM*usYQBGgH6j541sZr5A9R;N~HLRAG- zgB?~#C9*Y@vd;?`pQvK1zdFF(D&#+|6mIa-9=!o0A+t(^e1O>z$X3PtY6lm|n9D~( zl~oY=5fSn@{GS!B4h#OE(SzcSi0LQFXIzn*`G?h}r zK$z-?vvk6a`q&r59JPjy`iL$Y5Xue(t%?!L-eiojjBl`XZ#81PJFaNJO^xzHgsm$^ z5Kgv`YokVQN);w6mod9YO;tTy4nBq`j7V@hbz#xEL5a82%{>qrT|J| zA7Ub^h_Hi^CJ0k&$woiI;aUbbnYj89N~msRkSh^Qh%PKdG-HmnDXZKlffZz8q#1T2 zG4j%gp7AE=ZDxQoa@3tJ(nciwXp$w^pdy)^&Uz*q(!0%%H1z7WhXPXr(KX(94kl)3 z7zJ=7f(o&02SqGKqMe-@12MU}@d8TCQLY3(cuscbvs$Kx;bP_nTd=#g@4|-KD4k}` zI8P6dpWbKFdEFc9t<#-!%ks2WQd$bY$ld1oR{sQw`LZMg`M3Dt7UecC)m|N zHe$g(KoIksX9|Hte}F;K-HW(dgLr%$mOG}fh1x0R-mJsYcS_+{9fLL$^>r%YC#fxk z-=<>A3?X**HDhNB!S#e)!NVFmCS?_RtR^IW%M_H zHss=AVT8%SIl~zqLuFHh+}`RLuY>EB1}#XS59rsWU(4Y+@`7KDAUJ1b(1IxPTB-@a z{kjI&M&_>sO+7Roiggoz3*P|4Ch($=mzB7Y=A`fnSMz%8LMAJXlHnGJAR8KM=I#O) zYlMk1_W)$oGoW52)^sXc_kYK`<5t&W>p}t#Rj{GdVYp#7sE){5jrU|EOtdkugU8lN zHZEF=>k-)lu!JpgeVG#n7WJ7khDtb_LlYu{E8#AgL`)fsoXQY%in}3FR@_7-Ti9#I zf;eThL+EcO?3k4AhgNo6!&55Zych*BZIP86Sgi^$YalMdTvzDW08e;x#EoFMM3`wT zb=-`NZv$bMnRHLh4TbOq!s6U{uvIRtf&#g87%c6@m7IH+D{zj2h4$jN8*6$NqU_Oy z%l5)%lqnPuTq8qs<{COM*9)``qMoQw7%G=iAl?BrQnVt5sIy!}!z5|LCQIn!NPWxU zs0&Q3w+V6`#4Qwckts$sA=FXC8g&Iy9MRAiG=wolUFGJW$dT<{mF3l7uVwk`&{vD{ z`%!@8^WZ9}8#QEkxqUNk35jxzciBQsKge=o3x#~PfTe8V5x9|sdX0p^d5gfk5kjq# z4*EDDj7NwO&0EISYQ2?k&m3%TDB_%#DpQw<=!ZEmH9@AfhR0a*ziV4VSR>)fyycKe zwcSJaF`9YFP|*;fj{=WTkA4tcBlQ$EXzl2UwwOW1)m7QV3v&M2Dj@8aen2x!78N2!E;R#Tai z3L~2`rl6t=8Cp9t`&JgW%!r8{qhT1}Oh4dTM)pL~q%qp$@W#N15e;&q7~&K1qN3VmB*yBKTc1-n|Jc5q7$A{G}A2s;y| zrmPm25gy>e)+N~B!fatZ`7gNCBL7=G2V07OeyqW_PjyRS=*Ny=*Gk0qV<-gvdkNcU zcS>(TT{z*6RUs1TU9f_2d&1aV{Dw`ZyRfhzkxDpbZ!L39LRM?$q*Do3kY&#A7a)T0 zF*jK<71G^A!?R$&NQ{AcIs~*94lmfxvbWuZ!wa6lTdr|Myu zOwDR9g_E^#Oz-L?TAsyLqD~?o^Ab)i4uT>tw6F#R=sFi9K@{PR-i*(k0ks7L?T;1jT;BUU2ad zlq5+|M)dvx-gXj{WFtZ8hc5-;Lu_gRQr1I)kGMB82NO>p%tSp@rRAaPN$+4)+xi?{IBKNemLRm9FfNRDtPrtf$IhRBZa`jgSCae%MH;}oE)xQ}-45dIlF z#P#*AoTE%g>xz~Fy0WD#PSmwdVA+iYo`YXE6evLf%vRQDFd=xD<+H*#1$ygYKgy#o zS-g~CT<*@_fcV%=)SR_k2-ZA3Ogsf`+g;oy2LJ|i7gy*|C&3>v>a^gmXZ{(SD|E-{ z0KOcMZ3v~hVt)wkfj&IwAzV5$6sidh=*ch~#`hE#@Ju*t=!uN_$Ux7|%;4BsbcU23 z!q+qNnc`^y+j=sEUr5!fBfi*x-w&vq3cLK##)e*O{0bcFg|OP6;Z^1a`-|U^N%=nL z<}czo!%t82EX<%$FOkSIw;+&I&3Y@cuY0lVGj^k{GYs#AQ#gDa`<8z&dbM+iM0q`{ z&q7EU+nXt!nbKqyXq#hO(V!2`^i3-7(0&ZC>w~r6c=@a(;6mi#)jRtX())-y&+%fP zUX4az-B>*GB*5$Wi|xz(_^AL&oKH9xyZ_+S*_2$}uy2Z2Yh;`oG!x?CB(HJBpO2pq#U9`_3vg5c zJ`s>E;F5qXYB;DuU1xM`r!8lZwsrcP$jQQ8Vx~m3~>;Vj0AAmCPA*8g2Fbu1O z7ynw2gX&~zexpvN;S=kjuBnINUOjlj8vyatN(Sji0rB_YYLkSq029;mn`V&ADzQ+Mxk#7p%>^^0g5adX zRfhUCJvpG_o_~c~`;*6OD+*m~d-Pq?y8qdc_ouer>(ag*@M(`8oYG@AZSK)ml;Quq z!}hK6!|D@q+HC5_(kaEHp$ZeuUS%kCR4U1%N$~(($JJgda8P)BKOBi3VDos4^QD^71=$BrL#{I)bd0{N1ldLV6%djHRBFw zmQ#H*Vrqep79-7khDyfM`3(0>tCBKXako9!QOLu-*chKVpH*cUmB=nSGgds6+npfJdHNuJoi~;V`~nr)&a0 zz7y4AzDpLy<94BB4hp!v3+H6=*Qm&gU!zK`cSDU9P_`RZ^F5gH;T}jSdr*CY_R3;; z`d+A)1XyQcY<4D&Tv?_dXUPn_I1AEXBVe)-bJq#!h=2|woX*LX>3MxNcq#iJIqnA~ zeLt+11at^>&H-4>4?+qSkRssrL6Fzw2%Z4zLkM^FA%wg45F+sUMi$O}`e8rj3rOvU zO{wU|@E-rctLFA+Kl981P%Q>BBR@Nko!}XRAcYJDFLN+9v^gJZ%pU^Dx&RBUw}h8m zOV-3^9EZVT80O9&#zJ}XFy_hqhBGbC9nK7V*-P-^W5u+Ce!tV4&hvsOX%e^m7+14) z2kwHb1&|`r;nMdL9Ku8CSoN7uTR*}1Ta*E5@j@7OE`-{ZfwgptBu~C|krc?cEtYh= zbup}WcEVNOr}$v#K7(rf3|D>MCGhOK1Zt#!b4%c$?{4@zx)jqC(NGQ1Fr0`6xiK2# z$YoG3?*T=-7h!CSfkCdv2g9g`l$wbe+7=5+Yb+e(WudOB|AlFa7 zfah1Tv0YQ+V0e}dR~z|h1Z4x#&o@H4vk^SM&qeG4E`QGSJaiM% z+V=~|QFQ#8gDkmt$H42d{-= z%x@di4FamS!GZisfL1`FfTAzqrQu8PR;58blqOzbJEV8F<4OoM=HASDwMN@>LmzJB z+HlF9uMS5p&k1mzi~KH{i)VlTQ2I#Lw0 zWfm4jLE~76TGlTIInjs{?i>fF-4*=qJ{I#^1?&0L)lw+8IDlmTaDds%9J3GGM-JeN zXGuf&_HB5_wab}Zp2tDv)o<9p3;wy&VMczR?|QcOs|)fLbA#O z1mrP;<=r_yxyNsf=w{2NTXNTGyCkq5##fyZ~;0DL}1!d<^;CaSSEVE+F7Itk;fX%&ZXPw+cboUkLKkLcIJtMSzP% zFtiJpeFB$S?FqbN@=2(_oP>jSPJv=N1=Z>_w(yJ7aCPi7)H(rPXPDZurs#}AlFPiF z*j}sU>~;}jvo9i~QX!4F1Z(moMEzO`tP4s|7N!!6 zxm^~#%YrAs>nEtm0?zydb@UZT3$8%Acm*Z3^eSc?yNa>8t0;>V*8qb`Vck)Rv5Tef z+);{s3n+uSRKT7x1SyrXaQXkWsJ$0Izi}U5!HFh(;?hj`X52H$&`?%F>TO!H`Kc<5 z{k3TYr3RlVG_Crcrgea5TDv>fsNdSx^AFMX4X=d(JuGGj>8g-y>tu=iWE~cFsF!K~ zrke%10a3|BBa4<+_qW}w>@lppEv?}XEv?H_7)IP+dT!ec>6aTKJ2!EK2HZjty8GHw ztuXX9w`fL1bNilXZohBCYQ4>^np;(1?f}u@mR4Y|T|{$>4j1{ngPi={+?sv`@2}0R p{qr0q diff --git a/.gradle/8.10/executionHistory/executionHistory.bin b/.gradle/8.10/executionHistory/executionHistory.bin index 2177cdd01b3d65d3f655cadb7b28c6362b23ce72..3778430bf1cffdf4a829a73cbfc54ca5b7c22791 100644 GIT binary patch literal 275816 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

0U zb)f|b=inbf##l-g;VCBJD1vYZ5GZ)rDHJHH3QD8MJ!eK|B)zG9`&&)cH>NXQGGB^_ zqD1f+yF6SR#8;f!O)AK734CPb(i}Fp8x)7y2!>~Qk;5<^VNo8H;WG-ieGpuha7yHH zUZNzFlkjYyC^wAY|LX3zJ+|i2I-OA_{WY&`wv1zL{l;k_Z8# z1&ozhoJAO%Ln(qpah^paUZzomm1s&tjfI4fz8iVv(sDIE{G!y~Hs_%c4c7E2Xg(Ni zR1-yB&~fExf{+nRZlL(1Ql!2=<)EC*ZJr*6fQ@*bx zcXQ4BJ0^~dkTXKeYn*bW(Ri)$J7i-iS907skAtLSIG(~#mY^vCXIPxaXkc@R!*Glf zX_Q4#fuX!S?hC?~x1WBMMn4_>zkXl*k@igDxI>4`d!s$wo=ii^!%P#bjM5AVf<|T~ z45e5Cge6b#2*#om2i#8Km_fBC(|4*HS2?}CD%qv-+ydw4?m614tocy15#`~Qt8pTM z2q7f`#2-!)m@Jbh!IL$ zAQ_TGK#YMR!g7MZNN^t$<6*z?vLGQ@!$9U@n#4Vr@coxbLyvVX(kUV^qX&4$gTIsKF)Jg1`@~zgpsjWA)Qc zwzjbiSloSNzn%1gp?mrb9%8!rfPDyiZw;C2r6@^fYUgLWLc(20mW$+ z6+rlj956M7fXzc?2L=voAYLP4Or6R0&XucL=8j)*qG-R{MIMj{$oWZiL)wpWj2J}t zJk!-tuoY}1h-nOwaG8?`1o#B(0)<6Tf{`dVV?foG8IX}&c3?m@Dkg`jk3N-=IHb?C z2Ii5)myd`&*Z?*=6IDe+jEA9WEoKNH)UQg`n^g3<-(`oQNDSHP~e$%W|^7 zFg$`%3@ipkk^#f5+HeikRa;swkxH)Lx)ggp^IDC}UgpCQF^!NA*Tbn)|A^-~Aqi5i z)t^>DDIeevU<;CQlnwBrK|D~1$lwfz^P(v8jEo_mOEU;5i6B6DoRHbT;jjl`xQ6c^ zwF_5cUTQ{-Zm|RD`a`)_%)dv3ucDIb2aj%CDYz4f%BzV)tH)_|C(G~=4m@B^8-YPF z0#xDwYnDX_FyC=RViB6dF#;?{O6C<4A439$E9a%c+bUBuDN%0qX501sSNrbiv*Oif zmYH`)jJbweVtTSKqQHH)4Q3UN75KkMA_9$q<<80o1=f`a6HFko2&Ov)=cdhfu*SN$ zhUT|VuetnTLc@9ci;P>n@#=d|n72J3nkuqc8x6NH2!^pVPJ{b^;86_aa1_jN)gX~L zffr<)C3%6SZQhZ72=?v%uBqahXS!y_Y~f~^<@OO8C5AN6eFnY)BI}VZ=WtR08(Sj4b3ll=#IrahiLxX~phSD+gFh6IaR5b|Vrd$FojbRSSyplVz^1o$ zJz+i_@f0UQ=?-<0%0|9{mnyAQdEF}^GPOvEWZA1b`X;A(IffK+wBwJdM>{9?-(ub#5$ehbD06VQxH3T*Ri#7gz`&Lw6UwwVXz6pCy zbNd??Tw&f95r!dbR>K%d6p{`58k9|pqBuC3IRSi2ph40| zpb;D<(l`qTwO z^&FUx(rVWaNmYt}d9mrS2$?0j(@(JkwWE(f1zrMaK_e)r_yP$Bwt%w0)jWy7smCy) zEP%)pvyMK+6!baweqQ;^#`brwV)lge`qFQ=n@4y~-{aiVymRW22Q_#|@No&i;9#Xv zz|sf-)+$HSG=@|mKoa|b1vU%3%AHp$jc z3KpL3KN1<4WN03+m%x35iUJ{{;Pe6tB>0PjvNFRcd)3&Qem+tF1Cmc?5{q-&&3Si* z&z|R)pYU3{z7;0k&3!ryj@605L<|8XiuQDCNnwGOHj%`yUyV>rSJOh{S` zP(B1@{xW=mt;)2DJL^0)wr|lXmDk2bDf*GU16y^^C8lx4>Tnz-g8|915(S$Xe7`i# zkracnU`2wb60}&wXGVmi$bjWTk?n<}V-_zdxv_QAYL^{b-zc{(_bJLc-F2EsislhX zB6x|$WQ1WDP84~bCoymsvotG$zy(LW*E{9Ma9O~7h&sK^HKJmH?_77y361!zllK1{ z5#Pb-kmz=%0!w?8`~SAY#GW2is{wTpiz96k$6y2wHYe;4P{VP86DgKM!0G{I0R#6X zsFJuuv)+MLZJ=`4TAmc!ynJcq(_>37D!24TsWBsuMueh|Ofrrv@1~QNCLDfmUD1;j zpM0TeLfZJ95#ehf=Wu%81iwN!0^)Tx9E3Q@ViHXtA_#O8>;nX>WAOiqJeX*} zAl8=!1>~kORK;-l*%GV!G=6-+v=`p~xYzA^)uMN{W-Bxm_Rx746eGsX)x0; z8MJc=;xjZZ6A>Z&em^y!ON5Cd^x|x$?>n>iXsh)81q23pBD$?fi+P~Vhl!y?Q(!O&1APHwNDk#i6qgZM&Z5K*iZ3ml$E6IMRCiqKlC5v{8Fi-Q>zgBz zk-)Vf)p2LP56YzbOo1dyBv&G48+ zLa;>f7HNwU8k{d})5^Y;_Vc0M1$uqe;_{gZB}Si&;yU^9n)*R!GEyQj4&urLA;6C3 zzy%8Kbcg`~NdhK-Q{DnMn~p_4$WYGi=dFgJ5oi11`>m4AuoXTFG{pXK&%svTUo$yT!P>sgp(n_ zj3_6V&QHd8U*zdJv#&nY`_n5Go6f&6{=%+8ksY?mN#eucoejio0SIPD*xyU0&Vdqb%A#sfm5d(KAI1mUD;ukVP3XDiX z90M2~qAnDe4UpjiZmE!TsNwM}!=UJzan1heJLK?>EnDi!D?3w0I$VZ|@jll?@K&NCFc@+!+`8JGzpa_M5s}kWECHTdPn8boj2?qcL zu@97!{Re0Hek_NlUY&Ha#)tcn0%>3UJ*wZ7$dsc{tYg@F#;c9bR>d~f{3jxKfd#NB zF_99%8w6%CBrBk_1PUG)0urdL1Q_^)H!AH5BFlMV`M5;u*4Hc6q^HLXv>)hjwNlh` zZZwRw1Ud?Zj0A`@Vz6N`#kWR+#{|J4Nl3y)$OC{Wmk6(33L||cyZp9^Rr;Rmv#!$O zMWgNC4R|toI<+w~L6Z&lvEX`}KHnrNg9`;*gcJh_b0kUN2m_`Hn7WLN6Bwa*gdp;c zX9LA&%6ruA+wsMdhZl0zJp+Hab@{!>V;_N{Q#lij_bb0!vy62fW^!vqP^8U?644eF>tfhubJ0Qt}| zzGTn#)k;}+pZ(KBHM#ZFqR4IDz%>Y@MaQ1OMaa=%P#LmDKq~S;2IO8cA}ZprZ^0-- zKwst|z^%`}ihY)3tleVc)4LCMe?~^F@lO&rscX@aj5&B~74uA!H6%@wJXH0TG4Shv ze$PW%7dU$$N0B8E9)p+~A#sq+Wgu_tC`boWQ(+7z-2e6sJTU}0IH{()U?2IVm_AX@ z*ae>PNSPi$TM;-I7{1^I5n-MsNG<{YKB2hW0DppDAp?!1!PSpRUYDCb+DGV=_-u(- z$9cO_=kduaYTo{Lb=0M!qMK>RDs~6Xfu#;2k^{?-6>*FQ(-+JS@Ct$rBB78%kY%+i z>X<-oppukfs&%5;A_^2B0q9&VhHE$RKFE+wE!LikHh$OYX+5C~eP zAZv%mNJze*7*3KXaA~rTNCM6i2x?t^nVwCpFj?jxSSZ5e;%rm1E^AE^)W(e9??D*GdrzHO;`=jOE8 z6L($QI?uH9a@!7c)J3Htjxx%|Omk?}+*jVnk~)E(Knwi2DhZOe1E*hZtvQ?GWbBGr6j!2dpZIMcQ* zbKhOxbT__zW7XyF%Twdc3m)dAZHuGq%?VT1+}^V4`I%h3&##VqG*Y%Lg?@_J-L>YD zKg%P9m(?n@b3oL0o$6fFsb%946*okzYezL*+mig% zKjUwXKR+0~FuKzxea6s#MU2-y^$sw-b1zxjQq__F%4h%`R7xe|B1{+o^8TUt|n?*jD*1z zw%ETPwBgu9apA!q-~M>XmBRfWq9kq0{sD_EGZO#m*CE}}x9f_uF^^t~wq@ars`8Wy z^n#RropHbz4FMYMx}eXqth~`ECTLq4j@h%@{NxuqTmH3WLA|f{-+Ta8 z&eim6OYwSj(4q5czCE&J^^E`3JvZi|PR_PmE;wuBp3?8~o6@B=eSh-|h}=Dyr>WVN z-P1Pj|JA&3$AOW5)abC_sR0jTVz%YViOOG(+B&)S0Z;kY-b@`XKU`_q7TkrTKiM0t z`PZ%K^#(1lKOdRFVMu_igeb32$a<6;N!gZ{X48j{OxW{c%d0bHJ5smQdQ?)fE!%eP z_DZQ-&wl$NvG; zFnC&R~w~c?!%Wu6sl?N!-wU) z`{qtIre2A4Ds|n{ynTve#u|TfJJVgz&=FQ5<^w3C!Y51Z)hF(f|3SNu2Mzuq z*mstDy`Z#Kt)cp$##;W~)%K4;7b{grZD5a^Rd3}YZ+(0}8nTD5F;59GDhFoMk2Kcu z*ydS<8cn;bFnUh$SX^C#yXaMu1eMEQ3rjkRRF5bLTuZ+4MqOCLF1bIq`~ zLwD+uyu|pA4`JHnB8z#E$e~?nti|)@4<~P}9x^z7=ftxEPb@F(EoYg{^5g(){eQDY zAsU&+TDm;bxpuF=()u=fCu!JgW&4-%R(8#XtPnu*bhV89m>e65##%lZJN#2mlb5?c z_UvbKUfGhcDfDWE1uR4iBlnom7o*@#qp_A2>)&;c>w_(se{|L0A3mDaHj;j&>++KQ zyaWZeAC0vXY=zwZ==rZoZEUq*=b0LdE_(a4_%UA&Ao5ffQ2P=#4DKvy>-_2t)4&r> z+ha`mQmM0ght>r`kIqwZt)dX3S7@x|)!)iwHhh}+toezbn|xVtLEX^eI=k_Cs!pm% zv=8h)I?i2dRqt-5nu9i0|1xev(@LS$$xvhSeg!&(>PI?m(}ezedUYFl?AI>MhI}?H zrxge>9txZWU15!ikg`;L_vUA{rd6^nSmWwE`t9;>TRL>P+${0XayfX{_%c<9)N&{O`b`A4;@qRq3>=UsumdW)OP3ZK&_?qa=885eZ zUibzcE$r6(vk{j*9k%eFi-RhQt#Np?^RWikhP^Un(6j8}xr3KkkHDj=cjm5XaG}?; z-*jyBKK}fo3-G9|`|AmXt}uy(=TD!|=E>Rx;1U1ihdZkMd*G?MpO@~i`I}ciD)6OY z4aZBVk_&{ls2Rbn)wa4>{Q6I{`PKVAn`Vc*p$L&t7u4$}$>L_fz4hMj_PvgeEj6$( zzKCozW@LeB2JQ(G2P_zf@*la6vi4%1O&1EyiBP97!{JPKH-Iwmj$};`KnlJ2X0#FX zq@*d=hT$brGo^d5@nWTRrtxO0lAjd^*W+Re#6UaSozT;^*EINVJbV~7Sx%%fq>R*5 zH(ZLtg(R0JwKv7Nh1Ap@mg&W}UHzru{?6UFQU82jzUkAyz83T-X0hV8gbrt7vIOo+ zhuVYG4ZcGar8osAZ4RXF4JF8`QP?f0X{$Nrs3}xlnc~VZV5cRgLp>W6U>o(W9B$7jk;^er7aODG9s&p{^;xj+4plKQJ3*Ou1 zwN%Vk_kNE8*ZUGng{oiU)88AhW^a}8W8T?zq4pOc0mmtKsJs+A8rnFB zPM1|F;_Z+f!lHWu`0r9J~rS__>{XRk&qccF^$v(@h?CoY*e;eMZQM>j{ zTes?tp~Kb7*if?2)2md>a-Shyu2(jL&JQh}VtTUr-S#k(J~APYM<-`Nh3z9tE_I}& zdeMjIr$DKgr9Q6+uhoM#JzmCUFo`?jWdoFU2eJRx#Hc$ z%&}vwuO|Z)FIwRSuj0jrg4?kpvM#Z^KQP9@WvBFblNXHoQGaczm^nEj*-#m(e>X@3 zcmoQ)5aZMjEkMOfju~TM05ut%qJrO`jvX3XnepD|nNY4#+0JmVyJJ2w9?P5~xpk_l z8kt6VA40i$bJkbjQ)_MNO5HOD21-E2o4lhH4%V2ZCCFYi3J~+L0XplCI+MqYHvZ(2k|oY9#eAe` zCkpex-sc_3z}T_xW9Uavc|Bh81U)Y_OjU%HsKP5(0~%lGrwkN9hj`^Yq6G~YVZH%G8u;dLZ>CnVn!iJF+k-= z&+S~MM^QVJ>ygIVgjvfdBtZtO90^(*>)$#f+I*IkS)-E_W+|hP6lDR+k*2`Th+)ta zP|3({R5G$_1&Qq8RxVNYq?$hK+*P@lNZsIhD|1w4)ZlqAbKb}LH&6^QKNRxt_%!>6`MtYy`~ zoolDfAN6Nx64 zdiv)5t=G+XXF?70PxmhzL$8K*TzgT_K4z(ZS`8yFMPb%3x(7Hv(hj)SU8sGuSo2*I z-Wu7zD*2xIM6_1PkI~c*I){Z@!pKX8GT0C4Bj-95`QzCy0{HR&@O^?NA+MITmTIpW;>F38Qn9oFu8own_C{YR+86<>R zz{rmTWxOBm4YtpH?|pA_Md13?FS2XtVRpR^kv-h{Mfy13 z`bDvp=cb-3bfEUQ1=CIpx^=Vn^XJUDtw6&?cl|^Nw|r5^6Tz>1QHTTKT4WO)wjH39 z9n$FtvwBfTGW@F-g`neX?$Ti#&S4cV3Q2*ts6`0mZ{{xYdhH^Eox-eL6q1U-+C?FV z`6VjE7;gtV5=s}@)fl^C=>`F!CUtE*>!c|W50>py2YGfpMK}<@#etZp<$RKAxJP@ zt04?^MC>TE@d&4IkF5 zw{B53P<$tRO|_Yu?EE!*i}Gdf(y8-{K&kVH4yQnwshp(7>y_Vq?DBBS76lIXHCs#9 zTb#aa@43MZD^yQd+N;orNEGJ~e4^G#Hr}h2>+vf#WC+SIs}^Ms$d{DghNiY@w`tsf z=ayG5J7tXH_ekyAz!gx?_r*X*KHQ>3zC(S{ZCV=ZHf4Lls>e@Fri!)i@H(`Ci3-CQ zoNm0=_^cMGG59{rnnfXD_;UTIsb81sU*OER65CJApEG6OEvVTZ4I+&9>P3fi!5(hO zqJYu9;%0x^>0L8xv>rXNP}9a$*rj!%EEW}YOgn_atypB>>Q}KS*7DoZ)9XJvh_~8r z-THV5cI-ar0Ta=@dP@{31&i$7PX;e|iw)VOFbft1z<{~+!qB`T6+iy{O4|Y}hFBWw zooA!V5qiBMeU`&*JL{cgt!b?DydJKSp(mDw3j^)RV>y1usWjQ_Lemey?1ZfU!#g^R$o{h z;cy$v8VF|r@MYtm#9)DKhX?own*wO$iC=D!VSS`BiEZ*}W}gLDXXQ za9he|4FlPlT}#;r{c88rHNZUAN)&ljj&S?Q`l}pXIR2VPP(RsokxlPC=}=k1(3-++ zChJdv${K!pQ&2P62wdQP1&ZvUJIN|8WyPfvZYSAnz$hMOuUjXmlWc@%vtFObuK(29 z$O6JjBUs>>@Y=}wj|Mz*Xe0a6{d0&C#L{}m+KnEvcC{C*J=`9$-f=nhkiB&Os2Qr5 zPXk%6V&(HpDdPcWF?4>lhuc6lJ1}q_XWu~f0Vwb(MC5(7&oh+*we71t-0rd2fdMfH zX$+ct4(c8oDH|)biR}KZW9?e+SJ-ypwT=xKo^$KigZIxX{skK#Y~v5(5FEWtUkwDqZ5M0zj`YJWs9kIX=7k~AvXYy; z`#9^7F3+ILt3BLavDrWYah83r*prb>u3w{AyI+@A5W?Orul8^o#rlH)jJ+T<#tmu| zyXF4z_Dj^TtBIFNmspSr(CrnRa$$9e%^C;dAp0({yYC;p5Jsn=azm$A-MHYwYZ2=+ z(ATI4Y7u+<{?W`<8#C--T@wqpKdi4Nri1X)!h-t4Mk({laMc@AWqj6~!@_(k`Xuz! z2&*}4Hc(*lz3OC8bJz%YB)juXN&V6ew{SbdW*u%y4vx>s7StIwf@4@=W3PP^gGXj+rs8{5{MFvjdm7wP*2!LI)~a4*bw&7&Y-pgc7uK7^QSF=tzaWa z?aIyU>7YKa4|DFcC9nzX!<9L03G4tHrPDJoPVbHOK|y8S-oFD zZw~6)3I|j)v!*S9yIoM}s7h z3%Sykz-F%z9H)xHoLiaFLG=%FTSd-|JZVc{o7V`VIbmllrzL|XzIxjuRs(U!n2uF=hYjdDJ;C9s8SbOm9QvY{=3{aYi< z(Di0N=TR=SC9rvGba^4lnb1Lh;)2hVN}>bv^TzZ`7PVaF zpysL(9JmS#=WvQeE0g)YJFA{C`Ijh3aHdH1o={5LoyG~0J;`HLySP|WI9HMc{bb_x z!m4(){R?DMr|`}syClodV8)Rl8SzN%IgEnF16uK>P<>SM?B!ADn);9>1uiHfJ;g%ED{&&BGuLw5g+8LTP3rf~t9ue#nbmd56_ZN`;b^SR~e2Ax;@ zdZxy571W=Fp$(<<+T+8z?|XmshtHKLFtQcZA!eAjo4jdaur{g#TzahJ^`oDo6Lu`G z-)QsumrJiz0gf(d8l9#6>0kY6*uIVXo=kuLos5^tewi|4$N}}M6YH3!W}|iK;|{y_ z(+z4iiyb$vkF{s(&aW-MuJFk9hNf`=3XcB$cYgln`#s$^)SR^7!j7>|{WR=5_4|WM z0*#069y@sB{DNjT&VFki=-9C#W~gySo2F%_ap+Kwzdq=6#qnpmy)dfGS3L)QJb#A@ zb>Oq62{6$Abk|3mOzqO(#hDA<`?^rIDU~j4=y*jPF^@ETcO>kG=Ws4Ta1tPXgM5zW99I`erQ_cRTs}g^PWbj*S_p2w>A#zcWq;(@K7!=%lZU zwI$nD+57H`^9l`D!OSgh8W(WH=->Zt)#%1&j=x{1$jcQweEQ_NC#S35pYyzFjE{WR zzk8$5kL88GK4>|o*ZY;;ys_`^`RaF1KWQ2lAYk?HzxSgwzIETJ+itAw+rLqhYUm2} z`=_b_Nee3Nne%+j_#a9=k+fvi^*VFmkx;JXv-5i%woWNLomp9;ekph~`0}+s={Hg* z4Xd%D+iwd8%J8T+b7^nW-FMEW-`%{tS)Xxccr@$ey46pw{;b5^#U&>@rdOH=kA`By zzu0uad}GwS2GyIFT5&S$wzU@@OssmF)Y|nXwNUOTs7Y;>&;?BIw@K}ZXd|e%q|F(r z>{`;A&OaDB(12EP+!zgL?KwA~wfv1O=+udrIJysWU`oFwo$j3cVh8MzQ$Lyun4lZ& z1V^&bFgcK`k6s@SUks0{PpaPypR3QS-+!pNI=>mSX!CAOZ~y2AYQ}8(kDD=DCd{gS zE8*mV!r0|EH{4p=>iK$E~|4pWx368`J+L1yQhYl%2li1m zQ^dy0P8#F(=BZB_|PF1&XLMRd45E(Z_CuQS!^mRuPQ2(@IRQ5)`Q-!XRR zF`Wi}40}Bu0C9q+K>{2QQgS`*u_nG^E&Xr3&F?S!?UDwy%TIjc-v)cemkULiFby#+tIYanq8ZEm=@-= zOu-QTLvmVi8Imi2oaXd9kJYQ%@1^*#Vcbf(6)l7i$zp(RfrZ*?Ilz0lwd zzb91QJH)gkC%UqO6PPHL2c&67sJ%HSCvgqbK2FB@RPKLA-~NpFW&u6F(VZO=2GCx| zq@Q@u#@m9%Z_Inv(@E(p@&1IQ&sp;Gqyv@GPtBf2Ui)isqG@iZwb2O)7#)~Pk~b^j zaXFKdL4j!Kqlo}b`eS2(*45Zlze~|x&$A!gUTNx7_l{|GHgxIZY&n7Z4@8A8I`z`h zlJy{vL0*kFd1=kIVoN(EmTBL3W1G#PMMaK40`D#6!Rjfg3A#e#iyA1+>B*iN+5V+| zx+7VH(3k2|iM1eQCJ8lfzxZ#V5;ey&Wg9(iI%o>Dw(v4rwjjfN+1|Y0s5#D&F$Kwq zrFy^o{N~oe^il<0{e5mz)AIYC0S(mA|CGS?(CRqdZLEd%DPXa8H#|_Y#*ib!Z8y5? zo-)g^*7W_o7W z4$!39Afv$kR^N>K>Q7dXQIrktVYjEJ@raU6TU;gw+m)xEjA)WsLdA0h5u%|`QzhGbEdkbfCbPV1}yO_Hf(*RosU&(G3@Z#bJlmiG3^L*iNk%x ze_-qeFS3TLvC0*CRjM`xS(u15ukh2c#JG8(josi?_J#8If4|I{Yy@p0c(MKe6tLvv zlA>)-bxfQ4+?=b0Q`$^iV>%SplKaE-<1PO``MvHxaJdC8zdv>c6D(c5zgPMMa5T2~#A zKb5|K{zKwahl;wd+obc!4tGM9aoD%Sfs&Pa?mZFL=T`4m`v1V@OkB2r&fRxZSM+g5 z1O!H05EM#x36N~0X6|!7=6tV|WH4y^Q~T<6ed|d7n8bsf+K@|4qXWKTJ`zoIR5RK) z^T~2*cibBL?NxE_jktF4z#31jU!wUsf9fr##y7q*^a5S?ZRfFnS9E-DN6^g*({N|H zCoSCr`L2@b{)7GZSO*Bs1;oQFjh>iX``wJsziE4#U4Ffvv-X(pKl9=naY>4I(74m5eZZjsA7bZd9KlHf+Gk)Cg9$v&a+HLK9LGU_Mw&t} zR1$eg68y$O+ZUyaklLw;PrTaly`mq!SHAD3o9pen-pKqwW%os=@h14q>P(YTl}-|F z59fk#oE0isq+|%zbfqf=skDM&P>Dh$mX}yrq(~CM5gNsDTB2}*rx21tF`C0Dvj1iM1yTAzv%8qf0>FVs?V-JSh>}qj=Xi`I5D^xEV|hko5LRRhi1kNtlpLyeZm~U z4e0Rcpp*jhK!U)d0#8e#EHf0tkvt-xluWXefD0IjU^GfF7_MxT5OYB(`_e#vYLl~X zI)`jKiB~zdx^L5+b<7d8-3SML8RCVKa?=p*45p+wQ*}BRPM~04qbNdA2q(}ygCeBB z;S!Bww1i+d@S4PM0c#Nm%$K0;n+|`!y=_7!+jr}elM3AEXpW%ba5w}B4wsPb@bE5) zO9I!r##y3>u@ooB3`58m%?LOyViYFPBrOOaxg-L?Sd8Li|7Gz9<;z?vzA5zXa%ykI zI!$8s)-Uc}XFeUE6o#?&!y(2cc|hmWXkZDLaZ&>Lg~~JuoueoM1Q*2;z*IQKV4Q&B z5{h8{G^h}KQU7rLn@1+RRrJ=-)`Q|&R=V}P`C{&%&Zj3QDM>jx_PC7j1cQ?(#S0=z zgG3e(o|RY>7f4Z(5#TMJVP(l5djp6s0UL(=I(|Tf{udr6N|!11%_}#|QMz*-SeMl+ zbaST%Myw@B(mv4n5!RWgU2s+oMUEg5k)q%Lp?MVHKwyzLjo`epJ~Yje1cwl$EMoUQ z6qTdb=Tux;;IO^eS%-=DxL@3R_^Q(!XhMgvHL`S*oT>7PEQKPGfpZ-WKTHq^i9#hA zZ0!>ngNKh1pB7%StQVtYp@O-H`apb_j68NcG*HSB;>-!V=ojHOM@37I`J-PUJ zMYD(7@NpTqUH=R+7rJ%nbvD$Ef}wVvhT|I*aRNtq9zzgLrrIX#KD45S!C8vp(%n_8s4o!km#nA(S3rP0W+T9or ztchY01KgxGyrC(cU`doE1RPXdhD8X0!6b}fM2sOhlowHu%#cVJ8Van20!7nz>-t?(&p@#YYO0Sq$%K`Vg3$>3HYrbp3TO<2dCEtrg+sb~GWT;!pdHiNr zdt_Ccr0Xli6N;JzY88jeILn|Y#nB`PEQ)6tjK?remUxhC{_E`n$8W6~9CW_gp;ncp ztIA(J-{sX;x|$#U_BXMXBj-95`QzbLTOAc&$wiv-D0G%L!W@!}FeQ#ddY!Xh;5 zM}jim5BCP!=f3e|)yikD{QmLiHBFdK=D^g`Fc0zEYJwAMd2Z^tLI-M(TQKd!pj$V4 zKYuO~4B=o_bUW}Dc$`TR)S*c1=}{}%LF;4CXzUWlU<5AVpd?WO#^3}eQY`32j3hD~ zMsOS}ieM4JTG+hIQ_T_f576t?04sob=GIGNxUI=t zYCNuVNGBA_SrWkv04dKSn2ZS&*r0$dB8Zd>$Rjiub2uk+;EK?oXrq0IWWZ&L!mq&p^W0bz~FS{a9jo_nam?NL&|uF@oKkL)kzjVMvUEg z^Vc5Kkd&RKH;V5z2WCg#2g(ko_f_yK$Oa00emG5wBnx&gxQjr`M=?SIVSr*RD)KVN zQDEeApmAogiNmRg1sJOF$PD|8S6`XhF6DN;3rz|?^^IV@djGa#mTvF$7Q|(1dgVs% zsSt)&1TL=oA|qG2Plcf4QgoIh#Fo{4tLk0_CZ@oXLJBg=(2Ru1IEWq^b^t<>7*DeT z2ObHO$ky$sI2-lm@IfYU1m@NKtc2cc1?6NGVNJRFZQPNA$!2pnk0EHAPc zZTN##7p2!3pq5D2c0&;I5qPxj2YIWxtM9f3nMJv`q_a9XG+>!F8dW+kBeQsU9QIQyip{Cxr zL2Mi{#%Cp_pcmbXP9LpU%d_k1#K(RUw{?p-!#@4a+v6Y1h3Gv8;}na)uEq$g%CEYg z9himzJ%I)+|v>v1_JPSh$(VF)t{=DwSgd6t{#nB6yR}*zDvmCww`KBQ<_P<;G#t+A9-`#`o|;D5y6On+#F9j+#S?^YFaG?vK5PK*QiCKdhHw8 zF_yw|>@O5WV6^@JDdqoMa%P2*)P_As|X5F@yzULl9Y( zrIBC&zDLaY2^D7l`Bb4IYYX+AKWY6?G+K^(Lr5EMH$KBUq{@zXU2c=8Oo^-{gO*Dp zEJ+eL!hlr-lg`LEfk7+ha|1VB;OLYIGQs?FSF=damYlrMXiPMu%GyeT?? z-yk&@?v$o;koIE@)$5K0gNVcg5+ONQ0~F_I8CHRUU<#$kcAOU|9s!aKf)wc1&=7rB zsYSzf?Y|+X(=kN@;~q5hFLEJHC^FmdS~r?x_HvDZ+{M3xa`jwLWShA8kKBND_W zaB$;em>|h498m}%^G5VvQ)>Js$Cs4fhNiY@w`tsf=ayG5J7tXH_elKT;gO=RlR*VR zL9|gZWCe;71q^3!2yAmGj*>tE4~rwhjuMH`E90Y-{W^OtI%R0X4Mk7w-~G(X)WAqx z2!Y2y;9BT-KUfRGSWDB=Shp$L6IMNbYBE);xWFW4=}SX29@en41%^*vE@qt=4|V)-&yPZ#ZTS zO!yAN01v@1#(RO=JyNE}>P}BfbGkerm=q-k10M|!IRX$e;lYU@axBS!NP^4_aN<#L zEHDfW-WwxsrARejC?J>sigq~ca}BKi?#Fu?-b#HdRlYIqH}j*{1v1t$YUv6kPKo?idaLA=#|>(<9huw(Z{stw<>qfnH) z6vkQxj%ohWh~L_uxLL$jr@`kF5Od&tzc9CDmL3n%!vRvj>9TvDS+kxgh7KCn*pbR5 z+a)lkAr!;oA`Uta0x>j}$In{{NzskL|eR7##R zr`tHSlDnIb!s-=p*oNA81H;^sXYA{1@fh0oa6h%^S zz>|b5N}$-_A`AMNprk55;tm}D1P6+yFUD#J-51-Md*5x+st+1BepKN;$hjqJBZ<3Y zi*2sZBGaS0?LDv=V#Q7j7KFhlxz$NL^SJo#mlSy!IeIcyV_P?$tcUeDD`}d zwX9rPuEvL7l=|D|JT#)gnmz^1f!9~Ulp+Djlrc9^>-rdLDcfVwfji~X%)eve zNONE*ga^aalc_I13etdJAp?jZLAuGTgrSg5#&Hs_#F1DCK=Y7yiF;j$o=o3$Z(QZ{ z_Nru;%5w{xpS$O1ud?PN(eXZOId8fZW+cENag?1wF@a+ci6_BmW+0>mmM7?SA_nQA ztU!QlqaniLwPKCTHyEmkF=E-s#*5xr_gC9ajvCFYj=W>u9~r{>0jT9itN%hU3UXXM zin&aKmyL%#1W{sM0L@cCz*kOSqKHzEbxc6|F+rd#!-E||+WiKDA_{{wRBQe=yGzY< zQ|+fOHnKha^$Xo1i3BNf09IietS2Ru5aBpR zIRXb|4Z|=RgV>grXSFb)5w-IPJ-qSQC*?1d9pK*Lm|1u7-)rWX1FtWIvB}_=(DR(V z{#V^|sx`xcBNW1CkeLR7cR{8h#0d#@2rvVrIx;xmPce{VD-e*NhVdlhJJza=)n?m7 z7BRisZ|;bTi`pet`O7sX648}CuG$H#p~}kyPf{epbAlktN}4b1GDtq7Wteb?Du_7A zlLSNQsA`9=I#k1S#FRlz>X**wyzjlkzkffdOzTJ@gjtySCQ17zhPt34bh0DGp{PJG z#jxoJ267m|YXEA43|c%dg8zdPB}$@sj20NhCIll3G+NyImob(kSs}%=gR^`BxLc!PDF89gj{7A7dccE2!Aqs z;P{eJbKTo5R-gGR=K8EjpVVEr=}+^i`>tUA6k?hU1r=^;LHdxU_|q(GOF#t@@*x5!lPE}ixI-W- zyOw}RvJA0&2;gUhz?HGSxR!dN^r=sq-&o(S>9Rwy88;_HV#wxztCR`FW1$Bo+(*$6 z7UB40K&%ts>I1|8XHWn+mL@>H%SsS}$H7aCdR=^30DQ3?G^tC&H;#YOr9+YO?UyZ1 z`ZJPqGzY9zF{t4S2@tY?q8tgXcn)ryDVq*O;TP~niU@ePMOFag)~l?l0#F^Qouh@W zc3Dw+`0=)Vih92u&q+86b5}iIgZjLZ>=AJ$zP+Z+k z0eYR~Mpx=s%hoow0gJnj?6;F%FmzA9!9&b}rCY*K{uD;ICmkwFpo%*QBkT|*!%0Sl z94iX0AA$76Kv6~DmL3=~EQce&+X&opBkX#5hLplfq8d%K>L4`c`085H3$<#IuS`8Z zXdsHO{UlO>mb?9(V=XgwRw&wjSFz`cyI(r|>GD;CIk3u47_$sVLR@byGZXZ@CJs36 z91YqiCJZQii~#OhRj?8iY&HU?c(7V|j3QJ&E-t|41A9^Mj#CU6L%}C9MWf61M|q@%SXhX>whX@HqO;P&v1ofOY0?4$@N>8V$Ww@tC88u z99S$c95zmf>)~|AB|GE6vPy!6jp|SE8$`F$Go&=sE?kXysTno8#SWzF59MBoG;>6k zY=EfZFH{hR3%u&TkcJlRUeB?XCMC+P-fX+R|7zboeOA2s%rbLeX`wLYSUs7OWxz8r zJ=qsa5Wlz`?iYgmh5y6lB!NZ+iDG30{089p096P9cQa^fJO{b`K7&CERt9Tg_uHq} zT>dbj;k^At#;x9X^}Qz|;e#w|%Ih%*%-8}al9H+g=?OT)Kw1Uh$iTg40fS4Q5Q`Gw zzBd83tpG+9Xq$%9K+W~?$hK$PKrUOTkq*N_n7>Pkhh}IJlE-^UV z-4qzOBuvAlQ*B9A5TVdLGP6O!Dr2s^y|3_|=jc^muZ={=1)%Glu>h2L7QEzOy0LJ* znh+tYgB8J#%MjqogHRxeOJGJLIKc!>yx;n1EOceqt?w+@=j$fCu=3K?alf38E(pl_l_6K0qa|=iGdP&4C=GYqzzQM3>h~QBVF?(kG4QNY^&i{0 z>m^w_`Ah3(%64TVaReD!vmS0&O#I+Sy3UM1i4zv%DN5m7mIven$aRwl2)Yw+^NMGo zl!hovk_66+Y@h)807XMznucHJ&h28BRa`%?>8)K)m`~onJ5*6J5_zXv)#cPss`|;Q zD@riaBqV-nxdQ^^@dzl~6@#oi5FrGJ5)yG z9QJeC&Otl=n0mBxa{n#nE%%SNVn=zeLEBZm6Yiu+kSm}>tRy&6U`jbu#-Nx4CxGV` zV!|x=D<~E&mGLOlX`nf{_@U^l$~edm@CK{)t*E}f`udE06ZV|u_BSrLB9fpr@NZTo zgMWBmyY{AM2%wOKO0pcde?%H`AEAT~oaO|Fi8RjQ5V3+&kby`W8v=x1O2g*vCE^oj z_3b)1O>DKL&r^6L)>8;i5Y#0Ls@-EG9BJ_@f|TGaQA(%4-G0zW34vlHjz<_2XC%1M zAtDfK5#c=11E5CyO3V53iC?j6$2RKy-u20s_s@E6OwE65<@yx}mzz^EOXA?tp$KjEkrm*NVw1sG@v}hvmk1Wagqew4!hEz!JA48Ae2=wZvV_|cdO083%{?LR%w0O zbH!U6%Ke03SFbcE604+D!(9kQMoFHb1RD0GKy%<+1dR~nH(Z@%2~+}Y5n>jC*9YpQ zLQtWAU(uT&Gv3y7U_wf(T|XpMDgNcfrpIzCT$JVLck59v3dKoDrXlDF_pl&>t7zij z+r>q=m&Jg!!-8!igS7DvxA?&E4HxzPr{?mhQ?|@6c;tmw%3K}QCX)P3*vR)R{RBJ0 zot^?3fEF^6X%I0Y=z<6j;s%8)poFqJAnvFf|2Rd95Wj$@{xldsdpPIk0BL|r9}wiLR}XcgOgryK}rZsL$0h0wMOWm@vv86ymkhaJW}nkjJCVW z9IPY*`gW>sH)WMNyn}?@@PMb??1#YaMmeSx43X_b!7Z1S%o1*#D;=t6aDz z&-0GwedhPoqC-HzU{7J$BK4reLPeh=+)0&61=m~V*bPP%9)Cht=N27j%Ls1}d zK!}B?k={q4H|lE4l`3{;Hs2T-;0F7!Y5{eE573gS3T95=d1N;csf@ubl^9 z`a;_QuMhAfu(&w0^=(Z>P|n8i*}cWd`g$8D7F1NMSgDnpqLR4pVHdT{;AlYKPb_LX z_?z9JVt`i~4VTD}Rz}?ulAge1gjcMiU_nB!Xw)bl(GVZW_3Sw#!8;+BBE~3znUEh(EwRmdRBp+2X^&9HO#nsCBu7LJtWr zKsXAZ8LSyjR9MXEsac40aqJRFEiP*R`)AVb2VXZ_?Iaiw+m<79L%qqYHtkNG4Es;5 zL!q1+fghYpd_RVgM#5Bu-Y^>a6UClp_sB{f6DbR6`dk`4)w|rRk|*kXHK~98l(N}^ zo&5yB3X7?^clG>0p$k2T#`#eJ))XEEi%hjbh71b)41^RB%~j9{(!i00z;F)j z%3~sBAx)Egr@L=!fBVhg789Gz{dRg2+e>^tHA^9=>a_VW)aDu$xZwxBNHN<$#>Biy z-%TxCs$_|FH;b?7-ZXV+w$@YT;D*q@p8>|0c$vGpe*w3O`%-4StJ$Mwy|UQ?p-h_r zJAn|B+A(LogiyN+K#Yk&e#K5aI#@WgenK8H@%o_B4pgapiXUrXAh-a+nAkE|bh&Ad z_t%J|MdkZ6m?d|hXzsw9d$__xie2O^Mh{J*l1YlFAf)^8s_dBQuULq5=)$rb0 z;fdtPmjYc(M2%BDZo2SxiM1mp&pP&^_0L}xa52GbuF3v3Wl%q+#>~cLs}3miWx^H{ zCARq1=~STR_J;RnO?q4Q&6RAO7oT(j8lo631T7{mw;muJF7n)ZCUM$CX7PY^Z?ofC zWiFF3m1ww`zy*-SM9=H}R*kGtDPmBMi=vauqDnb{s0B@xQJ_YsQ?-fEI18|t==f~% zmHE{QB-h$F>i*`Qof|o}!82mr8fQ2yyyPsnVq#o$aNnPXmgqLK-8%Wl0bX8T8dxzw zj4b9oASw6LY#F9x8PP}N!T zykg(E&r15Oc$M_qQ5o?ORh01gx@@7I6t6F_C}Ulos#$ zk4)Xu$}KdqOo3ev^wijs!p=rDh7yGUqSN4r3DMCm`Rg<(QDni(|GnKosHY)6qQ*xPK{w`LTXE zt(1pIo%uZZP351?OL9f7em#CbiUTg6y$r`mveH%D?OS2paK}-}sI;~v)2MN2&tXnfd zyJKkSF9d*?*zL6>HfitftGYW)126A0-=e4OFb2Ruc zaeVu(bImsX9Xk40?BM}L&Q5f|4up&F`Jjgh|AP0AeEj*vxJ8}l*vipy^&C(Y_H-Q| zc`)g@bma}s03IfeZd&}POt4#nHdB^0h$~Xr!%2a6lTtP@Z~7U&gDYT%iQcvPcdUIo zbrt1#X4s{-PY3;XK!=HAp2LdPDV)2?+yiarGliN@a3Jt#Z)v47ysVIdP3D&qA%}^i z!auaH;Nf}t{+}Xc^S2e(I1s`9G(oJ3GEo2M48UQ6ZEz_d;qS#0M&~(vrd|1bwHyo! z81mv~(<@zdmT}~>2RBUAYIUH}>C~RzaZ=B~((*S-CyjMFePjX(HgfGGuwkO}LXReY z_w?DTUa@a(y_Cc*Uli0ZapY~UM&56lN#?c>tKRQOy$@d=&@fTs+^`!XB^Q&cM(0l8 z>bfO3*Id9Mz@s!99SvD_VTOr9Kd;x-iFjY8b5iK$s_*~))w%YrlR$=vtt0NX)}7MJ z77oh0cz=i8+Nk*#3NTDu z^-Sn?Hs7B;4pCRwjr+NJUjV!?QFTVq^P>`QZQ7;#&JB3rozry6%x5b4Y*UWSblh zEKDR0|KBFJnK$l_8O1bu-eJoE=W0AB!3q--SLGY#cCX;w`Q`4lc<=w=cn$>>Cfp}p z46c%g8<8?C`FQ=}7YpP-NMT~xz`cH_$zun~ZyLhQ3>2@-*?_{t*Y8LF7+I&{)un|7 z?24Xyph!-I6DAgp-sz*bw6$aEO-cR2-{|`~kip@-5`<1&0VYhiwGRc*dTBwIUI5ysaoL(BAmUzF`EyNb! zzs6Q%CxQqQy<1Z4wx_C}uDn;`Lb-cElU*c)1@x~`2q!`a6a50+{Fh&wQSe5)+m~DJ zOo{z60E7vK@|`|8@I{FaYi{kD7XSFkmkb|FBt3g0sv4BQCP*fq_ET)tI?xATYEMn; zjZJIV3=fZpF?32A3T*+<;5_hPqWp(9vzAOd_2|fQ(T1mOJE$CB-d;p3U6jV8+6`S- zXc$&gv=Vpw;LdY~f#L0U?R_cx6?hUuK%;RB=5$W9w&x6aAs!0vv6oS z12UKx)S`3kS*agyz0c>>sddvaC36B`FcG@?UDBJRKSoF&PUti{VWR4VW4Zi)j0eFq zbsbzVkw2#8?JliG7MQ=BDY@aoj1gZDSTIp#TF0gr?*5f~Vw378Pu}VL&OsICTBu+` zyKDHqYtwYAF7D|!ef_h%gL4W{FmZn9I$~b%UjsVC>ihTD968Ye-^6tGT0jDu4_}LC zM#wWB*%+O{xch;^%}XluKLhMaslff7fGJ{t)25?<8LdQnw@#T68Ju87pkrdyyz*R1 z3DwH50iBPZI^+ITHu$#y1U7%M#WN#l1mIG&3gDj0BrK3S0IZ6hVBC@hG$ml)16B-( z?F>FqODHLo5gDLH@QB%PHi;$^&zu&QUU;JQUx!!vtUdq6(dFw>h+v|iPaSFE@~ZvD z7XCJ_M!g3Ub1pzI@ih11182S&NFR)41Nz^J8R}qVhfyPc9(Z8l^sGbYZ;DqPzcBVm znBvX4hmYnQSOIAsYld}HNI_;B8kqRauU1In+QQWf zw4RbOtrFcP2LS^UWRxVfbYE%JzuwOA8NO0m&9R~wON?FD!U7Yc6J{??jY?`!Vn~(S z@%LWebD$^#^KQwo8Hn~@1_dS#r)*Fy=(mk=pWl>v-so<;1H}L~qPY$dm>5WQn%>7du zHsk9gC}3jcrqe?`ioW=%MXvw7E;M=b3kQ5njIPUpfQjFCZSUSXgjyWXvI0@0f9M4V zf;uLvI1S^*Yz?M8GTOvWg#jk&E>WdknR=#n>*w>9=)(`!{31YriK8cu|Guxy(|kQA zjh|NPtM#uAXWf&=(sd04FfnsRXrXc1`dz;-@LRWS>nm+?Ai0cnn^6ki1pvUr?HXNr z)DP5bIsSFevI&Xn+B@JE*@^0WHuztn+okvCZY2gz?@O<|p6}Svg05~na5nfa;di`8 zfe^nMuO2N;{@;LB1?4U;%5@3!FEMx4!514EKR&wVt0Gf%iQjf{QIaPuZtnu%U*fob zk-l9*QpT^S+{t(7mN|boP)UjH`)|Si65a0R8@~0_jv}`@JscTOKOy(&tgA*ld)XBK zb`Iz-;r8>bg~5G~&o8>A^gZUo=-UpcNS?{h2l-3fdERi#nh~+zG&)!HtxUeEnuFyH zE(iQ2Lf!IAp7Ul+avM$2(lJvfQl`fS>2kXB%j*tQ<3k-}1jAyX$~*-hl+!rs#>G6AXtUi>7+U7}Xag!Iz;~TT4Rvg1~(9nk)zt^33*?Ur* z&CNUat1w#<_ip_Ded2$Mm2{jCNB^ui;pUzdU5|OSetdr5xPx;dB6(q>EiU>EFo7Be zP%%N`{}Pb0JJD18h7OTln5MteI-=zI^J?)i#}0I(A8cF{#=lVAp!Oo=T3m83$FB2W6eZ#y=gp3eo*obcf_h$GVicRwS_h-P_k@m^KXG0S0YBMK)><6pPITNCT7PSB@0RLj`R4m>DdsNP1XA81G2WJn(A!AL zI}=j26#*_6%dR+MeIwl;_EhcIxM@On?&GR3(dx{^u|wiF8*SinF`DVa^&R4@CuP`2 z(W8bZ?)D9ud8<{tcS3?_Rc2D8r?B^P3*i@zH69p6g;~p?+OucZvAhRP>`yJ-^laPE zC!)2P$zhksuFow>OI&*&VJ*hmkdhDU?HZ@-)qnrV?fpis7p=@pjC2G(y9jM4+}UR> zLWiH{_})xj@#@%yT&K2&g#MVh2l50`K6j#2t15#fPtSld0WI=7%-u)jy)BEQvLeZ#Hp9?>tE$&vw? zkIu1g|M@I2cAdARxDcwb(1C6|Kk5vBaccLWua}QKdurjzKfPaymfI7<@Vd9{>$XR~ z%Vq=L9}^W3%HQDH#KORbB*w=^X^vm+ROQ@7 zhddKbc8vT@KPOSN)%4gQcHncJ{H9oo z<#K6nh`$)6I+8qOk9_0uke>={ue-i>a&OUkdr}$cSjlBc=d%kW9D2g+1jN#z`sg4w z$|%s>o%>5Y?J%)Kx6@$*`q4WMWG+xfbVR3JpvnlgS?zuKZXbF5Cic~s)ofk~(V^SZ z7QvUl-n6gQy@)z3cf{tJqJuWj^LL9!gg4PgAtn|P74LS%Im{R$Zo-AIY_!3}<(*x( zp_p>FAt5ntV|5nixZ6m|;M9s{X5gU;l-!8HZ~M{bi*8@vxK{CLUH@*JdhFrx!E?!@ z6-85QTtVaSx7W>^UaETQ!_<)iRnDb&`JNPh|FKv!+1lwc{<^g29lzP}`X}Xc=RWf> z`s(!Ik9hJgONyqZ^KFc8l<1+jzbmd&%J@U23;xpk;+)aK8zW1JCRw==#^27nW&6k` zg?=2}?zms$ZKn?Qo*?}8pCv_;tSolpZ?{za_?1uj0d-eIukMuZ7uAj=({Cr)TE@l~ z0?IyGvSRzy!LpFPhnG|=ynBZ5!k|*3WIN`k@tr*DD_w8%FeEn5h<(2Yl|4IhzVOcQ z@7&#p3O^TK{_0H1rH3c(ji|0^q;VTzFat$DS=xa{Ix=ai!v+4_S-9fa`Jv^ejqJQq zp!2FQ==4in)z@cUD*0^juv@na~ngq=j#p~N|&bYl!(}@ zSiW8$d$$VUCN97DZX~YMXp%0ATASsf}RDECj?>vR7CaH`1e*IEdZqrMN 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 delta 2378 zcmeHHX;4#F6z0AM0a7TBJeHtPQ0r*5LMVeR$dph(5=AsBOO>GnlpU&q5f}wU3vFqK z;E`Np7o>=_vc#8j2q=g`DWdj$RI8J`co@c9NWCI?}-X&5!a>x5;r z)Im$j+>-nznm<2jeP~Q_TtxJiu(0rTkx`MMj`5qLV#v3iG07~a$Ct8D)g-O}Zu^TyBQ?COuQAS#E{Kr@RJ9>G4kL)Up)V|cTVr3XOUw#PZzo` zR58y!$Z|^H#`s5@S?3*$`=TC7eye5%EcR}?`?vcMEB$e6Th7hJI9p7aL4(8;J)BY< zT*-ljE3MF37-))xMU56FG?PeoZk48XjW;C=k4<~tcpX*2##N@8AbRw2B9^;R`uMDn zvO$exstgy(C>}pbDwER8e(#~WCl4Oo)gNQ5otx|&LvlOczb`r9|1kOb%5nSbY&4+xvhusVpI0o#4GL3QZ| z9;YUyF7Zl1mrL^Z8L?T8dZ<>TuSlX89Zdm)FKdL906lo*XHBUnSaBhmxCVGoh%9(C zX}>Vs8U~D<~JuTh$LxbDnCvCl^uSsVR+tPhvof~0^FTQ`emy+z>H@Y#|ho) zYTNUiZnPXNQQ#F{AbZq5naBc;d^sIGpY*0-Yjg@uPDN3C;+i~MA>LxeP5nj9ao zQOY<)O?%(IxuE00Y=O;1+oH%8&RY<+kDxq{*N0P09LjYBy352^ZH}~I8#aYGIjeE= zdYVNqUc(gzNDuzX@rAY9S=0am90CoBAY2$mvtWnM!|5&-HWn`g=iERD?q*RZoXKyY$|lPhnz_HeM%dvjY-`%^TTnVjAp4zG}sp|f#QNxEKEWyx;-^xQp^{hN+fH0 zHif^X#Nw_tqyv(ol{hnrQ2VB+Q}q}Yufi*~BF%z#W7j>T=r?+}rDQd>-9ZSY>CO=R zvk`9CP9#Zp@rK`NZE)Ww!4Fc2cK@qAFah1trC`*@l84~69bw8i&|JD2XXTSiWnWf= z2X_(at}>;fkN|=m5*Xg^g$=(Uf|{m=Ky|qvoPNlnyAwJBOVXjNYD3uclQ&LDC#5o9!Qp&9uR;2B7s53>Cc!M)F z7cyFTbn~zR3R=b7OA|4bF)GIU79&1B>_TzCu#HEP0JiZ$AJZzrl(f0hrwEYkh33=s z1US>i$Je}&fG!zPV6F)H&~?OED?)R*PbL&|7okEdNTe)rr3kSU37!_A1#}rP9*WQ^ wI*S+{Vr0iH9?=wc$Pw+qEmE4raxvo3-;rp!7 diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock index 0ce4c9646ca85d214092028f7db63bee6e79e803..778ae703f930bcf78ae246bed6ada1f3858ddff5 100644 GIT binary patch literal 17 UcmZRsbes@(Px9DK1_RL07c2uXt^ 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 diff --git a/.gradle/8.10/fileHashes/fileHashes.lock b/.gradle/8.10/fileHashes/fileHashes.lock index 340e0dd0673653407cd5d6c667877cdda9e87606..4577896007bea6b094d682bc2b7981c78375319a 100644 GIT binary patch literal 17 VcmZS9`MA7`o8<*R0~j#c0st$`1C{^) literal 17 UcmZS9`MA7`o8<*R0|dAO04vo4p8x;= diff --git a/.gradle/8.10/fileHashes/resourceHashesCache.bin b/.gradle/8.10/fileHashes/resourceHashesCache.bin index 3d2189638c23437e4cea73219677f8823a00620a..4320628bdf822d3491500c210841da4ae2445301 100644 GIT binary patch literal 21217 zcmeI(do)yQ9|v$H22mQ7TS_ibLgkV|jml*(GiH#GOE+o|!%411j8HD69Ji=OqTJ^6 zMlT_^h}_WxA^& zNM$AB)F|L7iSRUp>^hP+4F=xd=N?f*efbofuLt~Nv15-_vt}vcR2SgCzmx0` zjW56>P80(E#p1PtbA^{M;^ecyf4q*8+0viHM4aXid~sk)cSq%F3)HUx=N8x4+Jh-UzH~!&ufmj@dw}v z?K351ZZ91WH+>I$^M|A5qkZMe(fJhM%Bxp|M^*F_5vPs;*J5gv#{QYfi+T%i?WwoL zw+b$HAx^vsTqiE&!Ch|=bU#xz0@n-EB;)C}=;vzyVqAWq^3ZhcqKni^k|gwB5k+~MF4U5$^70>q7_<~)!;A>B&e3H2J_$1aW$ zq|;dFx@2+SPT?()icuozX#NmzI=4ZBxMk-h#A()ZUSX1%%tJ))pGLQVGmgmJwO8EP zgyc;az&#BOE-D0k+lDxK0QhN>GsemB=g@sl+Ya2&7Mv zojFpViA}r;$A?8clDHM$6<%FGqvelHOtn%+!iFV9PwqXalBrk3#wKuenY{X`Lw>G1 z1=Okq;i3_tJOobZ9!`v8xfVIym`*>}<- zQ2(q;q~XeQoC@ZhZCc!g8^0Lsa=^*6?(D8V{sfyatq$_K8nEqt&a2|{6<1QJ*aQ!G z%k&k+{1vt2+~z?!_AoY~YDH*T#O!rf#c8cxaWc~nn;`Uy`0SHWXPNZ27~ST{)x{>R zdMOqA*)LKEtCha3@|Y;OK;p@-=h}Vkqh9yf7>Bb`7ns3z#$T<^Hqd-1xHnmB=c`cc zj3UOSH%}D#m9*5OH8XqN6R`=ngZ_EF{Co%fy!C%$2mSn$`!{xwQgIkRY((EQ%-__cmhU)2E4(?#!Ewa*+z1{9Z1dDr`bK?2p#Ux~17s9jzI;pH;`P ziKZ9(#h+z7g`Y{F84*>PFLSDm^3z@2E(ns?X)>518(T25zu?K(z~ zd#-gVHlfo^5h<%?9pWWfOWdilYr`g^Synfd&3H#B>CeXz{Pi zn@;lDbpQD{3A;+j0NJn3N|6Lgh+OL zWS^GBk30SNhq(tfA;dG18=d{)^;lL5P5RGuo3IIq1djuj%^JgbIxI%ry8_O$H}kHB zBCoYla&1z*e06lAt~P%yY+}tepP|mCE29cpK7AYF?!Un%JQWolCRVwms;#EdaysQ4 zv58L?weQU=df{3WmNn7!mc@BSWZv%)rDg59EXQ%8=^e7WwXr zZ=Q0nk8)lWxbZmWDWG|)1cyg=sN4G-&R1F@l`-yTg-v8Cj5H~cKYV_z9sBLbL%s#} zhnsnHWhm48H|F!b^b$EYf=cCkoeKbyS!^C*vud{ zyiX!%f&CFvL`z%Wt5$XFBY&a$xOFUcMuvvAS^tPEPeprH>9sxOOR$ORwjiASkuSKc z*Ks9=L2uS$6IU%(?%$@e)VM*BkiUD6Tnskh|NUk?Y43Hvi@YiO$q_XR+@+K@Y`ZGw zW7SQFo8F;(*z*l`1~D=6LY561uPU}9RPEMiI5uJ7@= diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 0350ff23745792ae6b0e29c9fdba156ee56cca00..06bcff59a652a195edd84d8b91bec88354f003ce 100644 GIT binary patch literal 17 VcmZQ(PG7Ze^~s_h1~6cB001+%1c?9u literal 17 UcmZQ(PG7Ze^~s_h1_&?)05hosdjJ3c diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 4ed6f06d6395816365de6075047efe060d9cdefb..695f9c2a60314a1331c210d89e786645e64cb8b8 100644 GIT binary patch literal 19847 zcmeI&YfMvT7{Kv5qop{CI*_YiWI72{VHMfJhy!OhJ*R~+ML=$01G-EWoTS5X*@lLp zf)~U|gbg9{0%Ontl_@t7TO8w(v4tTRVpB!{CkhVM5ihLdeV>|rBmF?w@|*-X`St00 z&ikkB7kG=uvt2n$58m^``uQoIVF4_F1+V}XzyeqR3t#~(fCaDs7Qg~n01IFNEPw^D z02aUk|Ca(GhK-n$&DgrXFn3~^!sB_oXgJ|A8`HZVhEA%j^!*f95)P%fP6nZPw#9T}NQ_ ziNG>dTE|v;-4Jkf^`L(1y9rUmC3fJNI!{-Xm*ZD--U7VS#ZK!K#?a18C3x2r-I1Xq ze`nL{I)MMOUoP`6Of4bK6oK~+ms|K>YT8KWw}by?Rd%e0<7yb?TJ)>9o9TeI81?#7PkDq^>FNhypM&`Ly;M30zuOGb-yqtOlxc=Kh8?9e; zK5?lW{8{oAPt|nbY3jq^Oa1vRY(&*A;%q)RKRBjAqTM|ARyG_GUvRVQZbf&SEDNbm zf(x9((#B`*D~WS1;4AEPt1HVBX;1bCaN(x5HJ2JS_GF#~p0&JbR%4OZ5lvkBD?DId zS~69vOMQd-C*Tf)UFUY!JUB!>2i$4@R99Wa`a5*}SHRbmG6A3Nev={2d4ap;S!71^ zDYSGx3S8u+O5fx4ofUDZr_p;{Iv?aFU7_*cKq_$y#5+G zUjV+RI(J{7yuFvsPlJc=T-(%`V?RQi8v~DUQxEJ7b(*2;1c65uGh*}TKj`NPdj~x3 zp#$4t=9xy<*$4iC(uVoQVI}P*i3LwoSJf9tr>Dp~n`!i}<1zQrl7s0wrQjcpHS}El zy?6z^A3pej4l`x*l-`-H(+>V|pCCJHKsip=2?tLvUnd<1lHVgP3kHXuJRaYUJci$~ z02aUkSO5!P0W5$8umBdo0$2bGU;!+E1+V}XzyeqR3t#~(fCaDs7Qg~n01IFNEPw^D z02aUkSO5!P0W7fi3y2N3IQI|jOXwF*_%E5`|GpSDVZtss9~& delta 113 zcmZpl%{X-m;|3E6L8+cNXOH||M^y$e@OV7=t%Sm4RY{4-XMtE$N@B7r5SIe+QmKl` zzS0tt?@Aj?7L_%a94jj^d9N%`Oip6*R3N@9C&BX{3K$re8aFEb5Z`DpLv*vF#}!5Z DJ_{*- diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index ac4beb46220d110a11f9e5f196fa452a079e920d..2037b03cd40a019ddb04bbbb0d6ea81296790f97 100644 GIT binary patch literal 8 PcmZQzV4NlVWQHvO2Xz8y literal 8 PcmZQzV4Nl3y6qtV25bU| diff --git a/.run/DistributionServiceApplication.run.xml b/.run/DistributionServiceApplication.run.xml new file mode 100644 index 0000000..664df2a --- /dev/null +++ b/.run/DistributionServiceApplication.run.xml @@ -0,0 +1,20 @@ + + + + diff --git a/claude/make-run-profile.md b/claude/make-run-profile.md new file mode 100644 index 0000000..f363a91 --- /dev/null +++ b/claude/make-run-profile.md @@ -0,0 +1,175 @@ +# 서비스실행파일작성가이드 + +[요청사항] +- <수행원칙>을 준용하여 수행 +- <수행순서>에 따라 수행 +- [결과파일] 안내에 따라 파일 작성 + +[가이드] +<수행원칙> +- 설정 Manifest(src/main/resources/application*.yml)의 각 항목의 값은 하드코딩하지 않고 환경변수 처리 +- Kubernetes에 배포된 데이터베이스는 LoadBalacer유형의 Service를 만들어 연결 +- MQ 이용 시 'MQ설치결과서'의 연결 정보를 실행 프로파일의 환경변수로 등록 +<수행순서> +- 준비: + - 데이터베이스설치결과서(develop/database/exec/db-exec-dev.md) 분석 + - 캐시설치결과서(develop/database/exec/cache-exec-dev.md) 분석 + - MQ설치결과서(develop/mq/mq-exec-dev.md) 분석 - 연결 정보 확인 + - kubectl get svc -n tripgen-dev | grep LoadBalancer 실행하여 External IP 목록 확인 +- 실행: + - 각 서비스별를 서브에이젼트로 병렬 수행 + - 설정 Manifest 수정 + - 하드코딩 되어 있는 값이 있으면 환경변수로 변환 + - 특히, 데이터베이스, MQ 등의 연결 정보는 반드시 환경변수로 변환해야 함 + - 민감한 정보의 디퐅트값은 생략하거나 간략한 값으로 지정 + - '<로그설정>'을 참조하여 Log 파일 설정 + - '<실행프로파일 작성 가이드>'에 따라 서비스 실행프로파일 작성 + - LoadBalancer External IP를 DB_HOST, REDIS_HOST로 설정 + - MQ 연결 정보를 application.yml의 환경변수명에 맞춰 설정 + - 서비스 실행 및 오류 수정 + - 'IntelliJ서비스실행기'를 'tools' 디렉토리에 다운로드 + - python 또는 python3 명령으로 백그라우드로 실행하고 결과 로그를 분석 + nohup python3 tools/run-intellij-service-profile.py {service-name} > logs/{service-name}.log 2>&1 & echo "Started {service-name} with PID: $!" + - 서비스 실행은 다른 방법 사용하지 말고 **반드시 python 프로그램 이용** + - 오류 수정 후 필요 시 실행파일의 환경변수를 올바르게 변경 + - 서비스 정상 시작 확인 후 서비스 중지 + - 결과: {service-name}/.run +<서비스 중지 방법> +- Window + - netstat -ano | findstr :{PORT} + - powershell "Stop-Process -Id {Process number} -Force" +- Linux/Mac + - netstat -ano | grep {PORT} + - kill -9 {Process number} +<로그설정> +- **application.yml 로그 파일 설정**: + ```yaml + logging: + file: + name: ${LOG_FILE:logs/trip-service.log} + logback: + rollingpolicy: + max-file-size: 10MB + max-history: 7 + total-size-cap: 100MB + ``` + +<실행프로파일 작성 가이드> +- {service-name}/.run/{service-name}.run.xml 파일로 작성 +- Spring Boot가 아니고 **Gradle 실행 프로파일**이어야 함: '[실행프로파일 예시]' 참조 +- Kubernetes에 배포된 데이터베이스의 LoadBalancer Service 확인: + - kubectl get svc -n {namespace} | grep LoadBalancer 명령으로 LoadBalancer IP 확인 + - 각 서비스별 데이터베이스의 LoadBalancer External IP를 DB_HOST로 사용 + - 캐시(Redis)의 LoadBalancer External IP를 REDIS_HOST로 사용 +- MQ 연결 설정: + - MQ설치결과서(develop/mq/mq-exec-dev.md)에서 연결 정보 확인 + - MQ 유형에 따른 연결 정보 설정 예시: + - RabbitMQ: RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USERNAME, RABBITMQ_PASSWORD + - Kafka: KAFKA_BOOTSTRAP_SERVERS, KAFKA_SECURITY_PROTOCOL + - Azure Service Bus: SERVICE_BUS_CONNECTION_STRING + - AWS SQS: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY + - Redis (Pub/Sub): REDIS_HOST, REDIS_PORT, REDIS_PASSWORD + - ActiveMQ: ACTIVEMQ_BROKER_URL, ACTIVEMQ_USER, ACTIVEMQ_PASSWORD + - 기타 MQ: 해당 MQ의 연결에 필요한 호스트, 포트, 인증정보, 연결문자열 등을 환경변수로 설정 + - application.yml에 정의된 환경변수명 확인 후 매핑 +- 백킹서비스 연결 정보 매핑: + - 데이터베이스설치결과서에서 각 서비스별 DB 인증 정보 확인 + - 캐시설치결과서에서 각 서비스별 Redis 인증 정보 확인 + - LoadBalancer의 External IP를 호스트로 사용 (내부 DNS 아님) +- 개발모드의 DDL_AUTO값은 update로 함 +- JWT Secret Key는 모든 서비스가 동일해야 함 +- application.yaml의 환경변수와 일치하도록 환경변수 설정 +- application.yaml의 민감 정보는 기본값으로 지정하지 않고 실제 백킹서비스 정보로 지정 +- 백킹서비스 연결 확인 결과를 바탕으로 정확한 값을 지정 +- 기존에 파일이 있으면 내용을 분석하여 항목 추가/수정/삭제 + +[실행프로파일 예시] +``` + + + + + + + + true + true + + + + + false + false + + + +``` + +[참고자료] +- 데이터베이스설치결과서: develop/database/exec/db-exec-dev.md + - 각 서비스별 DB 연결 정보 (사용자명, 비밀번호, DB명) + - LoadBalancer Service External IP 목록 +- 캐시설치결과서: develop/database/exec/cache-exec-dev.md + - 각 서비스별 Redis 연결 정보 + - LoadBalancer Service External IP 목록 +- MQ설치결과서: develop/mq/mq-exec-dev.md + - MQ 유형 및 연결 정보 + - 연결에 필요한 호스트, 포트, 인증 정보 + - LoadBalancer Service External IP (해당하는 경우) diff --git a/distribution-service/.run/distribution-service.run.xml b/distribution-service/.run/distribution-service.run.xml new file mode 100644 index 0000000..b3879c4 --- /dev/null +++ b/distribution-service/.run/distribution-service.run.xml @@ -0,0 +1,51 @@ + + + + + + + + true + true + + + + + false + false + + + diff --git a/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java b/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java new file mode 100644 index 0000000..2534d29 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java @@ -0,0 +1,23 @@ +package com.kt.distribution; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.kafka.annotation.EnableKafka; + +/** + * Distribution Service Application + * 다중 채널 배포 관리 서비스 + * + * @author System Architect + * @since 2025-10-23 + */ +@SpringBootApplication +@EnableKafka +@EnableFeignClients +public class DistributionApplication { + + public static void main(String[] args) { + SpringApplication.run(DistributionApplication.class, args); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java new file mode 100644 index 0000000..c0bebce --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java @@ -0,0 +1,86 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.DistributionRequest; +import io.github.resilience4j.bulkhead.annotation.Bulkhead; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import lombok.extern.slf4j.Slf4j; + +/** + * Abstract Channel Adapter + * 공통 로직 및 Resilience4j 적용 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +public abstract class AbstractChannelAdapter implements ChannelAdapter { + + /** + * 채널로 배포 실행 (Resilience4j 적용) + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + @Override + @CircuitBreaker(name = "channelApi", fallbackMethod = "fallback") + @Retry(name = "channelApi") + @Bulkhead(name = "channelApi") + public ChannelDistributionResult distribute(DistributionRequest request) { + long startTime = System.currentTimeMillis(); + + try { + log.info("Starting distribution to channel: {}, eventId: {}", + getChannelType(), request.getEventId()); + + // 실제 외부 API 호출 (구현체에서 구현) + ChannelDistributionResult result = executeDistribution(request); + result.setExecutionTimeMs(System.currentTimeMillis() - startTime); + + log.info("Distribution completed successfully: channel={}, eventId={}, executionTime={}ms", + getChannelType(), request.getEventId(), result.getExecutionTimeMs()); + + return result; + + } catch (Exception e) { + long executionTime = System.currentTimeMillis() - startTime; + log.error("Distribution failed: channel={}, eventId={}, error={}", + getChannelType(), request.getEventId(), e.getMessage(), e); + + return ChannelDistributionResult.builder() + .channel(getChannelType()) + .success(false) + .errorMessage(e.getMessage()) + .executionTimeMs(executionTime) + .build(); + } + } + + /** + * 실제 외부 API 호출 로직 (구현체에서 구현) + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + protected abstract ChannelDistributionResult executeDistribution(DistributionRequest request); + + /** + * Fallback 메서드 (Circuit Breaker Open 시) + * + * @param request DistributionRequest + * @param throwable Throwable + * @return ChannelDistributionResult + */ + protected ChannelDistributionResult fallback(DistributionRequest request, Throwable throwable) { + log.warn("Fallback triggered for channel: {}, eventId: {}, reason: {}", + getChannelType(), request.getEventId(), throwable.getMessage()); + + return ChannelDistributionResult.builder() + .channel(getChannelType()) + .success(false) + .errorMessage("Circuit Breaker Open: " + throwable.getMessage()) + .executionTimeMs(0) + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java new file mode 100644 index 0000000..bfedfc7 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java @@ -0,0 +1,30 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; + +/** + * Channel Adapter Interface + * 각 채널별 배포 API를 호출하는 인터페이스 + * + * @author System Architect + * @since 2025-10-23 + */ +public interface ChannelAdapter { + + /** + * 지원하는 채널 타입 + * + * @return ChannelType + */ + ChannelType getChannelType(); + + /** + * 채널로 배포 실행 + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + ChannelDistributionResult distribute(DistributionRequest request); +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java new file mode 100644 index 0000000..655d9a6 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * 지니TV Adapter + * 지니TV 광고 등록 API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class GiniTvAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.ginitv.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.GINITV; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling GiniTV API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "GTIV-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.GINITV) + .success(true) + .distributionId(distributionId) + .estimatedReach(10000) // TV 광고 노출 수 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java new file mode 100644 index 0000000..3b98443 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Instagram Adapter + * Instagram 포스팅 API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class InstagramAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.instagram.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.INSTAGRAM; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Instagram API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "INSTA-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.INSTAGRAM) + .success(true) + .distributionId(distributionId) + .estimatedReach(3000) // 팔로워 수 기반 예상 노출 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java new file mode 100644 index 0000000..68c7e06 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Kakao Channel Adapter + * Kakao Channel 포스팅 API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class KakaoAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.kakao.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.KAKAO; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Kakao API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "KAKAO-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.KAKAO) + .success(true) + .distributionId(distributionId) + .estimatedReach(4000) // 채널 친구 수 기반 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java new file mode 100644 index 0000000..0d7f44e --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Naver Blog Adapter + * Naver Blog 포스팅 API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class NaverAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.naver.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.NAVER; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Naver API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "NAVER-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.NAVER) + .success(true) + .distributionId(distributionId) + .estimatedReach(2000) // 블로그 방문자 수 기반 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java new file mode 100644 index 0000000..8ec0634 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * 링고비즈 Adapter + * 링고비즈 연결음 업데이트 API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class RingoBizAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.ringobiz.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.RINGOBIZ; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling RingoBiz API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "RBIZ-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.RINGOBIZ) + .success(true) + .distributionId(distributionId) + .estimatedReach(1000) // 연결음 사용자 수 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java new file mode 100644 index 0000000..41fa264 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java @@ -0,0 +1,72 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 우리동네TV Adapter + * 우리동네TV API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class UriDongNeTvAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.uridongnetv.url}") + private String apiUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public ChannelType getChannelType() { + return ChannelType.URIDONGNETV; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling UriDongNeTV API: url={}, eventId={}", apiUrl, request.getEventId()); + + // 외부 API 호출 준비 + Map payload = new HashMap<>(); + payload.put("eventId", request.getEventId()); + payload.put("title", request.getTitle()); + payload.put("videoUrl", request.getImageUrl()); // 이미지를 영상으로 변환한 URL + payload.put("radius", getChannelSetting(request, "radius", "500m")); + payload.put("timeSlot", getChannelSetting(request, "timeSlot", "evening")); + + // TODO: 실제 API 호출 (현재는 Mock) + // ResponseEntity response = restTemplate.postForEntity(apiUrl + "/distribute", payload, Map.class); + + // Mock 응답 + String distributionId = "UDTV-" + UUID.randomUUID().toString(); + int estimatedReach = 5000; + + return ChannelDistributionResult.builder() + .channel(ChannelType.URIDONGNETV) + .success(true) + .distributionId(distributionId) + .estimatedReach(estimatedReach) + .build(); + } + + private String getChannelSetting(DistributionRequest request, String key, String defaultValue) { + if (request.getChannelSettings() != null) { + Map settings = request.getChannelSettings().get(ChannelType.URIDONGNETV.name()); + if (settings != null && settings.containsKey(key)) { + return settings.get(key).toString(); + } + } + return defaultValue; + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java b/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java new file mode 100644 index 0000000..92f2c90 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java @@ -0,0 +1,46 @@ +package com.kt.distribution.config; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Kafka Configuration + * Kafka Producer 설정 + * + * @author System Architect + * @since 2025-10-23 + */ +@Configuration +@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true) +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + configProps.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java b/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java new file mode 100644 index 0000000..1b7c1d0 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java @@ -0,0 +1,32 @@ +package com.kt.distribution.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web Configuration + * CORS 설정 및 기타 웹 관련 설정 + * + * @author System Architect + * @since 2025-10-24 + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + /** + * CORS 설정 + * - 모든 origin 허용 (개발 환경) + * - 모든 HTTP 메서드 허용 + * - Credentials 허용 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java b/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java new file mode 100644 index 0000000..c17503f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java @@ -0,0 +1,71 @@ +package com.kt.distribution.controller; + +import com.kt.distribution.dto.DistributionRequest; +import com.kt.distribution.dto.DistributionResponse; +import com.kt.distribution.dto.DistributionStatusResponse; +import com.kt.distribution.service.DistributionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * Distribution Controller + * POST /api/distribution/distribute - 다중 채널 배포 실행 + * GET /api/distribution/{eventId}/status - 배포 상태 조회 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@RestController +@RequestMapping("/api/distribution") +@RequiredArgsConstructor +public class DistributionController { + + private final DistributionService distributionService; + + /** + * 다중 채널 배포 실행 + * UFR-DIST-010: 다중채널배포 + * + * @param request DistributionRequest + * @return DistributionResponse + */ + @PostMapping("/distribute") + public ResponseEntity distribute(@RequestBody DistributionRequest request) { + log.info("Received distribution request: eventId={}, channels={}", + request.getEventId(), request.getChannels()); + + DistributionResponse response = distributionService.distribute(request); + + log.info("Distribution request processed: eventId={}, success={}, successCount={}, failureCount={}", + response.getEventId(), response.isSuccess(), + response.getSuccessCount(), response.getFailureCount()); + + return ResponseEntity.ok(response); + } + + /** + * 배포 상태 조회 + * UFR-DIST-020: 배포상태조회 + * + * @param eventId 이벤트 ID + * @return DistributionStatusResponse + */ + @GetMapping("/{eventId}/status") + public ResponseEntity getDistributionStatus(@PathVariable String eventId) { + log.info("Received distribution status request: eventId={}", eventId); + + DistributionStatusResponse response = distributionService.getDistributionStatus(eventId); + + log.info("Distribution status retrieved: eventId={}, overallStatus={}", + eventId, response.getOverallStatus()); + + if ("NOT_FOUND".equals(response.getOverallStatus())) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(response); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java new file mode 100644 index 0000000..915cfa1 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java @@ -0,0 +1,49 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 채널별 배포 결과 + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelDistributionResult { + + /** + * 채널 타입 + */ + private ChannelType channel; + + /** + * 배포 성공 여부 + */ + private boolean success; + + /** + * 배포 ID (성공 시) + */ + private String distributionId; + + /** + * 예상 노출 수 (성공 시) + */ + private Integer estimatedReach; + + /** + * 에러 메시지 (실패 시) + */ + private String errorMessage; + + /** + * 배포 소요 시간 (ms) + */ + private long executionTimeMs; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java new file mode 100644 index 0000000..a65567d --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java @@ -0,0 +1,100 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 채널별 배포 상태 DTO + * + * 각 채널의 배포 진행 상태 및 결과 정보를 담습니다. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelStatus { + + /** + * 채널 타입 + */ + private ChannelType channel; + + /** + * 채널별 배포 상태 + * - PENDING: 대기 중 + * - IN_PROGRESS: 진행 중 + * - COMPLETED: 완료 + * - FAILED: 실패 + */ + private String status; + + /** + * 진행률 (0-100, IN_PROGRESS 상태일 때 사용) + */ + private Integer progress; + + /** + * 채널별 배포 ID (우리동네TV, 지니TV 등) + */ + private String distributionId; + + /** + * 예상 노출 수 (우리동네TV, 지니TV) + */ + private Integer estimatedViews; + + /** + * 업데이트 완료 시각 (링고비즈) + */ + private LocalDateTime updateTimestamp; + + /** + * 광고 ID (지니TV) + */ + private String adId; + + /** + * 노출 스케줄 (지니TV) + */ + private List impressionSchedule; + + /** + * 게시물 URL (Instagram, Naver Blog) + */ + private String postUrl; + + /** + * 게시물 ID (Instagram) + */ + private String postId; + + /** + * 메시지 ID (Kakao Channel) + */ + private String messageId; + + /** + * 완료 시각 + */ + private LocalDateTime completedAt; + + /** + * 오류 메시지 (실패 시) + */ + private String errorMessage; + + /** + * 재시도 횟수 + */ + private Integer retries; + + /** + * 마지막 재시도 시각 + */ + private LocalDateTime lastRetryAt; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java new file mode 100644 index 0000000..8a92b3b --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java @@ -0,0 +1,26 @@ +package com.kt.distribution.dto; + +/** + * 배포 채널 타입 + * + * @author System Architect + * @since 2025-10-23 + */ +public enum ChannelType { + URIDONGNETV("우리동네TV"), + RINGOBIZ("링고비즈"), + GINITV("지니TV"), + INSTAGRAM("Instagram"), + NAVER("Naver Blog"), + KAKAO("Kakao Channel"); + + private final String displayName; + + ChannelType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java new file mode 100644 index 0000000..fa1f68f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java @@ -0,0 +1,54 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * 배포 요청 DTO + * POST /api/distribution/distribute + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionRequest { + + /** + * 이벤트 ID + */ + private String eventId; + + /** + * 이벤트 제목 + */ + private String title; + + /** + * 이벤트 설명 + */ + private String description; + + /** + * 이미지 URL (CDN) + */ + private String imageUrl; + + /** + * 배포할 채널 목록 + */ + private List channels; + + /** + * 채널별 추가 설정 (Optional) + * 예: { "URIDONGNETV": { "radius": "1km", "timeSlot": "evening" } } + */ + private Map> channelSettings; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java new file mode 100644 index 0000000..9945d80 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java @@ -0,0 +1,63 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 배포 응답 DTO + * POST /api/distribution/distribute + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionResponse { + + /** + * 이벤트 ID + */ + private String eventId; + + /** + * 배포 성공 여부 (모든 채널 또는 일부 채널 성공) + */ + private boolean success; + + /** + * 채널별 배포 결과 + */ + private List channelResults; + + /** + * 성공한 채널 수 + */ + private int successCount; + + /** + * 실패한 채널 수 + */ + private int failureCount; + + /** + * 배포 완료 시각 + */ + private LocalDateTime completedAt; + + /** + * 전체 배포 소요 시간 (ms) + */ + private long totalExecutionTimeMs; + + /** + * 메시지 + */ + private String message; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java new file mode 100644 index 0000000..f65e964 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java @@ -0,0 +1,52 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 배포 상태 조회 응답 DTO + * + * 특정 이벤트의 전체 배포 상태 및 채널별 상세 상태 정보를 담습니다. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionStatusResponse { + + /** + * 이벤트 ID + */ + private String eventId; + + /** + * 전체 배포 상태 + * - PENDING: 대기 중 + * - IN_PROGRESS: 진행 중 + * - COMPLETED: 완료 + * - PARTIAL_FAILURE: 부분 성공 + * - FAILED: 실패 + * - NOT_FOUND: 배포 이력 없음 + */ + private String overallStatus; + + /** + * 배포 시작 시각 + */ + private LocalDateTime startedAt; + + /** + * 배포 완료 시각 + */ + private LocalDateTime completedAt; + + /** + * 채널별 배포 상태 목록 + */ + private List channels; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java b/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java new file mode 100644 index 0000000..467b6a6 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java @@ -0,0 +1,48 @@ +package com.kt.distribution.event; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Distribution Completed Event + * 배포 완료 시 Kafka로 발행하는 이벤트 + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionCompletedEvent { + + /** + * 이벤트 ID + */ + private String eventId; + + /** + * 배포 완료된 채널 목록 + */ + private List distributedChannels; + + /** + * 배포 완료 시각 + */ + private LocalDateTime completedAt; + + /** + * 성공한 채널 수 + */ + private int successCount; + + /** + * 실패한 채널 수 + */ + private int failureCount; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java b/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java new file mode 100644 index 0000000..aaa0b71 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java @@ -0,0 +1,65 @@ +package com.kt.distribution.repository; + +import com.kt.distribution.dto.DistributionStatusResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 배포 상태 저장소 + * + * 메모리 기반으로 배포 상태를 관리합니다. + * 실제 운영 환경에서는 Redis 또는 데이터베이스를 사용하여 영구 저장하는 것을 권장합니다. + */ +@Slf4j +@Repository +public class DistributionStatusRepository { + + /** + * 이벤트 ID를 키로 배포 상태를 저장하는 메모리 저장소 + */ + private final Map distributionStatuses = new ConcurrentHashMap<>(); + + /** + * 배포 상태 저장 + * + * @param eventId 이벤트 ID + * @param status 배포 상태 + */ + public void save(String eventId, DistributionStatusResponse status) { + log.debug("Saving distribution status: eventId={}, overallStatus={}", eventId, status.getOverallStatus()); + distributionStatuses.put(eventId, status); + } + + /** + * 배포 상태 조회 + * + * @param eventId 이벤트 ID + * @return 배포 상태 (없으면 Optional.empty()) + */ + public Optional findByEventId(String eventId) { + log.debug("Finding distribution status: eventId={}", eventId); + return Optional.ofNullable(distributionStatuses.get(eventId)); + } + + /** + * 배포 상태 삭제 + * + * @param eventId 이벤트 ID + */ + public void delete(String eventId) { + log.debug("Deleting distribution status: eventId={}", eventId); + distributionStatuses.remove(eventId); + } + + /** + * 모든 배포 상태 삭제 (테스트용) + */ + public void deleteAll() { + log.debug("Deleting all distribution statuses"); + distributionStatuses.clear(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java b/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java new file mode 100644 index 0000000..dac436f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java @@ -0,0 +1,261 @@ +package com.kt.distribution.service; + +import com.kt.distribution.adapter.ChannelAdapter; +import com.kt.distribution.dto.*; +import com.kt.distribution.event.DistributionCompletedEvent; +import com.kt.distribution.repository.DistributionStatusRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * Distribution Service + * 다중 채널 병렬 배포 및 Kafka Event 발행 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Service +public class DistributionService { + + private final List channelAdapters; + private final Optional kafkaEventPublisher; + private final DistributionStatusRepository statusRepository; + + @Autowired + public DistributionService(List channelAdapters, + Optional kafkaEventPublisher, + DistributionStatusRepository statusRepository) { + this.channelAdapters = channelAdapters; + this.kafkaEventPublisher = kafkaEventPublisher; + this.statusRepository = statusRepository; + } + + // 병렬 실행을 위한 ExecutorService (채널별 스레드 풀) + private final ExecutorService executorService = Executors.newFixedThreadPool(10); + + /** + * 다중 채널 병렬 배포 + * + * @param request DistributionRequest + * @return DistributionResponse + */ + public DistributionResponse distribute(DistributionRequest request) { + LocalDateTime startedAt = LocalDateTime.now(); + long startTime = System.currentTimeMillis(); + + log.info("Starting multi-channel distribution: eventId={}, channels={}", + request.getEventId(), request.getChannels()); + + // 배포 시작 상태 저장 (IN_PROGRESS) + saveInProgressStatus(request.getEventId(), request.getChannels(), startedAt); + + // 채널 어댑터 매핑 (타입별) + Map adapterMap = channelAdapters.stream() + .collect(Collectors.toMap( + adapter -> adapter.getChannelType().name(), + adapter -> adapter + )); + + // 병렬 배포 실행 + List> futures = request.getChannels().stream() + .map(channelType -> { + ChannelAdapter adapter = adapterMap.get(channelType.name()); + if (adapter == null) { + log.warn("No adapter found for channel: {}", channelType); + return CompletableFuture.completedFuture( + ChannelDistributionResult.builder() + .channel(channelType) + .success(false) + .errorMessage("Adapter not found") + .build() + ); + } + + // 비동기 실행 + return CompletableFuture.supplyAsync( + () -> adapter.distribute(request), + executorService + ); + }) + .collect(Collectors.toList()); + + // 모든 배포 완료 대기 + CompletableFuture allOf = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0]) + ); + + allOf.join(); // 블로킹 대기 (최대 1분 목표) + + // 결과 수집 + List results = futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + + long totalExecutionTime = System.currentTimeMillis() - startTime; + LocalDateTime completedAt = LocalDateTime.now(); + + // 성공/실패 카운트 + long successCount = results.stream().filter(ChannelDistributionResult::isSuccess).count(); + long failureCount = results.size() - successCount; + + log.info("Multi-channel distribution completed: eventId={}, successCount={}, failureCount={}, totalTime={}ms", + request.getEventId(), successCount, failureCount, totalExecutionTime); + + // 배포 완료 상태 저장 (COMPLETED/PARTIAL_FAILURE/FAILED) + saveCompletedStatus(request.getEventId(), results, startedAt, completedAt, successCount, failureCount); + + // Kafka Event 발행 + publishDistributionCompletedEvent(request.getEventId(), results); + + // 응답 생성 + return DistributionResponse.builder() + .eventId(request.getEventId()) + .success(successCount > 0) // 1개 이상 성공하면 성공으로 간주 + .channelResults(results) + .successCount((int) successCount) + .failureCount((int) failureCount) + .completedAt(completedAt) + .totalExecutionTimeMs(totalExecutionTime) + .message(String.format("Distribution completed: %d succeeded, %d failed", + successCount, failureCount)) + .build(); + } + + /** + * 배포 상태 조회 + * + * @param eventId 이벤트 ID + * @return 배포 상태 + */ + public DistributionStatusResponse getDistributionStatus(String eventId) { + return statusRepository.findByEventId(eventId) + .orElse(DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus("NOT_FOUND") + .channels(List.of()) + .build()); + } + + /** + * 배포 시작 상태 저장 (IN_PROGRESS) + * + * @param eventId 이벤트 ID + * @param channels 배포 채널 목록 + * @param startedAt 시작 시각 + */ + private void saveInProgressStatus(String eventId, List channels, LocalDateTime startedAt) { + List channelStatuses = channels.stream() + .map(channelType -> ChannelStatus.builder() + .channel(channelType) + .status("PENDING") + .build()) + .collect(Collectors.toList()); + + DistributionStatusResponse status = DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus("IN_PROGRESS") + .startedAt(startedAt) + .channels(channelStatuses) + .build(); + + statusRepository.save(eventId, status); + } + + /** + * 배포 완료 상태 저장 + * + * @param eventId 이벤트 ID + * @param results 배포 결과 + * @param startedAt 시작 시각 + * @param completedAt 완료 시각 + * @param successCount 성공 개수 + * @param failureCount 실패 개수 + */ + private void saveCompletedStatus(String eventId, List results, + LocalDateTime startedAt, LocalDateTime completedAt, + long successCount, long failureCount) { + // 전체 상태 결정 + String overallStatus; + if (successCount == 0) { + overallStatus = "FAILED"; + } else if (failureCount == 0) { + overallStatus = "COMPLETED"; + } else { + overallStatus = "PARTIAL_FAILURE"; + } + + // ChannelDistributionResult → ChannelStatus 변환 + List channelStatuses = results.stream() + .map(this::convertToChannelStatus) + .collect(Collectors.toList()); + + DistributionStatusResponse status = DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus(overallStatus) + .startedAt(startedAt) + .completedAt(completedAt) + .channels(channelStatuses) + .build(); + + statusRepository.save(eventId, status); + } + + /** + * ChannelDistributionResult를 ChannelStatus로 변환 + * + * @param result 배포 결과 + * @return 채널 상태 + */ + private ChannelStatus convertToChannelStatus(ChannelDistributionResult result) { + return ChannelStatus.builder() + .channel(result.getChannel()) + .status(result.isSuccess() ? "COMPLETED" : "FAILED") + .distributionId(result.getDistributionId()) + .estimatedViews(result.getEstimatedReach()) + .completedAt(LocalDateTime.now()) + .errorMessage(result.getErrorMessage()) + .build(); + } + + /** + * DistributionCompleted 이벤트 발행 + * + * @param eventId 이벤트 ID + * @param results 채널별 배포 결과 + */ + private void publishDistributionCompletedEvent(String eventId, List results) { + if (kafkaEventPublisher.isEmpty()) { + log.warn("KafkaEventPublisher not available - skipping event publishing"); + return; + } + + List distributedChannels = results.stream() + .filter(ChannelDistributionResult::isSuccess) + .map(result -> result.getChannel().name()) + .collect(Collectors.toList()); + + long successCount = results.stream().filter(ChannelDistributionResult::isSuccess).count(); + long failureCount = results.size() - successCount; + + DistributionCompletedEvent event = DistributionCompletedEvent.builder() + .eventId(eventId) + .distributedChannels(distributedChannels) + .completedAt(LocalDateTime.now()) + .successCount((int) successCount) + .failureCount((int) failureCount) + .build(); + + kafkaEventPublisher.get().publishDistributionCompleted(event); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java b/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java new file mode 100644 index 0000000..5e90b3c --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java @@ -0,0 +1,62 @@ +package com.kt.distribution.service; + +import com.kt.distribution.event.DistributionCompletedEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Service; + +import java.util.concurrent.CompletableFuture; + +/** + * Kafka Event Publisher + * DistributionCompleted 이벤트를 Kafka로 발행 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Service +@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +public class KafkaEventPublisher { + + private final KafkaTemplate kafkaTemplate; + + @Value("${kafka.topics.distribution-completed}") + private String distributionCompletedTopic; + + /** + * 배포 완료 이벤트 발행 + * + * @param event DistributionCompletedEvent + */ + public void publishDistributionCompleted(DistributionCompletedEvent event) { + try { + log.info("Publishing DistributionCompletedEvent: eventId={}, successCount={}, failureCount={}", + event.getEventId(), event.getSuccessCount(), event.getFailureCount()); + + CompletableFuture> future = + kafkaTemplate.send(distributionCompletedTopic, event.getEventId(), event); + + future.whenComplete((result, ex) -> { + if (ex == null) { + log.info("DistributionCompletedEvent published successfully: topic={}, partition={}, offset={}", + distributionCompletedTopic, + result.getRecordMetadata().partition(), + result.getRecordMetadata().offset()); + } else { + log.error("Failed to publish DistributionCompletedEvent: eventId={}, error={}", + event.getEventId(), ex.getMessage(), ex); + } + }); + + } catch (Exception e) { + log.error("Error publishing DistributionCompletedEvent: eventId={}, error={}", + event.getEventId(), e.getMessage(), e); + } + } +} diff --git a/distribution-service/src/main/resources/application.yml b/distribution-service/src/main/resources/application.yml new file mode 100644 index 0000000..5013aa7 --- /dev/null +++ b/distribution-service/src/main/resources/application.yml @@ -0,0 +1,102 @@ +server: + port: 8085 + +spring: + application: + name: distribution-service + + # Disable auto-configuration (No database required) + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration + + kafka: + enabled: ${KAFKA_ENABLED:true} + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:4.230.50.63:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + properties: + spring.json.type.mapping: distributionCompleted:com.kt.distribution.event.DistributionCompletedEvent + consumer: + group-id: ${KAFKA_CONSUMER_GROUP:distribution-service} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: '*' + +# Kafka Topics +kafka: + topics: + distribution-completed: distribution-completed + +# Resilience4j Configuration +resilience4j: + circuitbreaker: + instances: + channelApi: + failure-rate-threshold: 50 + slow-call-rate-threshold: 50 + slow-call-duration-threshold: 5000ms + wait-duration-in-open-state: 30s + permitted-number-of-calls-in-half-open-state: 3 + sliding-window-type: COUNT_BASED + sliding-window-size: 10 + minimum-number-of-calls: 5 + + retry: + instances: + channelApi: + max-attempts: 3 + wait-duration: 1s + exponential-backoff-multiplier: 2 + retry-exceptions: + - java.net.SocketTimeoutException + - java.net.ConnectException + - org.springframework.web.client.ResourceAccessException + + bulkhead: + instances: + channelApi: + max-concurrent-calls: 10 + max-wait-duration: 0ms + +# External Channel APIs (Mock URLs) +channel: + apis: + uridongnetv: + url: ${URIDONGNETV_API_URL:http://localhost:9001/api/uridongnetv} + timeout: 10000 + ringobiz: + url: ${RINGOBIZ_API_URL:http://localhost:9002/api/ringobiz} + timeout: 10000 + ginitv: + url: ${GINITV_API_URL:http://localhost:9003/api/ginitv} + timeout: 10000 + instagram: + url: ${INSTAGRAM_API_URL:http://localhost:9004/api/instagram} + timeout: 10000 + naver: + url: ${NAVER_API_URL:http://localhost:9005/api/naver} + timeout: 10000 + kakao: + url: ${KAKAO_API_URL:http://localhost:9006/api/kakao} + timeout: 10000 + +# Logging +logging: + file: + name: ${LOG_FILE:logs/distribution-service.log} + logback: + rollingpolicy: + max-file-size: 10MB + max-history: 7 + total-size-cap: 100MB + level: + com.kt.distribution: DEBUG + org.springframework.kafka: INFO + io.github.resilience4j: DEBUG diff --git a/distribution-service/src/main/resources/mock-events.json b/distribution-service/src/main/resources/mock-events.json new file mode 100644 index 0000000..8e086a8 --- /dev/null +++ b/distribution-service/src/main/resources/mock-events.json @@ -0,0 +1,134 @@ +[ + { + "eventId": "evt-test-001", + "title": "봄맞이 삼겹살 50% 할인 이벤트", + "description": "3월 한정 특별 이벤트! 삼겹살 1인분 무료 증정", + "imageUrl": "https://cdn.example.com/event-image-001.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }, + { + "eventId": "evt-test-002", + "title": "신규 고객 환영! 치킨 3,000원 할인", + "description": "처음 방문하시는 고객님께 특별 할인 쿠폰 제공. 기간 내 사용 가능", + "imageUrl": "https://cdn.example.com/event-image-002.jpg", + "channels": ["INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "INSTAGRAM": { + "hashtags": ["치킨", "할인", "신규고객"] + } + } + }, + { + "eventId": "evt-test-003", + "title": "주말 특가! 피자 1+1 이벤트", + "description": "토요일, 일요일 한정! 모든 피자 1+1 행사. 배달 주문 가능", + "imageUrl": "https://cdn.example.com/event-image-003.jpg", + "channels": ["URIDONGNETV", "KAKAO"], + "channelSettings": { + "URIDONGNETV": { + "radius": "2km", + "timeSlot": "lunch" + }, + "KAKAO": { + "targetAge": "20-40", + "sendTime": "11:00" + } + } + }, + { + "eventId": "evt-test-004", + "title": "여름 시즌 냉면 페스티벌", + "description": "시원한 냉면과 함께하는 여름! 전 메뉴 20% 할인", + "imageUrl": "https://cdn.example.com/event-image-004.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "500m", + "timeSlot": "afternoon" + } + } + }, + { + "eventId": "evt-test-005", + "title": "리뷰 작성 시 음료 무료!", + "description": "네이버 리뷰 작성하고 아메리카노 1잔 무료로 받아가세요", + "imageUrl": "https://cdn.example.com/event-image-005.jpg", + "channels": ["NAVER", "INSTAGRAM"], + "channelSettings": { + "NAVER": { + "reviewRequired": true + } + } + }, + { + "eventId": "evt-test-006", + "title": "생일 축하! 케이크 30% 할인", + "description": "생일 당일 방문 시 신분증 제시하면 케이크 30% 할인. 예약 필수", + "imageUrl": "https://cdn.example.com/event-image-006.jpg", + "channels": ["KAKAO", "INSTAGRAM"], + "channelSettings": { + "KAKAO": { + "reservationRequired": true, + "targetAge": "all" + } + } + }, + { + "eventId": "evt-test-007", + "title": "점심 시간 특가! 런치 세트 8,000원", + "description": "평일 11:30~14:00 런치 세트 메뉴 8,000원. 커피 포함", + "imageUrl": "https://cdn.example.com/event-image-007.jpg", + "channels": ["URIDONGNETV", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1.5km", + "timeSlot": "lunch" + } + } + }, + { + "eventId": "evt-test-008", + "title": "가족 나들이 패키지 20% 할인", + "description": "4인 가족 세트 메뉴 20% 할인! 키즈 메뉴 포함", + "imageUrl": "https://cdn.example.com/event-image-008.jpg", + "channels": ["KAKAO", "INSTAGRAM", "NAVER"], + "channelSettings": { + "KAKAO": { + "targetAge": "30-50", + "sendTime": "10:00" + } + } + }, + { + "eventId": "evt-test-009", + "title": "야간 할인! 저녁 9시 이후 전 메뉴 15% OFF", + "description": "저녁 9시 이후 방문 시 모든 메뉴 15% 할인. 포장 가능", + "imageUrl": "https://cdn.example.com/event-image-009.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }, + { + "eventId": "evt-test-010", + "title": "SNS 팔로우 이벤트! 디저트 무료", + "description": "인스타그램 팔로우 후 인증하면 디저트 1개 무료 제공", + "imageUrl": "https://cdn.example.com/event-image-010.jpg", + "channels": ["INSTAGRAM", "KAKAO"], + "channelSettings": { + "INSTAGRAM": { + "followRequired": true, + "hashtags": ["팔로우이벤트", "디저트무료", "맛집"] + } + } + } +] diff --git a/distribution-service/test-distribution.sh b/distribution-service/test-distribution.sh new file mode 100644 index 0000000..90d5f13 --- /dev/null +++ b/distribution-service/test-distribution.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Distribution Service API 테스트 스크립트 + +echo "=== Distribution Service API Test ===" +echo "" + +# 1. Health Check (추후 추가 예정) +# echo "1. Health Check..." +# curl -X GET http://localhost:8085/actuator/health +# echo "" + +# 2. 다중 채널 배포 테스트 +echo "1. Testing Multi-Channel Distribution..." +echo "" + +curl -X POST http://localhost:8085/api/distribution/distribute \ + -H "Content-Type: application/json" \ + -d '{ + "eventId": "evt-test-001", + "title": "봄맞이 삼겹살 50% 할인 이벤트", + "description": "3월 한정 특별 이벤트! 삼겹살 1인분 무료 증정", + "imageUrl": "https://cdn.example.com/event-image.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }' | jq '.' + +echo "" +echo "=== Test Completed ===" diff --git a/tools/run-intellij-service-profile.py b/tools/run-intellij-service-profile.py new file mode 100644 index 0000000..2278686 --- /dev/null +++ b/tools/run-intellij-service-profile.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Tripgen Service Runner Script +Reads execution profiles from {service-name}/.run/{service-name}.run.xml and runs services accordingly. + +Usage: + python run-config.py + +Examples: + python run-config.py user-service + python run-config.py location-service + python run-config.py trip-service + python run-config.py ai-service +""" + +import os +import sys +import subprocess +import xml.etree.ElementTree as ET +from pathlib import Path +import argparse + + +def get_project_root(): + """Find project root directory""" + current_dir = Path(__file__).parent.absolute() + while current_dir.parent != current_dir: + if (current_dir / 'gradlew').exists() or (current_dir / 'gradlew.bat').exists(): + return current_dir + current_dir = current_dir.parent + + # If gradlew not found, assume parent directory of develop as project root + return Path(__file__).parent.parent.absolute() + + +def parse_run_configurations(project_root, service_name=None): + """Parse run configuration files from .run directories""" + configurations = {} + + if service_name: + # Parse specific service configuration + run_config_path = project_root / service_name / '.run' / f'{service_name}.run.xml' + if run_config_path.exists(): + config = parse_single_run_config(run_config_path, service_name) + if config: + configurations[service_name] = config + else: + print(f"[ERROR] Cannot find run configuration: {run_config_path}") + else: + # Find all service directories + service_dirs = ['user-service', 'location-service', 'trip-service', 'ai-service'] + for service in service_dirs: + run_config_path = project_root / service / '.run' / f'{service}.run.xml' + if run_config_path.exists(): + config = parse_single_run_config(run_config_path, service) + if config: + configurations[service] = config + + return configurations + + +def parse_single_run_config(config_path, service_name): + """Parse a single run configuration file""" + try: + tree = ET.parse(config_path) + root = tree.getroot() + + # Find configuration element + config = root.find('.//configuration[@type="GradleRunConfiguration"]') + if config is None: + print(f"[WARNING] No Gradle configuration found in {config_path}") + return None + + # Extract environment variables + env_vars = {} + env_option = config.find('.//option[@name="env"]') + if env_option is not None: + env_map = env_option.find('map') + if env_map is not None: + for entry in env_map.findall('entry'): + key = entry.get('key') + value = entry.get('value') + if key and value: + env_vars[key] = value + + # Extract task names + task_names = [] + task_names_option = config.find('.//option[@name="taskNames"]') + if task_names_option is not None: + task_list = task_names_option.find('list') + if task_list is not None: + for option in task_list.findall('option'): + value = option.get('value') + if value: + task_names.append(value) + + if env_vars or task_names: + return { + 'env_vars': env_vars, + 'task_names': task_names, + 'config_path': str(config_path) + } + + return None + + except ET.ParseError as e: + print(f"[ERROR] XML parsing error in {config_path}: {e}") + return None + except Exception as e: + print(f"[ERROR] Error reading {config_path}: {e}") + return None + + +def get_gradle_command(project_root): + """Return appropriate Gradle command for OS""" + if os.name == 'nt': # Windows + gradle_bat = project_root / 'gradlew.bat' + if gradle_bat.exists(): + return str(gradle_bat) + return 'gradle.bat' + else: # Unix-like (Linux, macOS) + gradle_sh = project_root / 'gradlew' + if gradle_sh.exists(): + return str(gradle_sh) + return 'gradle' + + +def run_service(service_name, config, project_root): + """Run service""" + print(f"[START] Starting {service_name} service...") + + # Set environment variables + env = os.environ.copy() + for key, value in config['env_vars'].items(): + env[key] = value + print(f" [ENV] {key}={value}") + + # Prepare Gradle command + gradle_cmd = get_gradle_command(project_root) + + # Execute tasks + for task_name in config['task_names']: + print(f"\n[RUN] Executing: {task_name}") + + cmd = [gradle_cmd, task_name] + + try: + # Execute from project root directory + process = subprocess.Popen( + cmd, + cwd=project_root, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1, + encoding='utf-8', + errors='replace' + ) + + print(f"[CMD] Command: {' '.join(cmd)}") + print(f"[DIR] Working directory: {project_root}") + print("=" * 50) + + # Real-time output + for line in process.stdout: + print(line.rstrip()) + + # Wait for process completion + process.wait() + + if process.returncode == 0: + print(f"\n[SUCCESS] {task_name} execution completed") + else: + print(f"\n[FAILED] {task_name} execution failed (exit code: {process.returncode})") + return False + + except KeyboardInterrupt: + print(f"\n[STOP] Interrupted by user") + process.terminate() + return False + except Exception as e: + print(f"\n[ERROR] Execution error: {e}") + return False + + return True + + +def list_available_services(configurations): + """List available services""" + print("[LIST] Available services:") + print("=" * 40) + + for service_name, config in configurations.items(): + if config['task_names']: + print(f" [SERVICE] {service_name}") + if 'config_path' in config: + print(f" +-- Config: {config['config_path']}") + for task in config['task_names']: + print(f" +-- Task: {task}") + print(f" +-- {len(config['env_vars'])} environment variables") + print() + + +def main(): + """Main function""" + parser = argparse.ArgumentParser( + description='Tripgen Service Runner Script', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python run-config.py user-service + python run-config.py location-service + python run-config.py trip-service + python run-config.py ai-service + python run-config.py --list + """ + ) + + parser.add_argument( + 'service_name', + nargs='?', + help='Service name to run' + ) + + parser.add_argument( + '--list', '-l', + action='store_true', + help='List available services' + ) + + args = parser.parse_args() + + # Find project root + project_root = get_project_root() + print(f"[INFO] Project root: {project_root}") + + # Parse run configurations + print("[INFO] Reading run configuration files...") + configurations = parse_run_configurations(project_root) + + if not configurations: + print("[ERROR] No execution configurations found") + return 1 + + print(f"[INFO] Found {len(configurations)} execution configurations") + + # List services request + if args.list: + list_available_services(configurations) + return 0 + + # If service name not provided + if not args.service_name: + print("\n[ERROR] Please provide service name") + list_available_services(configurations) + print("Usage: python run-config.py ") + return 1 + + # Find service + service_name = args.service_name + + # Try to parse specific service configuration if not found + if service_name not in configurations: + print(f"[INFO] Trying to find configuration for '{service_name}'...") + configurations = parse_run_configurations(project_root, service_name) + + if service_name not in configurations: + print(f"[ERROR] Cannot find '{service_name}' service") + list_available_services(configurations) + return 1 + + config = configurations[service_name] + + if not config['task_names']: + print(f"[ERROR] No executable tasks found for '{service_name}' service") + return 1 + + # Execute service + print(f"\n[TARGET] Starting '{service_name}' service execution") + print("=" * 50) + + success = run_service(service_name, config, project_root) + + if success: + print(f"\n[COMPLETE] '{service_name}' service started successfully!") + return 0 + else: + print(f"\n[FAILED] Failed to start '{service_name}' service") + return 1 + + +if __name__ == '__main__': + try: + exit_code = main() + sys.exit(exit_code) + except KeyboardInterrupt: + print("\n[STOP] Interrupted by user") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] Unexpected error occurred: {e}") + sys.exit(1) \ No newline at end of file