From 23b5eb86605da0b22884811fc1f4f592508aed14 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 4 Sep 2023 17:14:54 +0900 Subject: [PATCH 001/111] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85=20=EC=99=84=EB=A3=8C=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 37 +++ build.gradle | 49 ++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 92 +++++++ settings.gradle | 1 + .../server/LivableServerApplication.java | 13 + src/main/resources/application.properties | 1 + .../server/LivableServerApplicationTests.java | 13 + 10 files changed, 461 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/livable/server/LivableServerApplication.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/livable/server/LivableServerApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..1f970eaf --- /dev/null +++ b/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.15' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +group = 'com.livable' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '11' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + set('snippetsDir', file("build/generated-snippets")) +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' +} + +tasks.named('test') { + outputs.dir snippetsDir + useJUnitPlatform() +} + +tasks.named('asciidoctor') { + inputs.dir snippetsDir + dependsOn test +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..033e24c4cdf41af1ab109bc7f253b2b887023340 GIT binary patch literal 63375 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfhMpqVf>AF&}ZQHhOJ14Bz zww+XL+qP}nww+W`F>b!by|=&a(cM4JIDhsTXY8@|ntQG}-}jm0&Bcj|LV(#sc=BNS zRjh;k9l>EdAFdd)=H!U`~$WP*}~^3HZ_?H>gKw>NBa;tA8M1{>St|)yDF_=~{KEPAGkg3VB`QCHol!AQ0|?e^W?81f{@()Wy!vQ$bY; z0ctx)l7VK83d6;dp!s{Nu=SwXZ8lHQHC*J2g@P0a={B8qHdv(+O3wV=4-t4HK1+smO#=S; z3cSI#Nh+N@AqM#6wPqjDmQM|x95JG|l1#sAU|>I6NdF*G@bD?1t|ytHlkKD+z9}#j zbU+x_cR-j9yX4s{_y>@zk*ElG1yS({BInGJcIT>l4N-DUs6fufF#GlF2lVUNOAhJT zGZThq54GhwCG(h4?yWR&Ax8hU<*U)?g+HY5-@{#ls5CVV(Wc>Bavs|l<}U|hZn z_%m+5i_gaakS*Pk7!v&w3&?R5Xb|AkCdytTY;r+Z7f#Id=q+W8cn)*9tEet=OG+Y} z58U&!%t9gYMx2N=8F?gZhIjtkH!`E*XrVJ?$2rRxLhV1z82QX~PZi8^N5z6~f-MUE zLKxnNoPc-SGl7{|Oh?ZM$jq67sSa)Wr&3)0YxlJt(vKf!-^L)a|HaPv*IYXb;QmWx zsqM>qY;tpK3RH-omtta+Xf2Qeu^$VKRq7`e$N-UCe1_2|1F{L3&}M0XbJ@^xRe&>P zRdKTgD6601x#fkDWkoYzRkxbn#*>${dX+UQ;FbGnTE-+kBJ9KPn)501#_L4O_k`P3 zm+$jI{|EC?8BXJY{P~^f-{**E53k%kVO$%p+=H5DiIdwMmUo>2euq0UzU90FWL!>; z{5@sd0ecqo5j!6AH@g6Mf3keTP$PFztq}@)^ZjK;H6Go$#SV2|2bAFI0%?aXgVH$t zb4Kl`$Xh8qLrMbZUS<2*7^F0^?lrOE=$DHW+O zvLdczsu0^TlA6RhDy3=@s!k^1D~Awulk!Iyo#}W$xq8{yTAK!CLl={H0@YGhg-g~+ z(u>pss4k#%8{J%~%8=H5!T`rqK6w^es-cNVE}=*lP^`i&K4R=peg1tdmT~UAbDKc& zg%Y*1E{hBf<)xO>HDWV7BaMWX6FW4ou1T2m^6{Jb!Su1UaCCYY8RR8hAV$7ho|FyEyP~ zEgK`@%a$-C2`p zV*~G>GOAs*3KN;~IY_UR$ISJxB(N~K>=2C2V6>xTmuX4klRXdrJd&UPAw7&|KEwF8Zcy2j-*({gSNR1^p02Oj88GN9a_Hq;Skdp}kO0;FLbje%2ZvPiltDZgv^ z#pb4&m^!79;O8F+Wr9X71laPY!CdNXG?J6C9KvdAE2xWW1>U~3;0v≫L+crb^Bz zc+Nw%zgpZ6>!A3%lau!Pw6`Y#WPVBtAfKSsqwYDWQK-~ zz(mx=nJ6-8t`YXB{6gaZ%G}Dmn&o500Y}2Rd?e&@=hBEmB1C=$OMBfxX__2c2O4K2#(0ksclP$SHp*8jq-1&(<6(#=6&H`Nlc2RVC4->r6U}sTY<1? zn@tv7XwUs-c>Lcmrm5AE0jHI5={WgHIow6cX=UK)>602(=arbuAPZ37;{HTJSIO%9EL`Et5%J7$u_NaC(55x zH^qX^H}*RPDx)^c46x>js=%&?y?=iFs^#_rUl@*MgLD92E5y4B7#EDe9yyn*f-|pQ zi>(!bIg6zY5fLSn@;$*sN|D2A{}we*7+2(4&EhUV%Qqo5=uuN^xt_hll7=`*mJq6s zCWUB|s$)AuS&=)T&_$w>QXHqCWB&ndQ$y4-9fezybZb0bYD^zeuZ>WZF{rc>c4s`` zgKdppTB|o>L1I1hAbnW%H%EkFt%yWC|0~+o7mIyFCTyb?@*Ho)eu(x`PuO8pLikN> z6YeI`V?AUWD(~3=8>}a6nZTu~#QCK(H0+4!ql3yS`>JX;j4+YkeG$ZTm33~PLa3L} zksw7@%e-mBM*cGfz$tS4LC^SYVdBLsR}nAprwg8h2~+Cv*W0%izK+WPVK}^SsL5R_ zpA}~G?VNhJhqx2he2;2$>7>DUB$wN9_-adL@TqVLe=*F8Vsw-yho@#mTD6*2WAr6B zjtLUh`E(;#p0-&$FVw(r$hn+5^Z~9J0}k;j$jL1;?2GN9s?}LASm?*Rvo@?E+(}F& z+=&M-n`5EIz%%F^e)nnWjkQUdG|W^~O|YeY4Fz}>qH2juEere}vN$oJN~9_Th^&b{ z%IBbET*E8%C@jLTxV~h#mxoRrJCF{!CJOghjuKOyl_!Jr?@4Upo7u>fTGtfm|CH2v z&9F+>;6aFbYXLj3{yZ~Yn1J2%!)A3~j2$`jOy{XavW@t)g}}KUVjCWG0OUc7aBc=2 zR3^u=dT47=5SmT{K1aGaVZkOx|24T-J0O$b9dfB25J|7yb6frwS6wZ1^y%EWOm}S< zc1SdYhfsdLG*FB-;!QLV3D!d~hnXTGVQVck9x%=B(Kk8c3y%f0nR95_TbY;l=obSl zEE@fp0|8Q$b3(+DXh?d0FEloGhO0#11CLQT5qtEckBLe-VN-I>9ys}PVK0r;0!jIG zH_q$;a`3Xv9P_V2ekV1SMzd#SKo<1~Dq2?M{(V;AwhH_2x@mN$=|=cG0<3o^j_0OF z7|WJ-f2G=7sA4NVGU2X5`o*D2T7(MbmZ2(oipooE{R?9!{WxX!%ofhsrPAxoIk!Kr z>I$a{Zq=%KaLrDCIL^gmA3z{2z%Wkr)b$QHcNUA^QwydWMJmxymO0QS22?mo%4(Md zgME(zE}ub--3*wGjV`3eBMCQG-@Gel1NKZDGuqobN|mAt0{@ZC9goI|BSmGBTUZ(`Xt z^e2LiMg?6E?G*yw(~K8lO(c4)RY7UWxrXzW^iCg-P41dUiE(i+gDmmAoB?XOB}+Ln z_}rApiR$sqNaT4frw69Wh4W?v(27IlK$Toy<1o)GeF+sGzYVeJ`F)3`&2WDi^_v67 zg;@ehwl3=t+}(DJtOYO!s`jHyo-}t@X|U*9^sIfaZfh;YLqEFmZ^E;$_XK}%eq;>0 zl?+}*kh)5jGA}3daJ*v1knbW0GusR1+_xD`MFPZc3qqYMXd>6*5?%O5pC7UVs!E-` zuMHc6igdeFQ`plm+3HhP)+3I&?5bt|V8;#1epCsKnz0%7m9AyBmz06r90n~9o;K30 z=fo|*`Qq%dG#23bVV9Jar*zRcV~6fat9_w;x-quAwv@BkX0{9e@y0NB(>l3#>82H6 z^US2<`=M@6zX=Pz>kb8Yt4wmeEo%TZ=?h+KP2e3U9?^Nm+OTx5+mVGDvgFee%}~~M zK+uHmj44TVs}!A}0W-A92LWE%2=wIma(>jYx;eVB*%a>^WqC7IVN9{o?iw{e4c=CG zC#i=cRJZ#v3 zF^9V+7u?W=xCY%2dvV_0dCP%5)SH*Xm|c#rXhwEl*^{Ar{NVoK*H6f5qCSy`+|85e zjGaKqB)p7zKNKI)iWe6A9qkl=rTjs@W1Crh(3G57qdT0w2ig^{*xerzm&U>YY{+fZbkQ#;^<$JniUifmAuEd^_M(&?sTrd(a*cD! zF*;`m80MrZ^> zaF{}rDhEFLeH#`~rM`o903FLO?qw#_Wyb5}13|0agjSTVkSI6Uls)xAFZifu@N~PM zQ%o?$k)jbY0u|45WTLAirUg3Zi1E&=G#LnSa89F3t3>R?RPcmkF}EL-R!OF_r1ZN` z?x-uHH+4FEy>KrOD-$KHg3$-Xl{Cf0;UD4*@eb~G{CK-DXe3xpEEls?SCj^p z$Uix(-j|9f^{z0iUKXcZQen}*`Vhqq$T?^)Ab2i|joV;V-qw5reCqbh(8N)c%!aB< zVs+l#_)*qH_iSZ_32E~}>=wUO$G_~k0h@ch`a6Wa zsk;<)^y=)cPpHt@%~bwLBy;>TNrTf50BAHUOtt#9JRq1ro{w80^sm-~fT>a$QC;<| zZIN%&Uq>8`Js_E((_1sewXz3VlX|-n8XCfScO`eL|H&2|BPZhDn}UAf_6s}|!XpmUr90v|nCutzMjb9|&}#Y7fj_)$alC zM~~D6!dYxhQof{R;-Vp>XCh1AL@d-+)KOI&5uKupy8PryjMhTpCZnSIQ9^Aq+7=Mb zCYCRvm4;H=Q8nZWkiWdGspC_Wvggg|7N`iED~Eap)Th$~wsxc(>(KI>{i#-~Dd8iQ zzonqc9DW1w4a*}k`;rxykUk+~N)|*I?@0901R`xy zN{20p@Ls<%`1G1Bx87Vm6Z#CA`QR(x@t8Wc?tpaunyV^A*-9K9@P>hAWW9Ev)E$gb z<(t?Te6GcJX2&0% z403pe>e)>m-^qlJU^kYIH)AutgOnq!J>FoMXhA-aEx-((7|(*snUyxa+5$wx8FNxS zKuVAVWArlK#kDzEM zqR?&aXIdyvxq~wF?iYPho*(h?k zD(SBpRDZ}z$A})*Qh!9&pZZRyNixD!8)B5{SK$PkVET(yd<8kImQ3ILe%jhx8Ga-1 zE}^k+Eo^?c4Y-t2_qXiVwW6i9o2qosBDj%DRPNT*UXI0=D9q{jB*22t4HHcd$T&Xi zT=Vte*Gz2E^qg%b7ev04Z&(;=I4IUtVJkg<`N6i7tjUn-lPE(Y4HPyJKcSjFnEzCH zPO(w%LmJ_=D~}PyfA91H4gCaf-qur3_KK}}>#9A}c5w@N;-#cHph=x}^mQ3`oo`Y$ope#)H9(kQK zGyt<7eNPuSAs$S%O>2ElZ{qtDIHJ!_THqTwcc-xfv<@1>IJ;YTv@!g-zDKBKAH<

Zet1e^8c}8fE97XH}+lF{qbF<`Y%dU|I!~Y`ZrVfKX82i z)(%!Tcf~eE^%2_`{WBPGPU@1NB5SCXe1sAI<4&n1IwO{&S$ThWn37heGOSW%nW7*L zxh0WK!E7zh%6yF-7%~l@I~b`2=*$;RYbi(I#zp$gL_d39U4A)KuB( zcS0bt48&%G_I~( zL(}w&2NA6#$=|g)J+-?ehHflD^lr77ngdz=dszFI;?~ZxeJv=gsm?4$$6#V==H{fa zqO!EkT>1-OQSJoX)cN}XsB;shvrHRwTH(I2^Ah4|rizn!V7T7fLh~Z<`Q+?zEMVxh z$=-x^RR*PlhkV_8mshTvs+zmZWY&Jk{9LX0Nx|+NAEq-^+Rh|ZlinVZ=e8=`WQt;e@= zPU}^1cG*O;G7l{Y#nl znp`y%CO_SC7gk0i0gY&phM04Y)~vU0!3$V$2T+h(1ZS+cCgc zaC?3M;B48^faGo>h~--#FNFauH?0BJJ6_nG5qOlr>k~%DCSJaOfl%KWHusw>tGrTxAhlEVDxc8R2C-)LCt&$Rt9IKor=ml7jirX@?WW+M z^I{b}MD5r$s>^^sN@&g`cXD~S_u09xo;{;noKZatIuzqd zW1e7oTl9>g8opPBT(p+&fo0F#!c{NFYYpIZ6u8hOB{F#{nP)@})X20$3iJtG$cO zJ$Oxl_qH{sL5d?=D$2M4C3Ajc;GN0(B-HVT;@pJ-LvIrN%|SY?t}g!J>ufQrR%hoY z!nr$tq~N%)9}^tEip93XW=MQ1@XovSvn`PTqXeT9@_7hGv4%LK1M**Q%UKi|(v@1_ zKGe*@+1%Y4v&`;5vUL`C&{tc+_7HFs7*OtjY8@Gg`C4O&#An{0xOvgNSehTHS~_1V z=daxCMzI5b_ydM5$z zZl`a{mM}i@x;=QyaqJY&{Q^R*^1Yzq!dHH~UwCCga+Us~2wk59ArIYtSw9}tEmjbo z5!JA=`=HP*Ae~Z4Pf7sC^A3@Wfa0Ax!8@H_&?WVe*)9B2y!8#nBrP!t1fqhI9jNMd zM_5I)M5z6Ss5t*f$Eh{aH&HBeh310Q~tRl3wCEcZ>WCEq%3tnoHE)eD=)XFQ7NVG5kM zaUtbnq2LQomJSWK)>Zz1GBCIHL#2E>T8INWuN4O$fFOKe$L|msB3yTUlXES68nXRX zP6n*zB+kXqqkpQ3OaMc9GqepmV?Ny!T)R@DLd`|p5ToEvBn(~aZ%+0q&vK1)w4v0* zgW44F2ixZj0!oB~^3k|vni)wBh$F|xQN>~jNf-wFstgiAgB!=lWzM&7&&OYS=C{ce zRJw|)PDQ@3koZfm`RQ$^_hEN$GuTIwoTQIDb?W&wEo@c75$dW(ER6q)qhF`{#7UTuPH&)w`F!w z0EKs}=33m}_(cIkA2rBWvApydi0HSOgc>6tu&+hmRSB%)s`v_NujJNhKLS3r6hv~- z)Hm@?PU{zd0Tga)cJWb2_!!9p3sP%Z zAFT|jy;k>4X)E>4fh^6=SxV5w6oo`mus&nWo*gJL zZH{SR!x)V)y=Qc7WEv-xLR zhD4OcBwjW5r+}pays`o)i$rcJb2MHLGPmeOmt5XJDg@(O3PCbxdDn{6qqb09X44T zh6I|s=lM6Nr#cGaA5-eq*T=LQ6SlRq*`~`b+dVi5^>el1p;#si6}kK}>w;1 z6B1dz{q_;PY{>DBQ+v@1pfXTd5a*^H9U*;qdj@XBF}MoSSQxVXeUpEM5Z0909&8$pRfR|B(t0ox&xl8{8mUNd#(zWONW{oycv$VjP1>q;jU@ z@+8E~fjz*I54OFFaQ{A5jn1w>r;l!NRlI(8q3*%&+tM?lov_G3wB`<}bQ>1=&xUht zmti5VZzV1Cx006Yzt|%Vwid>QPX8Nfa8|sue7^un@C+!3h!?-YK>lSfNIHh|0kL8v zbv_BklQ4HOqje|@Fyxn%IvL$N&?m(KN;%`I$N|muStjSsgG;gP4Smgz$2u(mG;DXP zf~uQ z212x^l6!MW>V@ORUGSFLAAjz3i5zO$=UmD_zhIk2OXUz^LkDLWjla*PW?l;`LLos> z7FBvCr)#)XBByDm(=n%{D>BcUq>0GOV9`i-(ZSI;RH1rdrAJ--f0uuAQ4odl z_^$^U_)0BBJwl@6R#&ZtJN+@a(4~@oYF)yG+G#3=)ll8O#Zv3SjV#zSXTW3h9kqn* z@AHL=vf~KMas}6{+u=}QFumr-!c=(BFP_dwvrdehzTyqco)m@xRc=6b#Dy+KD*-Bq zK=y*1VAPJ;d(b?$2cz{CUeG(0`k9_BIuUki@iRS5lp3=1#g)A5??1@|p=LOE|FNd; z-?5MLKd-5>yQ7n__5W^3C!_`hP(o%_E3BKEmo1h=H(7;{6$XRRW6{u+=oQX<((xAJ zNRY`Egtn#B1EBGHLy^eM5y}Jy0h!GAGhb7gZJoZI-9WuSRw)GVQAAcKd4Qm)pH`^3 zq6EIM}Q zxZGx%aLnNP1an=;o8p9+U^>_Bi`e23E^X|}MB&IkS+R``plrRzTE%ncmfvEW#AHJ~ znmJ`x&ez6eT21aLnoI`%pYYj zzQ?f^ob&Il;>6Fe>HPhAtTZa*B*!;;foxS%NGYmg!#X%)RBFe-acahHs3nkV61(E= zhekiPp1d@ACtA=cntbjuv+r-Zd`+lwKFdqZuYba_ey`&H<Psu;Tzwt;-LQxvv<_D5;ik7 zwETZe`+voUhk%$s2-7Rqfl`Ti_{(fydI(DAHKr<66;rYa6p8AD+NEc@Fd@%m`tiK% z=Mebzrtp=*Q%a}2UdK4J&5#tCN5PX>W=(9rUEXZ8yjRu+7)mFpKh{6;n%!bI(qA9kfyOtstGtOl zX!@*O0fly*L4k##fsm&V0j9Lj<_vu1)i?!#xTB7@2H&)$Kzt@r(GH=xRZlIimTDd_o(%9xO388LwC#;vQ?7OvRU_s< zDS@6@g}VnvQ+tn(C#sx0`J^T4WvFxYI17;uPs-Ub{R`J-NTdtBGl+Q>e81Z3#tDUr ztnVc*p{o|RNnMYts4pdw=P!uJkF@8~h)oV4dXu5F7-j0AW|=mt!QhP&ZV!!82*c7t zuOm>B*2gFtq;A8ynZ~Ms?!gEi5<{R_8tRN%aGM!saR4LJQ|?9w>Ff_61(+|ol_vL4 z-+N>fushRbkB4(e{{SQ}>6@m}s1L!-#20N&h%srA=L50?W9skMF9NGfQ5wU*+0<@> zLww8%f+E0Rc81H3e_5^DB@Dn~TWYk}3tqhO{7GDY;K7b*WIJ-tXnYM@z4rn(LGi?z z8%$wivs)fC#FiJh?(SbH-1bgdmHw&--rn7zBWe1xAhDdv#IRB@DGy}}zS%M0(F_3_ zLb-pWsdJ@xXE;=tpRAw?yj(Gz=i$;bsh&o2XN%24b6+?_gJDBeY zws3PE2u!#Cec>aFMk#ECxDlAs;|M7@LT8)Y4(`M}N6IQ{0YtcA*8e42!n^>`0$LFU zUCq2IR2(L`f++=85M;}~*E($nE&j;p{l%xchiTau*tB9bI= zn~Ygd@<+9DrXxoGPq}@vI1Q3iEfKRleuy*)_$+hg?+GOgf1r?d@Or42|s|D>XMa;ebr1uiTNUq@heusd6%WwJqyCCv!L*qou9l!B22H$bQ z)<)IA>Yo77S;|`fqBk!_PhLJEQb0wd1Z|`pCF;hol!34iQYtqu3K=$QxLW7(HFx~v>`vVRr zyqk^B4~!3F8t8Q_D|GLRrAbbQDf??D&Jd|mgw*t1YCd)CM2$76#Cqj1bD*vADwavp zS<`n@gLU4pwCqNPsIfHKl{5}gu9t-o+O< z??!fMqMrt$s}02pdBbOScUrc1T*{*-ideR6(1q4@oC6mxg8v8Y^h^^hfx6| z|Mld6Ax1CuSlmSJmHwdOix?$8emihK#&8&}u8m!#T1+c5u!H)>QW<7&R$eih)xkov zHvvEIJHbkt+2KQ<-bMR;2SYX?8SI=_<-J!GD5@P2FJ}K z5u82YFotCJF(dUeJFRX_3u8%iIYbRS??A?;iVO?84c}4Du9&jG<#urlZ_Unrcg8dR z!5I3%9F*`qwk#joKG_Q%5_xpU7|jm4h0+l$p;g%Tr>i74#3QnMXdz|1l2MQN$yw|5 zThMw15BxjWf2{KM)XtZ+e#N)ihlkxPe=5ymT9>@Ym%_LF}o z1XhCP`3E1A{iVoHA#|O|&5=w;=j*Qf`;{mBAK3={y-YS$`!0UmtrvzHBfR*s{z<0m zW>4C=%N98hZlUhwAl1X`rR)oL0&A`gv5X79??p_==g*n4$$8o5g9V<)F^u7v0Vv^n z1sp8{W@g6eWv2;A31Rhf5j?KJhITYfXWZsl^`7z`CFtnFrHUWiD?$pwU6|PQjs|7RA0o9ARk^9$f`u3&C|#Z3iYdh<0R`l2`)6+ z6tiDj@xO;Q5PDTYSxsx6n>bj+$JK8IPJ=U5#dIOS-zwyK?+t^V`zChdW|jpZuReE_ z)e~ywgFe!0q|jzsBn&(H*N`%AKpR@qM^|@qFai0};6mG_TvXjJ`;qZ{lGDZHScZk( z>pO+%icp)SaPJUwtIPo1BvGyP8E@~w2y}=^PnFJ$iHod^JH%j1>nXl<3f!nY9K$e` zq-?XYl)K`u*cVXM=`ym{N?z=dHQNR23M8uA-(vsA$6(xn+#B-yY!CB2@`Uz({}}w+ z0sni*39>rMC!Ay|1B@;al%T&xE(wCf+`3w>N)*LxZZZYi{5sqiVWgbNd>W*X?V}C- zjQ4F7e_uCUOHbtewQkq?m$*#@ZvWbu{4i$`aeKM8tc^ zL5!GL8gX}c+qNUtUIcps1S)%Gsx*MQLlQeoZz2y2OQb(A73Jc3`LmlQf0N{RTt;wa`6h|ljX1V7UugML=W5-STDbeWTiEMjPQ$({hn_s&NDXzs6?PLySp$?L`0ilH3vCUO{JS0Dp`z;Ry$6}R@1NdY7rxccbm$+;ApSe=2q!0 z()3$vYN0S$Cs)#-OBs{_2uFf}L4h$;7^2w20=l%5r9ui&pTEgg4U!FoCqyA6r2 zC5s72l}i*9y|KTjDE5gVlYe4I2gGZD)e`Py2gq7cK4at{bT~DSbQQ4Z4sl)kqXbbr zqvXtSqMrDdT2qt-%-HMoqeFEMsv~u)-NJ%Z*ipSJUm$)EJ+we|4*-Mi900K{K|e0; z1_j{X5)a%$+vM7;3j>skgrji92K1*Ip{SfM)=ob^E374JaF!C(cZ$R_E>Wv+?Iy9M z?@`#XDy#=z%3d9&)M=F8Xq5Zif%ldIT#wrlw(D_qOKo4wD(fyDHM5(wm1%7hy6euJ z%Edg!>Egs;ZC6%ktLFtyN0VvxN?*4C=*tOEw`{KQvS7;c514!FP98Nf#d#)+Y-wsl zP3N^-Pnk*{o(3~m=3DX$b76Clu=jMf9E?c^cbUk_h;zMF&EiVz*4I(rFoaHK7#5h0 zW7CQx+xhp}Ev+jw;SQ6P$QHINCxeF8_VX=F3&BWUd(|PVViKJl@-sYiUp@xLS2NuF z8W3JgUSQ&lUp@2E(7MG`sh4X!LQFa6;lInWqx}f#Q z4xhgK1%}b(Z*rZn=W{wBOe7YQ@1l|jQ|9ELiXx+}aZ(>{c7Ltv4d>PJf7f+qjRU8i%XZZFJkj&6D^s;!>`u%OwLa*V5Js9Y$b-mc!t@{C415$K38iVu zP7!{3Ff%i_e!^LzJWhBgQo=j5k<<($$b&%%Xm_f8RFC_(97&nk83KOy@I4k?(k<(6 zthO$3yl&0x!Pz#!79bv^?^85K5e7uS$ zJ33yka2VzOGUhQXeD{;?%?NTYmN3{b0|AMtr(@bCx+c=F)&_>PXgAG}4gwi>g82n> zL3DlhdL|*^WTmn;XPo62HhH-e*XIPSTF_h{#u=NY8$BUW=5@PD{P5n~g5XDg?Fzvb_u ziK&CJqod4srfY2T?+4x@)g9%3%*(Q2%YdCA3yM{s=+QD0&IM`8k8N&-6%iIL3kon> z0>p3BUe!lrz&_ZX2FiP%MeuQY-xVV%K?=bGPOM&XM0XRd7or< zy}jn_eEzuQ>t2fM9ict#ZNxD7HUycsq76IavfoNl$G1|t*qpUSX;YgpmJrr_8yOJ2 z(AwL;Ugi{gJ29@!G-mD82Z)46T`E+s86Qw|YSPO*OoooraA!8x_jQXYq5vUw!5f_x zubF$}lHjIWxFar8)tTg8z-FEz)a=xa`xL~^)jIdezZsg4%ePL$^`VN#c!c6`NHQ9QU zkC^<0f|Ksp45+YoX!Sv>+57q}Rwk*2)f{j8`d8Ctz^S~me>RSakEvxUa^Pd~qe#fb zN7rnAQc4u$*Y9p~li!Itp#iU=*D4>dvJ{Z~}kqAOBcL8ln3YjR{Sp!O`s=5yM zWRNP#;2K#+?I&?ZSLu)^z-|*$C}=0yi7&~vZE$s``IE^PY|dj^HcWI$9ZRm>3w(u` z-1%;;MJbzHFNd^!Ob!^PLO-xhhj@XrI81Y)x4@FdsI( za`o4Gy(`T$P?PB?s>o+eIOtuirMykbuAi65Y_UN1(?jTCy@J8Px`%;bcNmPm#Fr!= z5V!YViFJ!FBfEq>nJFk0^RAV1(7w+X`HRgP;nJHJdMa!}&vvduCMoslwHTes_I76|h>;(-9lbfGnt zoZomakOt759AuTX4b$)G8TzJ&m*BV8!vMs9#=e0tWa z%)84R=3?tfh72~=Rc;fXwj+x z+25xapYK@2@;}6)@8IL+F6iuJ_B{&A-0=U=U6WMbY>~ykVFp$XkH)f**b>TE5)shN z39E2L@JPCSl!?pkvFeh@6dCv9oE}|{GbbVM!XIgByN#md&tXy@>QscU0#z!I&X4;d z&B&ZA4lbrHJ!x4lCN4KC-)u#gT^cE{Xnhu`0RXVKn|j$vz8m}v^%*cQ{(h%FW8_8a zFM{$PirSI8@#*xg2T){A+EKX(eTC66Fb})w{vg%Vw)hvV-$tttI^V5wvU?a{(G}{G z@ob7Urk1@hDN&C$N!Nio9YrkiUC{5qA`KH*7CriaB;2~2Od>2l=WytBRl#~j`EYsj}jqK2xD*3 ztEUiPZzEJC??#Tj^?f)=sRXOJ_>5aO(|V#Yqro05p6)F$j5*wYr1zz|T4qz$0K(5! zr`6Pqd+)%a9Xq3aNKrY9843)O56F%=j_Yy_;|w8l&RU1+B4;pP*O_}X8!qD?IMiyT zLXBOOPg<*BZtT4LJ7DfyghK|_*mMP7a1>zS{8>?}#_XXaLoUBAz(Wi>$Q!L;oQ&cL z6O|T6%Dxq3E35$0g5areq9$2+R(911!Z9=wRPq-pju7DnN9LAfOu3%&onnfx^Px5( zT2^sU>Y)88F5#ATiVoS$jzC-M`vY8!{8#9O#3c&{7J1lo-rcNK7rlF0Zt*AKE(WN* z*o?Tv?Sdz<1v6gfCok8MG6Pzecx9?C zrQG5j^2{V556Hj=xTiU-seOCr2ni@b<&!j>GyHbv!&uBbHjH-U5Ai-UuXx0lcz$D7%=! z&zXD#Jqzro@R=hy8bv>D_CaOdqo6)vFjZldma5D+R;-)y1NGOFYqEr?h zd_mTwQ@K2veZTxh1aaV4F;YnaWA~|<8$p}-eFHashbWW6Dzj=3L=j-C5Ta`w-=QTw zA*k9!Ua~-?eC{Jc)xa;PzkUJ#$NfGJOfbiV^1au;`_Y8|{eJ(~W9pP9q?gLl5E6|e{xkT@s|Ac;yk01+twk_3nuk|lRu{7-zOjLAGe!)j?g+@-;wC_=NPIhk(W zfEpQrdRy z^Q$YBs%>$=So>PAMkrm%yc28YPi%&%=c!<}a=)sVCM51j+x#<2wz?2l&UGHhOv-iu z64x*^E1$55$wZou`E=qjP1MYz0xErcpMiNYM4+Qnb+V4MbM;*7vM_Yp^uXUuf`}-* z_2CnbQ);j5;Rz?7q)@cGmwE^P>4_u9;K|BFlOz_|c^1n~%>!uO#nA?5o4A>XLO{X2 z=8M%*n=IdnXQ}^+`DXRKM;3juVrXdgv79;E=ovQa^?d7wuw~nbu%%lsjUugE8HJ9zvZIM^nWvjLc-HKc2 zbj{paA}ub~4N4Vw5oY{wyop9SqPbWRq=i@Tbce`r?6e`?`iOoOF;~pRyJlKcIJf~G z)=BF$B>YF9>qV#dK^Ie#{0X(QPnOuu((_-u?(mxB7c9;LSS-DYJ8Wm4gz1&DPQ8;0 z=Wao(zb1RHXjwbu_Zv<=9njK28sS}WssjOL!3-E5>d17Lfnq0V$+IU84N z-4i$~!$V-%Ik;`Z3MOqYZdiZ^3nqqzIjLE+zpfQC+LlomQu-uNCStj%MsH(hsimN# z%l4vpJBs_2t7C)x@6*-k_2v0FOk<1nIRO3F{E?2DnS}w> z#%9Oa{`RB5FL5pKLkg59#x~)&I7GzfhiVC@LVFSmxZuiRUPVW*&2ToCGST0K`kRK) z02#c8W{o)w1|*YmjGSUO?`}ukX*rHIqGtFH#!5d1Jd}&%4Kc~Vz`S7_M;wtM|6PgI zNb-Dy-GI%dr3G3J?_yBX#NevuYzZgzZ!vN>$-aWOGXqX!3qzCIOzvA5PLC6GLIo|8 zQP^c)?NS29hPmk5WEP>cHV!6>u-2rR!tit#F6`_;%4{q^6){_CHGhvAs=1X8Fok+l zt&mk>{4ARXVvE-{^tCO?inl{)o}8(48az1o=+Y^r*AIe%0|{D_5_e>nUu`S%zR6|1 zu0$ov7c`pQEKr0sIIdm7hm{4K_s0V%M-_Mh;^A0*=$V9G1&lzvN9(98PEo=Zh$`Vj zXh?fZ;9$d!6sJRSjTkOhb7@jgSV^2MOgU^s2Z|w*e*@;4h?A8?;v8JaLPCoKP_1l- z=Jp0PYDf(d2Z`;O7mb6(_X_~z0O2yq?H`^c=h|8%gfywg#}wIyv&_uW{-e8e)YmGR zI0NNSDoJWa%0ztGzkwl>IYW*DesPRY?oH+ow^(>(47XUm^F`fAa0B~ja-ae$e>4-A z64lb_;|W0ppKI+ zxu2VLZzv4?Mr~mi?WlS-1L4a^5k+qb5#C)ktAYGUE1H?Vbg9qsRDHAvwJUN=w~AuT zUXYioFg2Dx-W)}w9VdFK#vpjoSc!WcvRZ_;TgHu;LSY*i7K_>Px{%C4-IL?6q?Qa_ zL7l=EEo|@X&$gX;fYP02qJF~LN9?E-OL2G(Fo4hW)G{`qnW zTIuc+-1VJvKgph0jAc(LzM);Pg$MPln?U|ek{_5nNJHfm-Y#ec+n#Yf_e>XfbLbN)eqHEDr0#?<;TskL5-0JGv|Ut{=$Xk8hlwbaMXdcI3GL zY-hykR{zX9liy$Z2F3!z346uu%9@-y6Gda`X2*ixlD_P@<}K?AoV?(%lM%* z(xNk=|A()443aGj)-~IDf3J+UA2p2lh6ei^pG*HL#SiThnIr5WZDXebI)F7X zGmP-3bH$i$+(IwqgbM7h%G5oJ@4{Z~qZ#Zs*k7eXJIqg;@0kAGV|b=F#hZs)2BYu1 zr8sj#Zd+Iu^G}|@-dR5S*U-;DqzkX3V0@q-k8&VHW?h0b0?tJ-Atqmg^J8iF7DP6k z)W{g?5~F*$5x?6W)3YKcrNu8%%(DglnzMx5rsU{#AD+WPpRBf``*<8F-x75D$$13U zcaNXYC0|;r&(F@!+E=%+;bFKwKAB$?6R%E_QG5Yn5xX#h+zeI-=mdXD5+D+lEuM`M ze+*G!zX^xbnA?~LnPI=D2`825Ax8rM()i*{G0gcV5MATV?<7mh+HDA7-f6nc@95st zzC_si${|&=$MUj@nLxl_HwEXb2PDH+V?vg zA^DJ%dn069O9TNK-jV}cQKh|$L4&Uh`?(z$}#d+{X zm&=KTJ$+KvLZv-1GaHJm{>v=zXW%NSDr8$0kSQx(DQ)6S?%sWSHUazXSEg_g3agt2@0nyD?A?B%9NYr(~CYX^&U#B4XwCg{%YMYo%e68HVJ7`9KR`mE*Wl7&5t71*R3F>*&hVIaZXaI;2a$?;{Ew{e3Hr1* zbf$&Fyhnrq7^hNC+0#%}n^U2{ma&eS)7cWH$bA@)m59rXlh96piJu@lcKl<>+!1#s zW#6L5Ov%lS(?d66-(n`A%UuiIqs|J|Ulq0RYq-m&RR0>wfA1?<34tI?MBI#a8lY{m z{F2m|A@=`DpZpwdIH#4)9$#H3zr4kn2OX!UE=r8FEUFAwq6VB?DJ8h59z$GXud$#+ zjneIq8uSi&rnG0IR8}UEn5OcZC?@-;$&Ry9hG{-1ta`8aAcOe1|82R7EH`$Qd3sf* zbrOk@G%H7R`j;hOosRVIP_2_-TuyB@rdj?(+k-qQwnhV3niH+CMl>ELX(;X3VzZVJ ztRais0C^L*lmaE(nmhvep+peCqr!#|F?iVagZcL>NKvMS_=*Yl%*OASDl3(mMOY9! z=_J$@nWpA-@><43m4olSQV8(PwhsO@+7#qs@0*1fDj70^UfQ(ORV0N?H{ceLX4<43 zEn)3CGoF&b{t2hbIz;Og+$+WiGf+x5mdWASEWIA*HQ9K9a?-Pf9f1gO6LanVTls)t z^f6_SD|>2Kx8mdQuiJwc_SmZOZP|wD7(_ti#0u=io|w~gq*Odv>@8JBblRCzMKK_4 zM-uO0Ud9>VD>J;zZzueo#+jbS7k#?W%`AF1@ZPI&q%}beZ|ThISf-ly)}HsCS~b^g zktgqOZ@~}1h&x50UQD~!xsW-$K~whDQNntLW=$oZDClUJeSr2$r3}94Wk1>co3beS zoY-7t{rGv|6T?5PNkY zj*XjF()ybvnVz5=BFnLO=+1*jG>E7F%&vm6up*QgyNcJJPD|pHoZ!H6?o3Eig0>-! zt^i-H@bJ;^!$6ZSH}@quF#RO)j>7A5kq4e+7gK=@g;POXcGV28Zv$jybL1J`g@wC# z_DW1ck}3+n@h2LFQhwVfaV@D+-kff4celZC0;0ef?pA#*PPd8Kk8sO1wza&BHQFblVU8P1=-qScHff^^fR zycH!hlHQs7iejITpc4UaBxzqTJ}Z#^lk{W(cr`qtW~Ap;HvuUf#MxgEG?tEU+B?G% znub0I(s@XvI(lva}$Z7<}Qg=rWd5n)}rX{nb+Aw;}?l9LZI-`N-*hts=c6XgjfJs ztp>-686v6ug{glEZ}K=jVG|N1WSWrU*&ue|4Q|O@;s0#L5P*U%Vx;)w7S0ZmLuvwA z@zs2Kut)n1K7qaywO#TbBR`Q~%mdr`V)D`|gN0!07C1!r3{+!PYf9*;h?;dE@#z(k z;o`g~<>P|Sy$ldHTUR3v=_X0Iw6F>3GllrFXVW?gU0q6|ocjd!glA)#f0G7i20ly>qxRljgfO2)RVpvmg#BSrN)GbGsrIb}9 z1t+r;Q>?MGLk#LI5*vR*C8?McB|=AoAjuDk&Pn`KQo z`!|mi{Cz@BGJ!TwMUUTkKXKNtS#OVNxfFI_Gfq3Kpw0`2AsJv9PZPq9x?~kNNR9BR zw#2jp%;FJNoOzW>tE#zskPICp>XSs?|B0E%DaJH)rtLA}$Y>?P+vEOvr#8=pylh zch;H3J`RE1{97O+1(1msdshZx$it^VfM$`-Gw>%NN`K|Tr$0}U`J?EBgR%bg=;et0 z_en)!x`~3so^V9-jffh3G*8Iy6sUq=uFq%=OkYvHaL~#3jHtr4sGM?&uY&U8N1G}QTMdqBM)#oLTLdKYOdOY%{5#Tgy$7QA! zWQmP!Wny$3YEm#Lt8TA^CUlTa{Cpp=x<{9W$A9fyKD0ApHfl__Dz4!HVVt(kseNzV z5Fb`|7Mo>YDTJ>g;7_MOpRi?kl>n(ydAf7~`Y6wBVEaxqK;l;}6x8(SD7}Tdhe2SR zncsdn&`eI}u}@^~_9(0^r!^wuKTKbs-MYjXy#-_#?F=@T*vUG@p4X+l^SgwF>TM}d zr2Ree{TP5x@ZtVcWd3++o|1`BCFK(ja-QP?zj6=ZOq)xf$CfSv{v;jCcNt4{r8f+m zz#dP|-~weHla%rsyYhB_&LHkwuj83RuCO0p;wyXsxW5o6{)zFAC~2%&NL? z=mA}szjHKsVSSnH#hM|C%;r0D$7)T`HQ1K5vZGOyUbgXjxD%4xbs$DAEz)-;iO?3& zXcyU*Z8zm?pP}w&9ot_5I;x#jIn^Joi5jBDOBP1)+p@G1U)pL6;SIO>Nhw?9St2UN zMedM(m(T6bNcPPD`%|9dvXAB&IS=W4?*7-tqldqALH=*UapL!4`2TM_{`W&pm*{?| z0DcsaTdGA%RN={Ikvaa&6p=Ux5ycM){F1OgOh(^Yk-T}a5zHH|=%Jk)S^vv9dY~`x zG+!=lsDjp!D}7o94RSQ-o_g#^CnBJlJ@?saH&+j0P+o=eKqrIApyR7ttQu*0 z1f;xPyH2--)F9uP2#Mw}OQhOFqXF#)W#BAxGP8?an<=JBiokg;21gKG_G8X!&Hv;7 zP9Vpzm#@;^-lf=6POs>UrGm-F>-! zm;3qp!Uw?VuXW~*Fw@LC)M%cvbe9!F(Oa^Y6~mb=8%$lg=?a0KcGtC$5y?`L5}*-j z7KcU8WT>2PpKx<58`m((l9^aYa3uP{PMb)nvu zgt;ia9=ZofxkrW7TfSrQf4(2juZRBgcE1m;WF{v1Fbm}zqsK^>sj=yN(x}v9#_{+C zR4r7abT2cS%Wz$RVt!wp;9U7FEW&>T>YAjpIm6ZSM4Q<{Gy+aN`Vb2_#Q5g@62uR_>II@eiHaay+JU$J=#>DY9jX*2A=&y8G%b zIY6gcJ@q)uWU^mSK$Q}?#Arq;HfChnkAOZ6^002J>fjPyPGz^D5p}o;h2VLNTI{HGg!obo3K!*I~a7)p-2Z3hCV_hnY?|6i`29b zoszLpkmch$mJeupLbt4_u-<3k;VivU+ww)a^ekoIRj4IW4S z{z%4_dfc&HAtm(o`d{CZ^AAIE5XCMvwQSlkzx3cLi?`4q8;iFTzuBAddTSWjfcZp* zn{@Am!pl&fv#k|kj86e$2%NK1G4kU=E~z9L^`@%2<%Dx%1TKk_hb-K>tq8A9bCDfW z@;Dc3KqLafkhN6414^46Hl8Tcv1+$q_sYjj%oHz)bsoGLEY1)ia5p=#eii(5AM|TW zA8=;pt?+U~>`|J(B85BKE0cB4n> zWrgZ)Rbu}^A=_oz65LfebZ(1xMjcj_g~eeoj74-Ex@v-q9`Q{J;M!mITVEfk6cn!u zn;Mj8C&3^8Kn%<`Di^~Y%Z$0pb`Q3TA}$TiOnRd`P1XM=>5)JN9tyf4O_z}-cN|i> zwpp9g`n%~CEa!;)nW@WUkF&<|wcWqfL35A}<`YRxV~$IpHnPQs2?+Fg3)wOHqqAA* zPv<6F6s)c^o%@YqS%P{tB%(Lxm`hsKv-Hb}MM3=U|HFgh8R-|-K(3m(eU$L@sg=uW zB$vAK`@>E`iM_rSo;Cr*?&wss@UXi19B9*0m3t3q^<)>L%4j(F85Ql$i^;{3UIP0c z*BFId*_mb>SC)d#(WM1%I}YiKoleKqQswkdhRt9%_dAnDaKM4IEJ|QK&BnQ@D;i-ame%MR5XbAfE0K1pcxt z{B5_&OhL2cx9@Sso@u2T56tE0KC`f4IXd_R3ymMZ%-!e^d}v`J?XC{nv1mAbaNJX| zXau+s`-`vAuf+&yi2bsd5%xdqyi&9o;h&fcO+W|XsKRFOD+pQw-p^pnwwYGu=hF7& z{cZj$O5I)4B1-dEuG*tU7wgYxNEhqAxH?p4Y1Naiu8Lt>FD%AxJ811`W5bveUp%*e z9H+S}!nLI;j$<*Dn~I*_H`zM^j;!rYf!Xf#X;UJW<0gic?y>NoFw}lBB6f#rl%t?k zm~}eCw{NR_%aosL*t$bmlf$u|U2hJ*_rTcTwgoi_N=wDhpimYnf5j!bj0lQ*Go`F& z6Wg+xRv55a(|?sCjOIshTEgM}2`dN-yV>)Wf$J58>lNVhjRagGZw?U9#2p!B5C3~Nc%S>p`H4PK z7vX@|Uo^*F4GXiFnMf4gwHB;Uk8X4TaLX4A>B&L?mw4&`XBnLCBrK2FYJLrA{*))0 z$*~X?2^Q0KS?Yp##T#ohH1B)y4P+rR7Ut^7(kCwS8QqgjP!aJ89dbv^XBbLhTO|=A z|3FNkH1{2Nh*j{p-58N=KA#6ZS}Ir&QWV0CU)a~{P%yhd-!ehF&~gkMh&Slo9gAT+ zM_&3ms;1Um8Uy0S|0r{{8xCB&Tg{@xotF!nU=YOpug~QlZRKR{DHGDuk(l{)d$1VD zj)3zgPeP%wb@6%$zYbD;Uhvy4(D|u{Q_R=fC+9z#sJ|I<$&j$|kkJiY?AY$ik9_|% z?Z;gOQG5I%{2{-*)Bk|Tia8n>TbrmjnK+8u*_cS%*;%>R|K|?urtIdgTM{&}Yn1;| zk`xq*Bn5HP5a`ANv`B$IKaqA4e-XC`sRn3Z{h!hN0=?x(kTP+fE1}-<3eL+QDFXN- z1JmcDt0|7lZN8sh^=$e;P*8;^33pN>?S7C0BqS)ow4{6ODm~%3018M6P^b~(Gos!k z2AYScAdQf36C)D`w&p}V89Lh1s88Dw@zd27Rv0iE7k#|U4jWDqoUP;-He5cd4V7Ql)4S+t>u9W;R-8#aee-Ct1{fPD+jv&zV(L&k z)!65@R->DB?K6Aml57?psj5r;%w9Vc3?zzGs&kTA>J9CmtMp^Wm#1a@cCG!L46h-j z8ZUL4#HSfW;2DHyGD|cXHNARk*{ql-J2W`9DMxzI0V*($9{tr|O3c;^)V4jwp^RvW z2wzIi`B8cYISb;V5lK}@xtm3NB;88)Kn}2fCH(WRH1l@3XaO7{R*Lc7{ZN1m+#&diI7_qzE z?BS+v<)xVMwt{IJ4yS2Q4(77II<>kqm$Jc3yWL42^gG6^Idg+y3)q$-(m2>E49-fV zyvsCzJ5EM4hyz1r#cOh5vgrzNGCBS}(Bupe`v6z{e z)cP*a8VCbRuhPp%BUwIRvj-$`3vrbp;V3wmAUt{?F z0OO?Mw`AS?y@>w%(pBO=0lohnxFWx`>Hs}V$j{XI2?}BtlvIl7!ZMZukDF7 z^6Rq2H*36KHxJ1xWm5uTy@%7;N0+|<>Up>MmxKhb;WbH1+=S94nOS-qN(IKDIw-yr zi`Ll^h%+%k`Yw?o3Z|ObJWtfO|AvPOc96m5AIw;4;USG|6jQKr#QP}+BLy*5%pnG2 zyN@VMHkD`(66oJ!GvsiA`UP;0kTmUST4|P>jTRfbf&Wii8~a`wMwVZoJ@waA{(t(V zwoc9l*4F>YUM8!aE1{?%{P4IM=;NUF|8YkmG0^Y_jTJtKClDV3D3~P7NSm7BO^r7& zWn!YrNc-ryEvhN$$!P%l$Y_P$s8E>cdAe3=@!Igo^0diL6`y}enr`+mQD;RC?w zb8}gXT!aC`%rdxx2_!`Qps&&w4i0F95>;6;NQ-ys;?j#Gt~HXzG^6j=Pv{3l1x{0( z4~&GNUEbH=9_^f@%o&BADqxb54EAq=8rKA~4~A!iDp9%eFHeA1L!Bb8Lz#kF(p#)X zn`CglEJ(+tr=h4bIIHlLkxP>exGw~{Oe3@L^zA)|Vx~2yNuPKtF^cV6X^5lw8hU*b zK-w6x4l&YWVB%0SmN{O|!`Sh6H45!7}oYPOc+a#a|n3f%G@eO)N>W!C|!FNXV3taFdpEK*A1TFGcRK zV$>xN%??ii7jx5D69O>W6O`$M)iQU7o!TPG*+>v6{TWI@p)Yg$;8+WyE9DVBMB=vnONSQ6k1v z;u&C4wZ_C`J-M0MV&MpOHuVWbq)2LZGR0&@A!4fZwTM^i;GaN?xA%0)q*g(F0PIB( zwGrCC#}vtILC_irDXI5{vuVO-(`&lf2Q4MvmXuU8G0+oVvzZp0Y)zf}Co0D+mUEZz zgwR+5y!d(V>s1} zji+mrd_6KG;$@Le2Ic&am6O+Rk1+QS?urB4$FQNyg2%9t%!*S5Ts{8j*&(H1+W;0~ z$frd%jJjlV;>bXD7!a-&!n52H^6Yp}2h3&v=}xyi>EXXZDtOIq@@&ljEJG{D`7Bjr zaibxip6B6Mf3t#-*Tn7p z96yx1Qv-&r3)4vg`)V~f8>>1_?E4&$bR~uR;$Nz=@U(-vyap|Jx zZ;6Ed+b#GXN+gN@ICTHx{=c@J|97TIPWs(_kjEIwZFHfc!rl8Ep-ZALBEZEr3^R-( z7ER1YXOgZ)&_=`WeHfWsWyzzF&a;AwTqzg~m1lOEJ0Su=C2<{pjK;{d#;E zr2~LgXN?ol2ua5Y*1)`(be0tpiFpKbRG+IK(`N?mIgdd9&e6vxzqxzaa`e7zKa3D_ zHi+c1`|720|dn(z4Qos^e7sn(PU%NYLv$&!|4kEse%DK;YAD06@XO3!EpKpz!^*?(?-Ip zC_Zlb(-_as+-D?0Ag9`|4?)bN)5o(J=&udAY|YgV(YuK9k=E>0z`$dSaL(wmxd!1f zME&3wwv@#{dgeMlZ4}GL!I`VZxtdQY$lmauCN_|mGXqEEj@i~du$|>5UvLjsbq!{; z@jEf;21iC1jFEmIPE^4gykHQzCMLj=2Ek4&FvlpqTlS(0YT%*W<>XgH$4ww`D`aihBGkPM(&EG};Cl&wzg8!jL z`rkqPzvH(0Kd{2n=?Bt8aAU&0IyiA+V-qnXVId^qG!SWZ7%_f&i!D{R#7Jo$%tICxY%j)ebORE>3H_c|to}c#HX;HAC?~B;2mmQrMp2;8T zmzde!k7BYg^Z1r|DUvSD3@{6S<1kndb%Qt%GA# z+sB2&F5L`R&fLRdAlpU_pVsJsYDEz{^ zKGaAz#%W+MPGT+D$+xowMY0=ipM)0p?zym&Aoi)qL(pO_weO(k?s|ELHl^W zviJiFUXRL&?`;3_;mvc02A@sbsW9}#{anvGafZ#ST;}za?XS3}ZG3B4m(SW{>w}Fh z)T5Yi*``Tstmi9SHXmuWSND@cj}qtY!`tuD29Dpu+-D3$h<5FY>jE>YJvqBmhw?oll`x7Ono(}R~P zle_eBwYy0Rr7kmf_SEt_gn4)AO-r`}^Z5Y%Rm8)K-?X>rvDL+QT?#)QwDsQ2c$tc* z&#hbgkL6}GnBDH;+lREM6MGIskRa@r>5Iq(ll2IepuhW86w@14=E{6$cz*cBDQ)CT>}v-DLM-v8)xaPBnmGBKM63RgDGqh!<*j90tSE4|G^+r@#-7g2 zs8KE8eZPZhQuN>wBU%8CmkE9LH1%O;-*ty0&K~01>F3XB>6sAm*m3535)9T&Fz}A4 zwGjZYVea@Fesd=Rv?ROE#q=}yfvQEP8*4zoEw4@^Qvw54utUfaR1T6gLmq?c9sON> z>Np6|0hdP_VURy81;`8{ZYS)EpU9-3;huFq)N3r{yP1ZBCHH7=b?Ig6OFK~%!GwtQ z3`RLKe8O&%^V`x=J4%^Oqg4ZN9rW`UQN^rslcr_Utzd-@u-Sm{rphS-y}{k41)Y4E zfzu}IC=J0JmRCV6a3E38nWl1G495grsDDc^H0Fn%^E0FZ=CSHB4iG<6jW1dY`2gUr zF>nB!y@2%rouAUe9m0VQIg$KtA~k^(f{C*Af_tOl=>vz>$>7qh+fPrSD0YVUnTt)? z;@1E0a*#AT{?oUs#bol@SPm0U5g<`AEF^=b-~&4Er)MsNnPsLb^;fL2kwp|$dwiE3 zNc5VDOQ%Q8j*d5vY##)PGXx51s8`0}2_X9u&r(k?s7|AgtW0LYbtlh!KJ;C9QZuz< zq>??uxAI1YP|JpN$+{X=97Cdu^mkwlB={`aUp+Uyu1P139=t%pSVKo7ZGi_v(0z>l zHLGxV%0w&#xvev)KCQ{7GC$nc3H?1VOsYGgjTK;Px(;o0`lerxB<+EJX9G9f8b+)VJdm(Ia)xjD&5ZL45Np?9 zB%oU;z05XN7zt{Q!#R~gcV^5~Y^gn+Lbad7C{UDX2Nznj8e{)TLH|zEc|{a#idm@z z6(zon+{a>FopmQsCXIs*4-dLGgTc)iOhO3r=l?imNUR-pWl!ktO0r_a0Nqo@bu8MzyjSq9zkqPe*`Sxz75rZ zr9X%(=PVqCRB=zfX+_u&*k4#s1k4OV11YgkCrlr6V;vz<{99HKC@qQ+H8xv5)sc63 z69;U4O&{fb5(fN``jJH#3=GHsV56@{d@7`VhA$K^;GU+R-V%%cnmjYs?>c5^6Ugv} zn<}L&i;2`zzW@(kxf$$gVH@7nh}2%G%ciQ_B?r{13?Q@=Q+6msQGtnyY%Gkjeor?g z7F*tMqLdhcq+LCCo^D;CtOACCBhXgK-M&w{*dcUdmtv@XFTofmmpcWKtCn^`#?oZC zUOm52 z7sK$hR|Vh6y&pfIUK&!`8HH*>12$nWA)Ynp+XwOj=jNLD z{QA4gezbe>wiP?`jJO;c&EId;=2u80s_r97;TX!6@*(<%WL+^bmxheMB3pKx0OpH^ zPs}knV+jpJ4TaD@r^V`mTsjf`7!z^H}eHQ#Rp z72(>Dm#QO!ZYR*O@yHic`3*T^t7jc=d`Jz6Lk@Y-bL%cOp_~=#xzIJl?`{Qu;$uC~NkePE+7wSW_FM`&V{gFN zl;lq@;FtAsl!h;tnOvj z#gYx!q$5MdZ0Jxjy=t*q)HFeeyI-vgaGdh1QNhqGRy8qS)|6S0QK7Gj9R?Co{Knh> za>xkQZ0}bBx!9@EUxRBYGm25^G}&j-`0VWX04E|J!kJ8^WoZ(jbhU_twFwWIH32fv zi=pg~(b#ajW=`)Vikwwe39lpML?|sY$?*6*kYBxku_<=#$gfTqQ_F!9F0=OkHnzBo zEwR!H_h|MNjuG$Tj6zaaouO}HYWCF8vN4C%EX-%Iu%ho;q$G#ErnafhXR*4J2Rp5* zhsi0;wlSwE*inVFO>{(8?N~82zijpt+9Y_-^>xnE%T*zk9gi|j7b@s<5{|qEquUD( zS;-%RySZOCOEh*>!kvbsQ265* z>X8*_Wy&~FB@aDHz%glyiAujXq-|2kDUjFTn9Rafsl+XNyFP%PG|l&ZGWBcEXxy=9 zeDn2PIoVuL$gX0RgVK1O$x3%pOzS7x^U5Pi;mtT)%cY;&e&M7GLM}zP+IPbqLt=^5 z7qLfri8myf;~2psc@^cA6mG&{C%e_(M$$!wC^5p^T1QzrS%I?(U{qcd+oJJkQxe10 zON{Q*?iz%F4MbEsoEc+x3E?&2wVR^v3|Q0lDaMvgS7mNjI{2w! z9|~=!83T%GW*iaChSS!`Xd^beFp9N4%K+k*j#jFumk}U?=WKL_kJAltxnxp~+lZzT zp@&&kSPTg3oSGos`rVBhK0|4NdHM_hnKuw1#0JV{gi_dKDJLB+ix~~HpU9%jD)@YY zOK)L7kgbLyN2%Dx#fuY}8swh4ACk7%BpP-n5(RhDq{gEHP*Fo4IviX{C49|B5h~SC zFr`=0)=h2^F5UpCAgt?R5u{6VvpUf#*nC zCQ`$!|C;L2lpjlG?(>T$(_$O3_YNNbPT~(?!j3aD8k=yu^ogw4bkjvgF|3BOq(hB& zG;^cPXmcUP$ox8zElCJ-zMbK9q^8{rri#8Cek5Ydr0YT-KTh@J z6^AcB9ejew8BY5kzZUZX(7Po==eW<(;uV~E7(BY5c0^xr`cuRwn)47bN?zOb!0?cw z#v}R$z66&m#+AHfo@(^V2#S~bhoUkkTArg+6w>JzZ52r96^({1W!?>4$h0l|-jDfj z>7(<+%67#(A|4hZ3>Y;hd&S?}F;`Vtqz|pK&B>NJ=Faci;gkf-+GmfQR8^zo_vul2 zB!)kfu4Dq_g)8TBBo52*sB6F`qa&JCR=_A$QWgX_K}fZm{Cb2#1q`^S3+WaS>sS#@ z-4k*G=#?z6d_e7JJ+Z8^(t0tNdL{K5F;2nfQbXgld}a(X)Gr;WojOy`^?es~AClT$ z5^lD{WJek0!p-QEH5E7n6DKQ0%_ZBZ=|jfV_MM{VmL8y-Wd|>OmeemP=C@xI@@M~1 zW2S*im@Rc=O>V886_UJ@oh1!2H$Ku&U*Hh_oxd{32)vf1$cRiepv28ricM;}#p!+k zaK{z1I=9Y%3m4|Pj*BD*Fn5Vh?O@oD^1UcjyeNh0fbhh~V6xb#4njlGW8OehUe!MnoR(wn#nsoyL1m!Rov)Nv4~&JEVl7L z#^qYdTpNI#u`N0UbVMiDmD>g2VQcG3>4D6gErgddZnSQTs){BExxRJRB?bIxTdZa z;!S8FHJPPiIDQ*FAUiWSYnjILFjDvxvSC zk z=j4Kx@Pg~&2Z?cmMDa;)#xVeorJrxDBqy{+`kG+ZPQqC@#ku-c3ucU+69$#q_*se` z-H#PFW^>-C0>++|6r=<$Z8)ZFaK=ZjwsNYXqRpl9G|yme@Eld5B-*I69Nx_TResHi z!5nm+>6zaJYQO#%D{~o-oOJ;q`fa5}l!8G*U-E$OM&7@dqciBCWtd}|SrDXz$TB($&m*=Epuolu2k`KUwO7maP3P0ok zmF57lSh0Ba@&sO1iZ5^+3s8{B8t|M;Pg&O+{tZJCiLWd6H@{b~9{CLF9s3Kn zt5)Rs9ejne?o{%f>B$Dl%X7fd~KY)I|(pxUeHj;gNsK6;ZR>`ciu;GxvhDUt!+31Knss2U(%ts8K z18)8;<2ax9RG?!|Lwdt^i5L^&O788roKmVAB)=EdK~HqR2Q=)H_VW}xY=95MP_Ov< zPEz3%DRK}+(aUBwsr83H8>`H^v~|A_t}0vPmRwKPt1{|qOY|PZu}j9+{ZhF&-H_TB zU9xWLpNTc`enI|)h9jQeqf5RfGLFk_vfX`40iMpd%KZF!lKbZTdBw$<^G6nuS+$fT zrbK)xo&;buPJcpOZ=x>n+bRXVFDs(23Xr=rDE&!)pVXZ;;A07NXGl_0m`{Z)DQIu$ zFDvY4xu-ifTe_$|n2B83eI;KUg6pVbw+N!nyLj~wnRi{4mNy{WDV)G1!6$y=+x6U{ z%4_9=Q^L!x_gAYp?J3+u5hA5cO8aHeI=6AC8^S{mzhqCBvBLYEutUC(X0>hKg|AvN zvkmJCQNA45_KjW{aEcyrBppcO6G0zTy%v1&@~+2!n?kA9?>0>AjFN|JdCnHQ8$hEU zw#mwGifHppLP?89LMb(Y3Li9iCPx7W%ek}2FgD2YSzjsR4Xj<=zN{Yo@7s7(k%mP4 znT2p&4EQ@q_chd-E z78uvD*C@oba`U3W2Iw`M#`5C8jOHv8^Li<|j^SI>>>`77Dp71Vtz=J?4Zck4SdRbd zfF}C_>Y(#)r@y!Q0`tMlG#b9>5`fAI$B&tWJfbGlYW$J4V+-s=HH!`+;1XeL@USdx zR0$G&&XBf9lQtkH5)p=U!8J!1{oc4E!N-~Abxl6E;;=3-hMYZ+44?u}zabmCE)yB?*_w91m$n1Yskp&@ z;kxeJX-#ioX^{elyLu~gzx|_KxLpX62MF%Axq3$!Z_P`pBWR?zP8OI`PV~6Aa0Oi0 zv_Ot1m&plf-ZF{e(z(Ms3*S5q$e|j;gOwGrmWsCHfLi(h8y?gc$(2H{884C1FvHQQ12tX=qFUsK~zM!W=K>;zaRsu4Xmcc@8nSs!vK+{ z?}bq}-m&p5jRSam67n>yG9ez=I^|J1O;Np8s=P~9MXYLxD+cFQK7PhG=bkjo{Naae zjp3NWWrlFWDb3Z5D07Q|WjZ=wOQ=aKA%en=O@hL$QCKpIXNZE=InFk|Fhq-&H!6&X z*MVy8=hL7Aw&pQjHrFf27C%3B<>FX{@fOLNhUoxL4*@nY}&M3G*T-p67a zo}~_&yGOB)#vbU|Q3FA8S^X)c-yBlmN(_%}`7Ha3uWFe?>9f=3hlO{^gv~$p`v?vk z_P*r43|(S{%ihs;)YH|jAMpP=-Ms7Ne75_YZZiL3CHVjSU`X1|?Ehh&gA=Xn7W7d@ zf8bM9Y>lG!`PWFDDA9G;x*{1Eh^55u66*9D+-4^dYZ{xXP@?sQLVrY%(azM;C^4FuN7CQ%$!3sr1JL=!Be& zuOZL^bLp$Qo2rL=WDzQIls%s!Go z{s}Q0b#+#8bKga|01t%^9Z=wEsevvXM_{$dCR97ed3@1kX)mtSS!JN^rtqKOj}p~> zfpCI@DX*DqcB6ZnBcl~}sGO~1s$AtfkX6fy3N8*ebvZc*KBW;dA=)?#BE&}-or74i zZUt5;{FBPnkZD8YUXDsx&2LvSziAlec3oc>&Lf1Doc3g?H9{OO_$M4B0qTat0UsWP zTlxUeQ3B;oJ%en4n?zQB6*Fb#wH7`$SQN5GI|=DnJKiYm{?-?#-H;#sIjz7kQ4&VW zN9d1(1$_W~S=<%qDD!mwRytas=eqX^iW}YSx3;wJ#)Xp_`Qk1DFiXac$-3;jQbCif zLA-T_s~5yP@Q@W>pXKl^gipQ>gp@HlBB>WDVpW199;V%?N1`U$ovLE;NI2?|_q2~5 zlg>xT9NADWkv5-*FjS~nP^7$k!N2z?dr!)&l0+4xDK7=-6Rkd$+_^`{bVx!5LgC#N z-dv-k@OlYCEvBfcr1*RsNwcV?QT0bm(q-IyJJ$hm2~mq{6zIn!D20k5)fe(+iM6DJ ze-w_*F|c%@)HREgpRrl@W5;_J5vB4c?UW8~%o0)(A4`%-yNk1(H z5CGuzH(uHQ`&j+IRmTOKoJ?#Ct$+1grR|IitpDGt!~ZdqSJ?cOtw-R=EQ+q4UvclH zdX=xlK-fhQKoKCPBoFAZ*(~11O6-tXo>i0w!T$u{lg!#itEUX3V{$S*naW!C@%rll zS{L(1t%xz(*B`{1NL!*aMc<~fE=g;gXi&Gb$HpD!P)8?JzfN;4F&wv(5HH<=c>>)n z({271)xREH89=C(5YKL{mmJJ_d>qHz;;gTvTlgM*vz9@YTTYZ#%_2A zS0G-t9oMQEpvfv(UjfQ8T$vAHi)zOj3>D*{xSRiu3acc=7cvLyD?_ZObdu$5@b*!y zaZ#u?7uF}SrHVQa=sTOhGW{6WUlq#RhPPm^GsRH#qlX8{Kq-i~98l;eq>KdCnWyKl zUu&UWBqu#Tt9jQ97U4}3)&(p2-eCLznXMEm!>i^EMpeVzPg%p;?@O;dJBQQY(vV;d z3v+-3oTPC!2LTUAx^S2t{v;S_h(EZ^0_dS5g^F*m{TEIy^Qal~%mu3h7*o`jWOH}i ztv8M)3X3a*+ry_KkYXYE4dB0?M|t}#Tp+(}6CQ zBbq;xhoHj}b@j-@koDB#XcCY~>_x&Y;i%MH|3tF^X2h{36UCVfQ-;oEA+4ZkJ`^Qi zQf^8}6eFO$Z+Dj-F1wkG##tTx>FjR2oOXFmbKFj6K3+=kePQ<4d7%z5R5cOB;zO6| zm9^m#U4lcA;7t&*=q|a-!`!)}SgYXT#i8hnxtx@kaoBF$QAS-hT7N5kH^l zB^i+})V>L;9_0Qqf-dyF%ky8Mp-dp#%!Nls3vCt}q3QLM3M-(Zs1k}1bqQ9PVU)U` ztE=?;^6=x}_VD%N@${>qhpkU*)AuUBu_cqYiY&@;O$HV*z@~#Tzh?#=CK`=KwBv+o zh%zu%0xPKYtyC)DaQ zpDW}*86g%>BH3IcWMq`g$j()0kWE(qkIL8A&A0mf&+BzxpKF}=`#jG% z&*wa!&pGFLs5_b#QTZE4Bp+})qzyPQ7B4Z7Y*&?0PSX&|FIR;WBP1|coF9ZeP*$9w z!6aJ_3%Sh=HY3FAt8V144|yfu}IAyYHr1OYKIZ51F>_uY^%N#!k~eU53at-_E-Gh?ahmM5y* z+BTIbeH;%v1}Cjo{8d%UeSMWg(nphxEU`sL< zQR~LrTq>Da(FqSP2%&^1ZL#DTo5Sbl9;&57tQ-@U&I#lj)aNSkcfEJwQD!33?anVU z?pw2q7WtMvfji493`rSFnyp7{w87cW`ak=UEYlk5PCB1K6UDVKXyozOChH4yHh~Q< zv>yvKw6WLfi!PZUx60JZcTNM7jo{ww9b8Q+S7C3WA5&llSwdwh$=Q(*(f3ofqcz=nwOmOy z(J!K=*wNoRU*${{Mbwapi9pTB(&VVKefqd-qrUb9*Eyr2E@oZ9Cgf}Mc;QP<0D)R4 zz=!*^VIG4T*7Xl=sJxrWv9hW^eJ%qYp5(d0?E6LZzJ}=7E+1{?GQA;z+!^VBD81}O z0kJ^dKy&WMw+1+aGVYY-v@i28@Gm+sX5=@U%F=Z?W)oar}2~Rc&F|+3A)n-U2GF10+QdxDb^iA@7eL$c7yhBtL z>lABrh^qy9XZ${E1}Ss5!N4;ig0-pUh6@|RPCHOWvgG{|l}2enRgJftsN%D|ck0YO zuAQd2aMPSyGuJ~jm)aY=+p~mGudw4erwE%P^)5f<*$$2C-4^I=e8-}7##ZQ!8!Tep z+Z_!}CAI~sry$|XK$ktXaxP*x<_ijCPp`2=6sNLZU<@9Sz-rz7^BCE9yh0jV4(I!Z zxmA4d;>B-!vD}Xp*&*N%`b^e&R;D97WS}{~{O-EtXeZNfdf51tw!WR6Noo4hjHPv5 z?heYYRSBPjMc}tFEU^|U8a1CxxK%)WTcn9P%`wR^I$QSeMn6=w>Z9OoVvcrl`zYlZ z2y`mAu0bV(Scc>G_EmIo_4 zm*~h`mxYZC&+U>C5G1FZH5L^U>Cq-9UDRQa35jz&NBj*0{uJKfZs5=Fn@&)Xh6aX(H3w9m9BGLePqVotxTeSPh5-mc7$# z-80t6yB0$Nx<54ohdO*QL7m_(&+#*=eoNiYDB4rE4Cag@qfyZS};Fx;Vf1;oync2k z9v#-w?d6R& zOI`CCS_d=tf3|?g3Z}b6-_Rdg3y~enQhmgkni0Cvf9m6%Ft8r;NC5|b%t&?lkl*4{ z8Ui^;Ds^gq6ti(1xB7y_$zA!i-M~#!!tl$ErTR>P~>T=Yky)8(uvPbvLmB=UfoD zrfl}8<1OQrm?8#j1!?s*T>AoectQl&m!o&*^JcIW`_&bk3tN}k^0rjl=HL$z*uIYt z?7l?^Dqr?q1210Sp$xoAy!&{2^{^Anl460 zI&7urrc&|Y{rjv04VOl{y7c82N6xzg5ueYmQ(q(zC3w_C#x*~%yf5j7MI{W`tsoxzA*PrmK)cTskU| zf2C}Bq$>S$-1JgIh0aW@LxI|-8(OGuD#^M01ghh}&#ObO>tZgSw_LW`zdf&IN$YO# z)|X_9m#JwLW5pErZB3ScggKcNzxA9(hyKkK9I#pR&79&*+SV_eu={00{HF=Bb+AEe znaSof+r1jZ!EL5XgqXWkckaFSSyEk}o!%p8XsD}O>borZ6x%X2b&q!s&1-O(>`kZ$ zB2l^5Cx9xQx9)PXN1xPM)@+LxACH_iZ8zGc(>wnFS_O|@hKsxpMjXOzLEa7OvSlM&&G9ioQw9~RsD4F zK7Q+_&|Q6{eZ^8Rx@pKL`le6kH+(fLc{=V&{b%I5=n}VHV4)X_2Y!pYxgC8wU)yP! zPF3t$?(jsC>Ge=&{kmPGUEETpaw(QTAl)m#{qR3_aq9!wK%6XHfV4C>Y^>Z|%ns7j z{Ja?^IA{+@;kR#IjHxkar%3$eJT4?xNBKUVmoO z`A8Zo-{~_;vcikZ(p}EZzU4kO6WPqkMyE{VvS?;44Z@lj zz^fKX9UL!8Wc(9VgI?P4*zpis8dzl};I>yr1>dtXU=FTAlx}Eht4-*7RACL^AflGh zyZb1hTf(~CkMo%#Q%NMgM9tE2D+)joqbtHYA89Ql1nqVTt+MxZ^*FRd&n5YlIi!8m z>$Ysd!l{+C)y;Wa(ZV-=<+NZKV;v4mt}v2m>`v$-$3b;GsLxf= zd~f(rmfpl``{0aVwN7y!>eGyJFP`L+TxHjHTOS{K^$L2`@6(Rli`{EFwpH@R%eZ6g zwf7rc43Yk!=k;{ z-Rn%~B3amGr}}SxfE$vS8FIPL=Qt57$|R#sSoFgdNUT?fYOYjPl%ZBFpi=jq=DWby7Zxm@y;B<89!9= zbgEH*Uy)~iq5kJLX$+ps$kV`#6jW#|9BGz^`ivNeid(wVbk4jl)VBpW&~;eXNi{#` zwx?{DXR~*sqQcFhY0XCfQ4-*2aN1BGX>$_swtKEqnd>j6vcZ!#0)pXRi?<{!P?tGw z2x_`RD$W)qD{?z}VDPt?+)8*rqLWFIPQ(9-VbBdf{7ff?w9CZ{sIi_gnuC$I0(+P8 zms9XB%}VQ>>pve##}jog6+cD?v~n4Pa9Vmc zg#K$|+`adO=B7`uj35Y}6EZ z{dY`x@w8;R-7zrsr1O_~Jvl*|o-x%jF=Rr1C}GXP^|IYN`1sqmG-oI@R#%X66c#5W z$$tQB)sqwiVm;Y^`Dw3mo|firP{*HsOQJre5%Dm^H@we0FN88VWJ0dja?_U38z73f zrCV!b3qNP0kM#%9T!W5`ynGcg%BL28FW1J-J1_S`BJGCaReQ!am(2%qZ3lLgzq|ns z!!fF@`0=*z)J2BwZ*hO|Yu^cI_nF$9l-Pb3jE7=P8gZ#!xiuZ7-cSa`gb`6mxGTgg z-DLdID?M!Z%+hHB#{?&0$GFRpf+_}q<_wbzX6K?w;%6szz1RbySDSr2r^h_qi$khs zXdZ9A0!_Bf)TR2-^-K~q`FQ!#1x(U4VbV%AA@Ei{%cA(EwC{XfjRi?`&9rav5;Q5% zO1`Rn@OA_ZB@N*mC#)?d3P!}Eh;=NgpIKsy{(yr`hv=aouwt@r&P&}Z3DNWo9ro30 zX52~(aTV$*HHlgB66-4GQru!_AZ|)V*I5X=WG)`N@U&D>e@@C#V@JwEL*L`7#$yes z62C^5%Qniaow2$3HrAc7U{qzpb&FA*xLI1JSWR@`RF=JCcvTI)%dH7;sWInt9JLu# z|Ao|Q?K)cDg_JKsym=joo5gR80wtv01N`um1nQ@Ms0Y*bVzxL34} zo?gizp?`=Y{*W>^Hy2%Jl)y?A+&7s1UVHFixuIy~sawXjcDCL`129cK7|ZQS0u;A} zTJC#WNmqkIrnHpAhHVcM(U^vJA~dl@jf_bs*3?i+=&vuC?Aiy_pcB~=1syDni4 zw+FLuz>F773u#$;NUQ9WDtUPY@+rA3WBhQdKFKOyzkA(URa7;4tW>3jQIfi8v0h3g zJC_HVDXS#>DWb|&se7FHnr=q&l#xg9o02}}u=b-R>@sw={Z zHF*?t2FmhqZ=|qa>x=A!*$S+0T zhO*D*M?NTf-eX`eO)9TIQu{7Dm77Acnj4b1jI9@c*ZL8wL%8kLEhd$KM8=Y!fbN@9 zC7B5#y>JM1n5M)!&im==EgHs2j+xCZG~+~QWCi?s!QyFo2kqx{%jE2n3^N*Ayz6Lp zhg5g^3# z+5FoJ@$u@9WJgPKpUWEd4}4AK9TJKU8W%ms!d0p%OIOX+bY+55zl!vIaz$XFI9Ep+ z;bL_}7PDI2Y`Ng*XY(65 zh0%`@Lve%fc;)N4_g12bNrt6gH=N#OHtxO`$lpWlw=Z6MF+E@;>GkZ#lAZTn`aHwf z&I1|aV#b_VHMIgBN*RzU9i@Z@m}0i>o?({&%fpEfaOpFeaJ7V37;m0?kzd}}Lk@9$ zL}8TEo7WZAcRi%zFZxkr6<0k#X-;lTD`Oc~cDb@olwgWCewvk{GJ}hCXbF!AdiLpd z|Cck$ZTKI?Ack{34Lva7+k=H8K2HTZiurox6F+>dy+@R9T^awxj590D$|kXUg+Ygc z(f)jlRwN(4z$#%PnOVc;#Fv{nAi{#UcXPNcmP#5O{zh_*`=q^JCeia{sN4zHjk2*y zqUVh{Ya{j>SPmP^i#Qfcq_MTqo8g52Fi^F zKBc$$HVI!xFx*4Y9l+nt)$AoZORD}%5I10oI3kx`-N30QueiwIw#0VV2E*Fb-nKW% z=+r^hos`Y-7~{cA1FVbK$_=~*z53+Q8KGjg;>ztg((H12%QTf4OYU8y)C}h5yo#$% z&Q$`vMM*g?ZcatAn2j!hFv8KuN(dw)T*}sF#THDHxo8xC^?vJ zc`U6bVo~hOr6I!8*GTZ<^D~;unKjK=!IR|GB4E>Mcvt*2GK);93jIDd<(nNjHO z4Hi@2^%Uyx=^Z~5eZ!5rO5%4H|eFoNjD#+Kcu%_57zZb4Z@Ak#X6txD^{U3wBl^r+W- zLorkK;uc;NgTj7dGxHQS+@T*T>Q*j4^Ll$ejQqWrwcHyG9y%Mk%m8nBVG5hvSaYm5 zJN^#-Q46kZG)@T8n2^QCjxIwxUVi%s>EY`E?#@_(A~njFrTiDq;8v|W-1jT|ROlNI zU$h|YoD4PVTE^&NC6_m{EAFBVqsM`P*`-AcDGWQygURzM32Xeq2xng~XQsYeTZ5v$ zQLaa2M_Iplw}4eL6fLPu`6`PYcVMysO>`{8CB~glD=TX7?JZcHfHNmykBM?QD)#D) zGp>R*<^D?WhFQKRc^}22l6F=D2RPrxaX2ZF!b1X0XF*d4%=!sbNcS1q2WOUE(7e4$ z^L8f;F)__d3>&KQFE8%$I4h^y5FYBfB&fWzn71_OSrPe-DHV{O#Q;GP z+Tw!J?eVjX19RKH?*hKQWQt8r7B#lYX8xoSHFGCW-*DSQ4EM4M3Mw%gkSYNK18@(e zfzMF}WWaCyS@1y%-~Xg0ry~tkQkUmKuI5lGAua{{vn22V!2T()AU5FpKh@Nv)s^Js zv~@VuUG;=CnLmQR{PeUBQf2;lAV!vG>^Z0N zL88rrjL-*J!43;7C=w9xhcw`yjRKq7o4L9=0SmR9PA-nX12@#h(iIu-0N_xm2OV)( zU_raT0y>$wm^oMi2|U3N;OhF9uy}`<-xVka#DV*l{O0yHzi9vUxa1Qtpi$buR*8cU zd4~lS1pT$L^!0=6qUKOpM+XPsy{f7W#1bjrEwaeN!Ik9(zySIT^pEHvHgJUneFN4) zk=k|$55(g8slmS|@+*4fr2urd3LwjIIZA**g+%l(SZNn4HwQ}y6o`vw>2&mR1X+&q zDa1Af0B;4rAMZMOlHbAqK|R_xuwJ7ANARtFE({-P2o{tJJR<>2KVp)ZK-M;)ejx zd*E~Mka<{OL7%CAhk4n|1qg?97-I!l0rOinjVi#arbgg4bi5;nY5oFL`UWtPk5&L#grSxv zE3!}=1px!ZTLT90aYc^s`~{VojjJml&<`@e41dFP+XU6D0AOkbn2rlI3>^LcqauG& zc$m3Z{!u8LvUrm^fT{qX5yD9{?r(CCiUdck%!T`KIZd2oQJz1joB&M(Teg_>;yS<2-5>BWfSPpG`Rt{!j6>kqMAvl^zk0JUEfy$HVJMkxP-GkwZuxL62me2#pj_5*ZIU zP~#C^OZLfl$HO)v;~~c&JHivn|1I9H5y_CDkt0JLLGKm(4*KLVhJ2jh2#vJuM6`b& zE==-lvME^Oj022xF&IV*? '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..096502d2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'server' diff --git a/src/main/java/com/livable/server/LivableServerApplication.java b/src/main/java/com/livable/server/LivableServerApplication.java new file mode 100644 index 00000000..fb598eb6 --- /dev/null +++ b/src/main/java/com/livable/server/LivableServerApplication.java @@ -0,0 +1,13 @@ +package com.livable.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LivableServerApplication { + + public static void main(String[] args) { + SpringApplication.run(LivableServerApplication.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/com/livable/server/LivableServerApplicationTests.java b/src/test/java/com/livable/server/LivableServerApplicationTests.java new file mode 100644 index 00000000..6a40278c --- /dev/null +++ b/src/test/java/com/livable/server/LivableServerApplicationTests.java @@ -0,0 +1,13 @@ +package com.livable.server; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LivableServerApplicationTests { + + @Test + void contextLoads() { + } + +} From 5b3240df817bfc9832cd4725f13ddddf21ac6d5b Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:45:13 +0900 Subject: [PATCH 002/111] =?UTF-8?q?JPA=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: BaseTimeEntity ์ƒ์† ํด๋ž˜์Šค ์ƒ์„ฑ (#5) createdAt, updatedAt ์ž๋™ ์ƒ์„ฑ ๊ธฐ๋Šฅ์„ ์œ„ํ•ด ์ƒ์†๋ฐ›๊ธฐ ์œ„ํ•œ entity ์ƒ์„ฑ BaseEntity๋Š” ๋„ˆ๋ฌด ๊ด‘๋ฒ”์œ„ํ•ด์„œ BaseTimeEntity๋ผ๊ณ  ์ง€์—ˆ์Œ. ์ˆ˜์ • ๊ฐ€๋Šฅ. * Feat: Building ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: BuildingRestaurantMap ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: CafeteriaReview ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: CafeteriaReviewImage ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Category ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: CommonPlace ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Company ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Evaluation Enum ํด๋ž˜์Šค ์ƒ์„ฑ (#5) Enumerate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ๋ทฐ ํ‰๊ฐ€๋ฅผ ๋‚จ๊ธฐ๊ธฐ ์œ„ํ•œ ๊ฐ’ ํƒ€์ž…์˜ ๊ฐ์ฒด * Feat: InvitationPurpose Enum ํด๋ž˜์Šค ์ƒ์„ฑ (#5) Enumerate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ดˆ๋Œ€์žฅ ๋ชฉ์ ์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’ ํƒ€์ž… ๊ฐ์ฒด * Feat: PointCode Enum ํด๋ž˜์Šค ์ƒ์„ฑ (#5) Enumerate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํฌ์ธํŠธ ํš๋“ ์ƒ์„ธ ์„ค๋ช…์„ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’ ํƒ€์ž… ๊ฐ์ฒด * Feat: ReferenceType Enum ํด๋ž˜์Šค ์ƒ์„ฑ (#5) Enumerate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ๋ทฐ ์ƒ์„ธ ํƒ€์ž… ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’ ํƒ€์ž… ๊ฐ์ฒด * Feat: Role Enum ํด๋ž˜์Šค ์ƒ์„ฑ (#5) Enumerate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์œ ์ €์˜ ์—ญํ• ์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’ ํƒ€์ž… ๊ฐ์ฒด * Feat: Invitation ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: InvitationReservationMap ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: LunchBoxReview ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: LunchBoxReviewImage ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Member ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Menu ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: MenuChoiceLog ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: MenuChoiceResult ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Office ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: ParkingLog ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Point ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: PointLog ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Reservation ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Restaurant ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: RestaurantMenuMap ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Review ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: ReviewImage ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: Visitor ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Feat: JpaConfig ํด๋ž˜์Šค ์ƒ์„ฑ (#5) * Modify: CafeteriaReview ํด๋ž˜์Šค ํ•„๋“œ ์ถ”๊ฐ€ (#5) description ํ•„๋“œ ์ถ”๊ฐ€ * Style: ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ (#5) BaseTimeEntity ํ•„๋“œ ์‚ฌ์ด์— ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ * Modify: InvitationReservationMap ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (#5) Invitation๊ณผ Reservation ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์„ค์ • * Modify: BuildingRestaurantMap ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (#5) Building Restaurant ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์„ค์ • * Modify: Invitation ํ•„๋“œ๋ช… ์ˆ˜์ • invitationPurpose -> purpose๋กœ ๋ณ€๊ฒฝ * Modify: InvitationReservationMap ํด๋ž˜์Šค Reservation ์—ฐ๊ด€๊ด€๊ณ„ ์ˆ˜์ • @ManyToOne -> @OneToOne์œผ๋กœ ๋ณ€๊ฒฝ Reservation์€ ํƒ€์ž„ ๋‹จ์œ„์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•œ ํƒ€์ž„์€ ์˜ค์ง ํ•œ๋ฒˆ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ * Modify: InvitationReservationMap ํด๋ž˜์Šค ๋ณตํ•ฉ์ธ๋ฑ์Šค ์‚ญ์ œ ๋ถˆํ•„์š”ํ•œ ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์‚ญ์ œ reservation ํ•„๋“œ์— unique ์˜ต์…˜์ด ๊ฑธ๋ ค์žˆ๊ธฐ ๋•Œ๋ฌธ์—, invitation๊ณผ reservation ๋‘๊ฐœ๋ฅผ ๋ณตํ•ฉ์ธ๋ฑ์Šค๋กœ ๋ฌถ์ง€ ์•Š์•„๋„ ๋ฌธ์ œ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Œ * Style: Member ํด๋ž˜์Šค ๋ถˆํ•„์š”ํ•œ ์ค„๋ฐ”๊ฟˆ ์ œ๊ฑฐ ๋ฐ @Column ์ถ”๊ฐ€ @Column์€ ํ†ต์ผ์„ฑ์„ ์œ„ํ•˜์—ฌ ์ถ”๊ฐ€ํ•จ * Type: LunchBoxReviewImage ํด๋ž˜์Šค ์–ด๋…ธํ…Œ์ด์…˜ ์˜คํƒ€ ์ˆ˜์ • ์—ฐ๊ด€๊ด€๊ณ„ ํ•„๋“œ lunchBoxReview์— ๋Œ€ํ•œ ์ปฌ๋Ÿผ ์–ด๋…ธํ…Œ์ด์…˜์„ @Column -> @JoinColumn์œผ๋กœ ์ˆ˜์ • * Modify: PointLog ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•ด column name ์„ค์ • * Modify: BuildingRestaurantMap ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•ด column name ์„ค์ • * Modify: MenuChoiceLog ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•ด column name ์„ค์ • * Modify: MenuChoiceResult ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•ด column name ์„ค์ • * Modify: RestaurantMenuMap ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•ด column name ์„ค์ • * Modify: Menu ํ•„๋“œ ์ถ”๊ฐ€ (#5) ๋ฉ”๋‰ด name ํ•„๋“œ ์ถ”๊ฐ€ * Modify: CommonPlace ์—ฐ๊ด€๊ด€๊ณ„ ์ง€์—ฐ๋กœ๋”ฉ ์„ค์ • (#5) buinding ํ•„๋“œ์— ์ง€์—ฐ๋กœ๋”ฉ ์ถ”๊ฐ€ * Modify: JPA ์—”ํ‹ฐํ‹ฐ ์—ฐ๊ด€๊ด€๊ณ„ ์ •๋ฆฌ (#5) Review ํ…Œ์ด๋ธ” ์ƒ์† ํ…Œ์ด๋ธ” ์กฐ์ธ ์ „๋žต์œผ๋กœ ๋ถ„๋ฆฌ --- .../com/livable/server/config/JpaConfig.java | 9 ++++ .../livable/server/entity/BaseTimeEntity.java | 25 ++++++++++ .../com/livable/server/entity/Building.java | 46 ++++++++++++++++++ .../server/entity/BuildingRestaurantMap.java | 36 ++++++++++++++ .../server/entity/CafeteriaReview.java | 41 ++++++++++++++++ .../server/entity/CafeteriaReviewImage.java | 24 ++++++++++ .../com/livable/server/entity/Category.java | 20 ++++++++ .../livable/server/entity/CommonPlace.java | 27 +++++++++++ .../com/livable/server/entity/Company.java | 24 ++++++++++ .../com/livable/server/entity/Evaluation.java | 8 ++++ .../com/livable/server/entity/Invitation.java | 47 +++++++++++++++++++ .../server/entity/InvitationPurpose.java | 12 +++++ .../entity/InvitationReservationMap.java | 25 ++++++++++ .../livable/server/entity/LunchBoxReview.java | 13 +++++ .../server/entity/LunchBoxReviewImage.java | 24 ++++++++++ .../com/livable/server/entity/Member.java | 42 +++++++++++++++++ .../java/com/livable/server/entity/Menu.java | 24 ++++++++++ .../livable/server/entity/MenuChoiceLog.java | 41 ++++++++++++++++ .../server/entity/MenuChoiceResult.java | 41 ++++++++++++++++ .../com/livable/server/entity/Office.java | 30 ++++++++++++ .../com/livable/server/entity/ParkingLog.java | 34 ++++++++++++++ .../java/com/livable/server/entity/Point.java | 25 ++++++++++ .../com/livable/server/entity/PointCode.java | 20 ++++++++ .../com/livable/server/entity/PointLog.java | 43 +++++++++++++++++ .../livable/server/entity/ReferenceType.java | 8 ++++ .../livable/server/entity/Reservation.java | 42 +++++++++++++++++ .../com/livable/server/entity/Restaurant.java | 32 +++++++++++++ .../server/entity/RestaurantMenuMap.java | 33 +++++++++++++ .../server/entity/RestaurantReview.java | 30 ++++++++++++ .../com/livable/server/entity/Review.java | 41 ++++++++++++++++ .../livable/server/entity/ReviewImage.java | 24 ++++++++++ .../java/com/livable/server/entity/Role.java | 5 ++ .../com/livable/server/entity/Visitor.java | 27 +++++++++++ src/main/resources/application.properties | 1 - src/main/resources/application.yml | 0 35 files changed, 923 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/livable/server/config/JpaConfig.java create mode 100644 src/main/java/com/livable/server/entity/BaseTimeEntity.java create mode 100644 src/main/java/com/livable/server/entity/Building.java create mode 100644 src/main/java/com/livable/server/entity/BuildingRestaurantMap.java create mode 100644 src/main/java/com/livable/server/entity/CafeteriaReview.java create mode 100644 src/main/java/com/livable/server/entity/CafeteriaReviewImage.java create mode 100644 src/main/java/com/livable/server/entity/Category.java create mode 100644 src/main/java/com/livable/server/entity/CommonPlace.java create mode 100644 src/main/java/com/livable/server/entity/Company.java create mode 100644 src/main/java/com/livable/server/entity/Evaluation.java create mode 100644 src/main/java/com/livable/server/entity/Invitation.java create mode 100644 src/main/java/com/livable/server/entity/InvitationPurpose.java create mode 100644 src/main/java/com/livable/server/entity/InvitationReservationMap.java create mode 100644 src/main/java/com/livable/server/entity/LunchBoxReview.java create mode 100644 src/main/java/com/livable/server/entity/LunchBoxReviewImage.java create mode 100644 src/main/java/com/livable/server/entity/Member.java create mode 100644 src/main/java/com/livable/server/entity/Menu.java create mode 100644 src/main/java/com/livable/server/entity/MenuChoiceLog.java create mode 100644 src/main/java/com/livable/server/entity/MenuChoiceResult.java create mode 100644 src/main/java/com/livable/server/entity/Office.java create mode 100644 src/main/java/com/livable/server/entity/ParkingLog.java create mode 100644 src/main/java/com/livable/server/entity/Point.java create mode 100644 src/main/java/com/livable/server/entity/PointCode.java create mode 100644 src/main/java/com/livable/server/entity/PointLog.java create mode 100644 src/main/java/com/livable/server/entity/ReferenceType.java create mode 100644 src/main/java/com/livable/server/entity/Reservation.java create mode 100644 src/main/java/com/livable/server/entity/Restaurant.java create mode 100644 src/main/java/com/livable/server/entity/RestaurantMenuMap.java create mode 100644 src/main/java/com/livable/server/entity/RestaurantReview.java create mode 100644 src/main/java/com/livable/server/entity/Review.java create mode 100644 src/main/java/com/livable/server/entity/ReviewImage.java create mode 100644 src/main/java/com/livable/server/entity/Role.java create mode 100644 src/main/java/com/livable/server/entity/Visitor.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/src/main/java/com/livable/server/config/JpaConfig.java b/src/main/java/com/livable/server/config/JpaConfig.java new file mode 100644 index 00000000..906573a7 --- /dev/null +++ b/src/main/java/com/livable/server/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.livable.server.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/src/main/java/com/livable/server/entity/BaseTimeEntity.java b/src/main/java/com/livable/server/entity/BaseTimeEntity.java new file mode 100644 index 00000000..7f00ea0b --- /dev/null +++ b/src/main/java/com/livable/server/entity/BaseTimeEntity.java @@ -0,0 +1,25 @@ +package com.livable.server.entity; + + +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/livable/server/entity/Building.java b/src/main/java/com/livable/server/entity/Building.java new file mode 100644 index 00000000..8553b7b6 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Building.java @@ -0,0 +1,46 @@ +package com.livable.server.entity; + +import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Building extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @ColumnDefault("true") + private Boolean hasCafeteria; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String address; + + @Column(nullable = false) + private LocalTime startTime; + + @Column(nullable = false) + private LocalTime endTime; + + @Column(nullable = false) + private String scale; + + @Column(nullable = false) + private String longitude; + + @Column(nullable = false) + private String latitude; +} diff --git a/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java new file mode 100644 index 00000000..d52d2fde --- /dev/null +++ b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java @@ -0,0 +1,36 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "building_restaurant_map", + uniqueConstraints = + @UniqueConstraint( + name = "BUILDING_RESTAURANT_UNIQUE_IDX", + columnNames = {"building_id", "restaurant_id"} + ) +) +@Entity +public class BuildingRestaurantMap extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, name = "building_id") + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + @JoinColumn(nullable = false, name = "restaurant_id") + @ManyToOne(fetch = FetchType.LAZY) + private Restaurant restaurant; + + @Column(nullable = false) + private Integer distance; +} diff --git a/src/main/java/com/livable/server/entity/CafeteriaReview.java b/src/main/java/com/livable/server/entity/CafeteriaReview.java new file mode 100644 index 00000000..154732b6 --- /dev/null +++ b/src/main/java/com/livable/server/entity/CafeteriaReview.java @@ -0,0 +1,41 @@ +package com.livable.server.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder +@Entity +@ToString(callSuper = true) +public class CafeteriaReview extends Review { + + private Evaluation taste; + + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private Long id; +// +// @JoinColumn(nullable = false) +// @ManyToOne(fetch = FetchType.LAZY) +// private Building building; +// +// @JoinColumn(nullable = false) +// @ManyToOne(fetch = FetchType.LAZY) +// private Member member; +// +// @Enumerated(EnumType.STRING) +// @Column(nullable = false) +// private Evaluation taste; +// +// @Column(nullable = false) +// private String description; + +} diff --git a/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java b/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java new file mode 100644 index 00000000..9ad4e926 --- /dev/null +++ b/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class CafeteriaReviewImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private CafeteriaReview cafeteriaReview; + + @Column(nullable = false) + private String url; +} diff --git a/src/main/java/com/livable/server/entity/Category.java b/src/main/java/com/livable/server/entity/Category.java new file mode 100644 index 00000000..e6c182da --- /dev/null +++ b/src/main/java/com/livable/server/entity/Category.java @@ -0,0 +1,20 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Category extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; +} diff --git a/src/main/java/com/livable/server/entity/CommonPlace.java b/src/main/java/com/livable/server/entity/CommonPlace.java new file mode 100644 index 00000000..741c52d7 --- /dev/null +++ b/src/main/java/com/livable/server/entity/CommonPlace.java @@ -0,0 +1,27 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class CommonPlace extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + private String name; + + private String floor; + + private String roomNumber; +} diff --git a/src/main/java/com/livable/server/entity/Company.java b/src/main/java/com/livable/server/entity/Company.java new file mode 100644 index 00000000..02317c5f --- /dev/null +++ b/src/main/java/com/livable/server/entity/Company.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Company extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + @Column(nullable = false) + private String name; +} diff --git a/src/main/java/com/livable/server/entity/Evaluation.java b/src/main/java/com/livable/server/entity/Evaluation.java new file mode 100644 index 00000000..a2885277 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Evaluation.java @@ -0,0 +1,8 @@ +package com.livable.server.entity; + +public enum Evaluation { + + GOOD, + BAD, + ; +} diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java new file mode 100644 index 00000000..b1e07670 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Invitation.java @@ -0,0 +1,47 @@ +package com.livable.server.entity; + +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import javax.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Invitation extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + private String description; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private InvitationPurpose purpose; + + @Column(nullable = false) + private String officeName; + + @Column(nullable = false) + private LocalDate startDate; + + @Column(nullable = false) + private LocalDate endDate; + + @Column(nullable = false) + private LocalTime startTime; + + @Column(nullable = false) + private LocalTime endTime; + +} diff --git a/src/main/java/com/livable/server/entity/InvitationPurpose.java b/src/main/java/com/livable/server/entity/InvitationPurpose.java new file mode 100644 index 00000000..ff58ffc7 --- /dev/null +++ b/src/main/java/com/livable/server/entity/InvitationPurpose.java @@ -0,0 +1,12 @@ +package com.livable.server.entity; + +public enum InvitationPurpose { + + MEETING, + INTERVIEW, + PERIOD_WORK, + SEMINAR, + AFTER_SERVICE, + ETC, + ; +} diff --git a/src/main/java/com/livable/server/entity/InvitationReservationMap.java b/src/main/java/com/livable/server/entity/InvitationReservationMap.java new file mode 100644 index 00000000..a3a29de5 --- /dev/null +++ b/src/main/java/com/livable/server/entity/InvitationReservationMap.java @@ -0,0 +1,25 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class InvitationReservationMap extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Invitation invitation; + + @JoinColumn(nullable = false, unique = true) + @OneToOne(fetch = FetchType.LAZY) + private Reservation reservation; +} diff --git a/src/main/java/com/livable/server/entity/LunchBoxReview.java b/src/main/java/com/livable/server/entity/LunchBoxReview.java new file mode 100644 index 00000000..6c6f158a --- /dev/null +++ b/src/main/java/com/livable/server/entity/LunchBoxReview.java @@ -0,0 +1,13 @@ +package com.livable.server.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder +@Entity +public class LunchBoxReview extends Review { +} diff --git a/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java b/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java new file mode 100644 index 00000000..69feb55b --- /dev/null +++ b/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class LunchBoxReviewImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private LunchBoxReview lunchBoxReview; + + @Column(nullable = false) + private String url; +} diff --git a/src/main/java/com/livable/server/entity/Member.java b/src/main/java/com/livable/server/entity/Member.java new file mode 100644 index 00000000..fb4d839e --- /dev/null +++ b/src/main/java/com/livable/server/entity/Member.java @@ -0,0 +1,42 @@ +package com.livable.server.entity; + +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Member extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Company company; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + @ColumnDefault("'USER'") + @Enumerated(EnumType.STRING) + private Role role; + + @Column + private String businessCardImage; + + @Column(nullable = false, unique = true) + private String contact; + + @Column(nullable = false) + private String name; +} diff --git a/src/main/java/com/livable/server/entity/Menu.java b/src/main/java/com/livable/server/entity/Menu.java new file mode 100644 index 00000000..56b0ceb3 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Menu.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Menu extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Category category; + + @Column(nullable = false, unique = true) + private String name; +} diff --git a/src/main/java/com/livable/server/entity/MenuChoiceLog.java b/src/main/java/com/livable/server/entity/MenuChoiceLog.java new file mode 100644 index 00000000..3405f35a --- /dev/null +++ b/src/main/java/com/livable/server/entity/MenuChoiceLog.java @@ -0,0 +1,41 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "menu_choice_log", + uniqueConstraints = + @UniqueConstraint( + name = "MEMBER_DATE_UNIQUE_IDX", + columnNames = {"member_id", "date"} + ) +) +@Entity +public class MenuChoiceLog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, name = "member_id") + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Menu menu; + + @Column(nullable = false, name = "date") + private LocalDate date; +} diff --git a/src/main/java/com/livable/server/entity/MenuChoiceResult.java b/src/main/java/com/livable/server/entity/MenuChoiceResult.java new file mode 100644 index 00000000..70ca60e9 --- /dev/null +++ b/src/main/java/com/livable/server/entity/MenuChoiceResult.java @@ -0,0 +1,41 @@ +package com.livable.server.entity; + +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import javax.persistence.*; +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "menu_choice_result", + uniqueConstraints = + @UniqueConstraint( + name = "MEMBER_BUILDING_DATE_UNIQUE_IDX", + columnNames = {"building_id", "menu_id", "date"} + ) +) +@Entity +public class MenuChoiceResult extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, name = "building_id") + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + @JoinColumn(nullable = false, name = "menu_id") + @ManyToOne(fetch = FetchType.LAZY) + private Menu menu; + + @Column(nullable = false, name = "date") + private LocalDate date; + + @Column(nullable = false) + private Integer count; +} diff --git a/src/main/java/com/livable/server/entity/Office.java b/src/main/java/com/livable/server/entity/Office.java new file mode 100644 index 00000000..0dd955a6 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Office.java @@ -0,0 +1,30 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Office extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Company company; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String floor; + + @Column(nullable = false) + private String roomNumber; +} diff --git a/src/main/java/com/livable/server/entity/ParkingLog.java b/src/main/java/com/livable/server/entity/ParkingLog.java new file mode 100644 index 00000000..044ea298 --- /dev/null +++ b/src/main/java/com/livable/server/entity/ParkingLog.java @@ -0,0 +1,34 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class ParkingLog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, unique = true) + @ManyToOne(fetch = FetchType.LAZY) + private Visitor visitor; + + @Column(nullable = false) + private String carNumber; + + @Column + private LocalDateTime inTime; + + @Column + private LocalDateTime outTime; + + @Column + private Integer stayTime; +} diff --git a/src/main/java/com/livable/server/entity/Point.java b/src/main/java/com/livable/server/entity/Point.java new file mode 100644 index 00000000..6ab3d481 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Point.java @@ -0,0 +1,25 @@ +package com.livable.server.entity; + +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Point extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, unique = true) + @OneToOne(fetch = FetchType.LAZY) + private Member member; + + @Column(nullable = false) + private Integer balance; +} diff --git a/src/main/java/com/livable/server/entity/PointCode.java b/src/main/java/com/livable/server/entity/PointCode.java new file mode 100644 index 00000000..7b82116b --- /dev/null +++ b/src/main/java/com/livable/server/entity/PointCode.java @@ -0,0 +1,20 @@ +package com.livable.server.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PointCode { + + PA00("์™ธ๋ถ€ ์‹๋‹น ์˜ค์ ์™„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์„ ํ†ตํ•ด ํฌ์ธํŠธ ํš๋“"), + PA01("๊ตฌ๋‚ด ์‹๋‹น ์˜ค์ ์™„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์„ ํ†ตํ•ด ํฌ์ธํŠธ ํš๋“"), + PA02("๋„์‹œ๋ฝ ์˜ค์ ์™„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์„ ํ†ตํ•ด ํฌ์ธํŠธ ํš๋“"), + PA03("์˜ค์ ์™„ 7์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), + PA04("์˜ค์ ์™„ 14์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), + PA05("์˜ค์ ์™„ 21์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), + PM00("์ œํœด ์นดํŽ˜ ๋ฉ”๋‰ด ํ• ์ธ์— ๋Œ€ํ•œ ํฌ์ธํŠธ ์‚ฌ์šฉ"), + ; + + private final String description; +} diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java new file mode 100644 index 00000000..153d4189 --- /dev/null +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -0,0 +1,43 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "point_log", + uniqueConstraints = + @UniqueConstraint( + name = "REFERENCE_UNIQUE_IDX", + columnNames = {"reference_id", "reference_type"} + ) +) +@Entity +public class PointLog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Point point; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PointCode code; + + @Column(nullable = false) + private Integer amount; + + @Column(nullable = false, name = "reference_id") + private Long referenceId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, name = "reference_type") + private ReferenceType referenceType; +} diff --git a/src/main/java/com/livable/server/entity/ReferenceType.java b/src/main/java/com/livable/server/entity/ReferenceType.java new file mode 100644 index 00000000..9f9ab817 --- /dev/null +++ b/src/main/java/com/livable/server/entity/ReferenceType.java @@ -0,0 +1,8 @@ +package com.livable.server.entity; + +public enum ReferenceType { + REVIEW, + CAFETERIA_REVIEW, + LUNCH_BOX_REVIEW, + ; +} diff --git a/src/main/java/com/livable/server/entity/Reservation.java b/src/main/java/com/livable/server/entity/Reservation.java new file mode 100644 index 00000000..bfb71908 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Reservation.java @@ -0,0 +1,42 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "reservation", + uniqueConstraints = + @UniqueConstraint( + name = "COMMON_PLACE_DATE_TIME_UNIQUE_IDX", + columnNames = {"common_place_id", "date", "time"} + ) +) +@Entity +public class Reservation extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Company company; + + @JoinColumn(nullable = false, name = "common_place_id") + @ManyToOne(fetch = FetchType.LAZY) + private CommonPlace commonPlace; + + @Column(nullable = false, name = "date") + private LocalDate date; + + @Column(nullable = false, name = "time") + private LocalTime time; +} diff --git a/src/main/java/com/livable/server/entity/Restaurant.java b/src/main/java/com/livable/server/entity/Restaurant.java new file mode 100644 index 00000000..aa5bb2e9 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Restaurant.java @@ -0,0 +1,32 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Restaurant extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String contact; + + @Column(nullable = false) + private String address; + + @Column(nullable = false) + private String restaurantUrl; + + @Column(nullable = false) + private String thumbnailImage; +} diff --git a/src/main/java/com/livable/server/entity/RestaurantMenuMap.java b/src/main/java/com/livable/server/entity/RestaurantMenuMap.java new file mode 100644 index 00000000..ac70ae5a --- /dev/null +++ b/src/main/java/com/livable/server/entity/RestaurantMenuMap.java @@ -0,0 +1,33 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "restaurant_menu_map", + uniqueConstraints = + @UniqueConstraint( + name = "RESTAURANT_MENU_UNIQUE_IDX", + columnNames = {"restaurant_id", "menu_id"} + ) +) +@Entity +public class RestaurantMenuMap extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, name = "restaurant_id") + @ManyToOne(fetch = FetchType.LAZY) + private Restaurant restaurant; + + @JoinColumn(nullable = false, name = "menu_id") + @ManyToOne(fetch = FetchType.LAZY) + private Menu menu; +} diff --git a/src/main/java/com/livable/server/entity/RestaurantReview.java b/src/main/java/com/livable/server/entity/RestaurantReview.java new file mode 100644 index 00000000..e01004eb --- /dev/null +++ b/src/main/java/com/livable/server/entity/RestaurantReview.java @@ -0,0 +1,30 @@ +package com.livable.server.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder +@Entity +public class RestaurantReview extends Review { + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Restaurant restaurant; + + @Column(nullable = false) + private Evaluation taste; + + @Column(nullable = false) + private Evaluation speed; + + @Column(nullable = false) + private Evaluation amount; + + @Column(nullable = false) + private Evaluation service; +} diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java new file mode 100644 index 00000000..9fc12457 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Review.java @@ -0,0 +1,41 @@ +package com.livable.server.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.DynamicInsert; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Inheritance(strategy = InheritanceType.JOINED) +@EntityListeners(AuditingEntityListener.class) +@Entity +@ToString +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String description; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/livable/server/entity/ReviewImage.java b/src/main/java/com/livable/server/entity/ReviewImage.java new file mode 100644 index 00000000..4af82a9a --- /dev/null +++ b/src/main/java/com/livable/server/entity/ReviewImage.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class ReviewImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Review review; + + @Column(nullable = false) + private String url; +} diff --git a/src/main/java/com/livable/server/entity/Role.java b/src/main/java/com/livable/server/entity/Role.java new file mode 100644 index 00000000..827bdd37 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Role.java @@ -0,0 +1,5 @@ +package com.livable.server.entity; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/com/livable/server/entity/Visitor.java b/src/main/java/com/livable/server/entity/Visitor.java new file mode 100644 index 00000000..7df113c0 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Visitor.java @@ -0,0 +1,27 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Visitor extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Invitation invitation; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String contact; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b137891..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b From ce184607f53dd1f9d9789c53b8c13be13127ac15 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:43:34 +0900 Subject: [PATCH 003/111] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=82=B4?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EC=99=84=EB=A3=8C=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Typo: ๋ถˆํ•„์š”ํ•œ ์ฃผ์„ ์ œ๊ฑฐ (#5) Review ํ…Œ์ด๋ธ” ์กฐ์ธ ์ „๋žต์œผ๋กœ ๋ถ„๋ฆฌ ์ „ ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ ์‚ญ์ œ --- .../server/entity/CafeteriaReview.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/main/java/com/livable/server/entity/CafeteriaReview.java b/src/main/java/com/livable/server/entity/CafeteriaReview.java index 154732b6..3b097100 100644 --- a/src/main/java/com/livable/server/entity/CafeteriaReview.java +++ b/src/main/java/com/livable/server/entity/CafeteriaReview.java @@ -17,25 +17,4 @@ public class CafeteriaReview extends Review { @ManyToOne(fetch = FetchType.LAZY) private Building building; - - -// @Id -// @GeneratedValue(strategy = GenerationType.IDENTITY) -// private Long id; -// -// @JoinColumn(nullable = false) -// @ManyToOne(fetch = FetchType.LAZY) -// private Building building; -// -// @JoinColumn(nullable = false) -// @ManyToOne(fetch = FetchType.LAZY) -// private Member member; -// -// @Enumerated(EnumType.STRING) -// @Column(nullable = false) -// private Evaluation taste; -// -// @Column(nullable = false) -// private String description; - } From 37130210c566ddd5475483e470e29a5c1dad26fa Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:23:32 +0900 Subject: [PATCH 004/111] =?UTF-8?q?JPA=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: Review ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ๋กœ ์ธํ•œ ๋ถˆํ•„์š”ํ•œ Image ํด๋ž˜์Šค ์‚ญ์ œ (#5) --- .../server/entity/CafeteriaReviewImage.java | 24 ------------------- .../server/entity/LunchBoxReviewImage.java | 24 ------------------- 2 files changed, 48 deletions(-) delete mode 100644 src/main/java/com/livable/server/entity/CafeteriaReviewImage.java delete mode 100644 src/main/java/com/livable/server/entity/LunchBoxReviewImage.java diff --git a/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java b/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java deleted file mode 100644 index 9ad4e926..00000000 --- a/src/main/java/com/livable/server/entity/CafeteriaReviewImage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.livable.server.entity; - -import lombok.*; - -import javax.persistence.*; - -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -@Entity -public class CafeteriaReviewImage extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(nullable = false) - @ManyToOne(fetch = FetchType.LAZY) - private CafeteriaReview cafeteriaReview; - - @Column(nullable = false) - private String url; -} diff --git a/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java b/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java deleted file mode 100644 index 69feb55b..00000000 --- a/src/main/java/com/livable/server/entity/LunchBoxReviewImage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.livable.server.entity; - -import lombok.*; - -import javax.persistence.*; - -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -@Entity -public class LunchBoxReviewImage extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(nullable = false) - @ManyToOne(fetch = FetchType.LAZY) - private LunchBoxReview lunchBoxReview; - - @Column(nullable = false) - private String url; -} From 56e02b2b3dd1e047bed601d878d02f348f5a090d Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Tue, 12 Sep 2023 15:01:31 +0900 Subject: [PATCH 005/111] =?UTF-8?q?Reference=20Id,=20Type=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EC=99=84=EB=A3=8C=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: Reference Id, Type ํ•„๋“œ ์‚ญ์ œ (#9) --- .../com/livable/server/entity/PointLog.java | 19 ++++--------------- .../livable/server/entity/ReferenceType.java | 8 -------- 2 files changed, 4 insertions(+), 23 deletions(-) delete mode 100644 src/main/java/com/livable/server/entity/ReferenceType.java diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 153d4189..86e1c0f0 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -8,14 +8,6 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) -@Table( - name = "point_log", - uniqueConstraints = - @UniqueConstraint( - name = "REFERENCE_UNIQUE_IDX", - columnNames = {"reference_id", "reference_type"} - ) -) @Entity public class PointLog extends BaseTimeEntity { @@ -27,17 +19,14 @@ public class PointLog extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Point point; + @JoinColumn(nullable = false, unique = true) + @ManyToOne(fetch = FetchType.LAZY) + private Review review; + @Enumerated(EnumType.STRING) @Column(nullable = false) private PointCode code; @Column(nullable = false) private Integer amount; - - @Column(nullable = false, name = "reference_id") - private Long referenceId; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, name = "reference_type") - private ReferenceType referenceType; } diff --git a/src/main/java/com/livable/server/entity/ReferenceType.java b/src/main/java/com/livable/server/entity/ReferenceType.java deleted file mode 100644 index 9f9ab817..00000000 --- a/src/main/java/com/livable/server/entity/ReferenceType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.livable.server.entity; - -public enum ReferenceType { - REVIEW, - CAFETERIA_REVIEW, - LUNCH_BOX_REVIEW, - ; -} From 3f97525dd19b58cf6165015c9f3e1aac1421fd2a Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:14:34 +0900 Subject: [PATCH 006/111] =?UTF-8?q?Modify:=20PointLog=EC=97=90=20unique=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=A0=9C=EA=B1=B0=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: PointLog์— unique ์˜ต์…˜ ์ œ๊ฑฐ (#9) --- src/main/java/com/livable/server/entity/PointLog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 86e1c0f0..0b0d9eb2 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -19,7 +19,7 @@ public class PointLog extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Point point; - @JoinColumn(nullable = false, unique = true) + @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) private Review review; From d381113b30479923216a3a8d710341ae2a967285 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 13 Sep 2023 21:58:31 +0900 Subject: [PATCH 007/111] =?UTF-8?q?=EA=B8=B0=ED=9A=8D=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ๊ธฐํš์— ๋”ฐ๋ฅธ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • (#9) --- src/main/java/com/livable/server/entity/Building.java | 9 +++++++++ .../com/livable/server/entity/BuildingRestaurantMap.java | 3 +++ src/main/java/com/livable/server/entity/Invitation.java | 3 +-- src/main/java/com/livable/server/entity/Member.java | 8 +++++++- src/main/java/com/livable/server/entity/Menu.java | 5 ++++- .../server/entity/{Category.java => MenuCategory.java} | 2 +- src/main/java/com/livable/server/entity/Restaurant.java | 9 ++++++++- .../com/livable/server/entity/RestaurantCategory.java | 8 ++++++++ src/main/java/com/livable/server/entity/Visitor.java | 4 ++++ 9 files changed, 45 insertions(+), 6 deletions(-) rename src/main/java/com/livable/server/entity/{Category.java => MenuCategory.java} (87%) create mode 100644 src/main/java/com/livable/server/entity/RestaurantCategory.java diff --git a/src/main/java/com/livable/server/entity/Building.java b/src/main/java/com/livable/server/entity/Building.java index 8553b7b6..f55087f6 100644 --- a/src/main/java/com/livable/server/entity/Building.java +++ b/src/main/java/com/livable/server/entity/Building.java @@ -43,4 +43,13 @@ public class Building extends BaseTimeEntity { @Column(nullable = false) private String latitude; + + @Column(nullable = false) + private String representativeImageUrl; + + @Column(nullable = false) + private String parkingCostInformation; + + @Column + private String subwayStation; } diff --git a/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java index d52d2fde..c0c5c41c 100644 --- a/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java +++ b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java @@ -31,6 +31,9 @@ public class BuildingRestaurantMap extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Restaurant restaurant; + @Column(nullable = false) + private Boolean inBuilding; + @Column(nullable = false) private Integer distance; } diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java index b1e07670..8671286e 100644 --- a/src/main/java/com/livable/server/entity/Invitation.java +++ b/src/main/java/com/livable/server/entity/Invitation.java @@ -26,8 +26,7 @@ public class Invitation extends BaseTimeEntity { private String description; @Column(nullable = false) - @Enumerated(EnumType.STRING) - private InvitationPurpose purpose; + private String purpose; @Column(nullable = false) private String officeName; diff --git a/src/main/java/com/livable/server/entity/Member.java b/src/main/java/com/livable/server/entity/Member.java index fb4d839e..bf246ae8 100644 --- a/src/main/java/com/livable/server/entity/Member.java +++ b/src/main/java/com/livable/server/entity/Member.java @@ -31,12 +31,18 @@ public class Member extends BaseTimeEntity { @Enumerated(EnumType.STRING) private Role role; + @Column(nullable = false) + private String profileImageUrl; + @Column - private String businessCardImage; + private String businessCardImageUrl; @Column(nullable = false, unique = true) private String contact; @Column(nullable = false) private String name; + + @Column(nullable = false, unique = true) + private String employeeNumber; } diff --git a/src/main/java/com/livable/server/entity/Menu.java b/src/main/java/com/livable/server/entity/Menu.java index 56b0ceb3..24ae90f8 100644 --- a/src/main/java/com/livable/server/entity/Menu.java +++ b/src/main/java/com/livable/server/entity/Menu.java @@ -17,8 +17,11 @@ public class Menu extends BaseTimeEntity { @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) - private Category category; + private MenuCategory menuCategory; @Column(nullable = false, unique = true) private String name; + + @Column(nullable = false) + private String representativeImageUrl; } diff --git a/src/main/java/com/livable/server/entity/Category.java b/src/main/java/com/livable/server/entity/MenuCategory.java similarity index 87% rename from src/main/java/com/livable/server/entity/Category.java rename to src/main/java/com/livable/server/entity/MenuCategory.java index e6c182da..ca4fd9ea 100644 --- a/src/main/java/com/livable/server/entity/Category.java +++ b/src/main/java/com/livable/server/entity/MenuCategory.java @@ -9,7 +9,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class Category extends BaseTimeEntity { +public class MenuCategory extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/livable/server/entity/Restaurant.java b/src/main/java/com/livable/server/entity/Restaurant.java index aa5bb2e9..fc77abb9 100644 --- a/src/main/java/com/livable/server/entity/Restaurant.java +++ b/src/main/java/com/livable/server/entity/Restaurant.java @@ -28,5 +28,12 @@ public class Restaurant extends BaseTimeEntity { private String restaurantUrl; @Column(nullable = false) - private String thumbnailImage; + private String thumbnailImageUrl; + + @Column(nullable = false) + private String representativeCategory; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RestaurantCategory restaurantCategory; } diff --git a/src/main/java/com/livable/server/entity/RestaurantCategory.java b/src/main/java/com/livable/server/entity/RestaurantCategory.java new file mode 100644 index 00000000..0765f4bd --- /dev/null +++ b/src/main/java/com/livable/server/entity/RestaurantCategory.java @@ -0,0 +1,8 @@ +package com.livable.server.entity; + +public enum RestaurantCategory { + + RESTAURANT, + CAFE, + ; +} diff --git a/src/main/java/com/livable/server/entity/Visitor.java b/src/main/java/com/livable/server/entity/Visitor.java index 7df113c0..9e397149 100644 --- a/src/main/java/com/livable/server/entity/Visitor.java +++ b/src/main/java/com/livable/server/entity/Visitor.java @@ -3,6 +3,7 @@ import lombok.*; import javax.persistence.*; +import java.time.LocalDateTime; @Getter @Builder @@ -24,4 +25,7 @@ public class Visitor extends BaseTimeEntity { @Column(nullable = false) private String contact; + + @Column + private LocalDateTime firstVisitedTime; } From 68a4464e7cbff6c298186e1db05502923d31aeeb Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Thu, 14 Sep 2023 13:21:34 +0900 Subject: [PATCH 008/111] =?UTF-8?q?QueryDsl=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: build.gradle ํŒŒ์ผ์— QueryDsl ์„ค์ • ์ถ”๊ฐ€ (#12) --- build.gradle | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/build.gradle b/build.gradle index 1f970eaf..698962e2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,15 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + plugins { id 'java' id 'org.springframework.boot' version '2.7.15' id 'io.spring.dependency-management' version '1.0.15.RELEASE' id 'org.asciidoctor.jvm.convert' version '3.3.2' + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'com.livable' @@ -30,10 +37,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' } @@ -47,3 +56,27 @@ tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test } + +/* QueryDSL setting start */ +def querydslDir = "$buildDir/generated/querydsl" + +querydsl { + jpa = true + querydslSourcesDir = querydslDir +} + +sourceSets { + main.java.srcDir querydslDir +} + +configurations { + complieOnly { + extendsFrom annotationProcessor + } + querydsl.extendsFrom compileClasspath +} + +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} +/* QueryDSL setting end */ From b5bb0fc5c7fe90a8c9d4d53f0acee8daee21a0eb Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:58:36 +0900 Subject: [PATCH 009/111] =?UTF-8?q?=EA=B8=B0=ED=9A=8D=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ๊ธฐํš์— ๋”ฐ๋ฅธ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • (#9) --- .../com/livable/server/entity/Building.java | 1 - .../com/livable/server/entity/Invitation.java | 1 + .../com/livable/server/entity/Member.java | 9 +++-- .../com/livable/server/entity/PointLog.java | 1 + .../com/livable/server/entity/Restaurant.java | 8 ++--- .../com/livable/server/entity/Review.java | 9 +++-- .../livable/server/entity/ReviewMenuMap.java | 33 +++++++++++++++++++ 7 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/livable/server/entity/ReviewMenuMap.java diff --git a/src/main/java/com/livable/server/entity/Building.java b/src/main/java/com/livable/server/entity/Building.java index f55087f6..fe6182f9 100644 --- a/src/main/java/com/livable/server/entity/Building.java +++ b/src/main/java/com/livable/server/entity/Building.java @@ -20,7 +20,6 @@ public class Building extends BaseTimeEntity { private Long id; @Column(nullable = false) - @ColumnDefault("true") private Boolean hasCafeteria; @Column(nullable = false) diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java index 8671286e..796a72e1 100644 --- a/src/main/java/com/livable/server/entity/Invitation.java +++ b/src/main/java/com/livable/server/entity/Invitation.java @@ -23,6 +23,7 @@ public class Invitation extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Member member; + @Column private String description; @Column(nullable = false) diff --git a/src/main/java/com/livable/server/entity/Member.java b/src/main/java/com/livable/server/entity/Member.java index bf246ae8..b0706ee9 100644 --- a/src/main/java/com/livable/server/entity/Member.java +++ b/src/main/java/com/livable/server/entity/Member.java @@ -20,17 +20,16 @@ public class Member extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Company company; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String password; - @Column(nullable = false) - @ColumnDefault("'USER'") - @Enumerated(EnumType.STRING) - private Role role; - @Column(nullable = false) private String profileImageUrl; diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 0b0d9eb2..9137f84c 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -3,6 +3,7 @@ import lombok.*; import javax.persistence.*; +import java.time.LocalDate; @Getter @Builder diff --git a/src/main/java/com/livable/server/entity/Restaurant.java b/src/main/java/com/livable/server/entity/Restaurant.java index fc77abb9..3e4e1a28 100644 --- a/src/main/java/com/livable/server/entity/Restaurant.java +++ b/src/main/java/com/livable/server/entity/Restaurant.java @@ -15,6 +15,10 @@ public class Restaurant extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RestaurantCategory restaurantCategory; + @Column(nullable = false) private String name; @@ -32,8 +36,4 @@ public class Restaurant extends BaseTimeEntity { @Column(nullable = false) private String representativeCategory; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private RestaurantCategory restaurantCategory; } diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java index 9fc12457..8567da1a 100644 --- a/src/main/java/com/livable/server/entity/Review.java +++ b/src/main/java/com/livable/server/entity/Review.java @@ -24,13 +24,16 @@ public class Review { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private String description; - @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) private Member member; + @Column(nullable = false) + private String description; + + @Column(nullable = false) + private String selectedDishes; + @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; diff --git a/src/main/java/com/livable/server/entity/ReviewMenuMap.java b/src/main/java/com/livable/server/entity/ReviewMenuMap.java new file mode 100644 index 00000000..df3fbf09 --- /dev/null +++ b/src/main/java/com/livable/server/entity/ReviewMenuMap.java @@ -0,0 +1,33 @@ +package com.livable.server.entity; + +import lombok.*; + +import javax.persistence.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "review_menu_map", + uniqueConstraints = + @UniqueConstraint( + name = "REVIEW_MENU_UNIQUE_IDX", + columnNames = {"review_Id", "menu_id"} + ) +) +@Entity +public class ReviewMenuMap extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Review review; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Menu menu; +} From 9707fa135d65d0b9f944870dbaa6103f64fe2250 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Fri, 15 Sep 2023 19:20:49 +0900 Subject: [PATCH 010/111] =?UTF-8?q?@Enumerated=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ๊ธฐํš์— ๋”ฐ๋ฅธ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • (#9) * Modify: @Enumerated ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ (#9) Enum ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•„๋“œ์— ๋ˆ„๋ฝ๋œ ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ --- src/main/java/com/livable/server/entity/CafeteriaReview.java | 2 ++ src/main/java/com/livable/server/entity/RestaurantReview.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/livable/server/entity/CafeteriaReview.java b/src/main/java/com/livable/server/entity/CafeteriaReview.java index 3b097100..0f60538e 100644 --- a/src/main/java/com/livable/server/entity/CafeteriaReview.java +++ b/src/main/java/com/livable/server/entity/CafeteriaReview.java @@ -13,6 +13,8 @@ @ToString(callSuper = true) public class CafeteriaReview extends Review { + @Enumerated(EnumType.STRING) + @Column(nullable = false) private Evaluation taste; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/livable/server/entity/RestaurantReview.java b/src/main/java/com/livable/server/entity/RestaurantReview.java index e01004eb..0fb349bf 100644 --- a/src/main/java/com/livable/server/entity/RestaurantReview.java +++ b/src/main/java/com/livable/server/entity/RestaurantReview.java @@ -16,15 +16,19 @@ public class RestaurantReview extends Review { @ManyToOne(fetch = FetchType.LAZY) private Restaurant restaurant; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Evaluation taste; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Evaluation speed; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Evaluation amount; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Evaluation service; } From 9e369a2c37a813c826ccfda14334557a8dc58c99 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 15 Sep 2023 19:41:23 +0900 Subject: [PATCH 011/111] =?UTF-8?q?=EA=B0=9C=EB=B0=9C,=20=EC=9A=B4?= =?UTF-8?q?=EC=98=81,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20=EC=99=84=EB=A3=8C=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ๊ฐœ๋ฐœ, ์šด์˜, ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋ถ„๋ฆฌ (#17) --- .gitignore | 2 ++ src/main/java/com/livable/server/entity/Menu.java | 2 +- src/main/resources/application.yml | 7 +++++++ src/test/resources/application.yml | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/application.yml diff --git a/.gitignore b/.gitignore index c2065bc2..34047078 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +application-dev.yml +application-prod.yml ### NetBeans ### /nbproject/private/ diff --git a/src/main/java/com/livable/server/entity/Menu.java b/src/main/java/com/livable/server/entity/Menu.java index 24ae90f8..baa1c61a 100644 --- a/src/main/java/com/livable/server/entity/Menu.java +++ b/src/main/java/com/livable/server/entity/Menu.java @@ -22,6 +22,6 @@ public class Menu extends BaseTimeEntity { @Column(nullable = false, unique = true) private String name; - @Column(nullable = false) + @Column(nullable = false, length = 1000) private String representativeImageUrl; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e69de29b..35663b09 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + profiles: + default: dev + jpa: + properties: + hibernate: + default_batch_fetch_size: 100 \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..3a0d2226 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + show_sql: true + format_sql: true \ No newline at end of file From 27ee95a3e35fb8bfa67a01a95cd3cb9e5ddf7426 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Sun, 17 Sep 2023 02:16:04 +0900 Subject: [PATCH 012/111] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=84=A4=EA=B3=84=20=EC=99=84=EB=A3=8C=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: ํŒจํ‚ค์ง€ ๋ฐ ๊ณตํ†ต ํด๋ž˜์Šค ์ถ”๊ฐ€ (#19) ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ์„ค๊ณ„ ๋ฐ ๊ณตํ†ต ์‘๋‹ต, ์˜ˆ์™ธ ํ•ธ๋“ค๋ง ํด๋ž˜์Šค ์ถ”๊ฐ€ * Style: ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ฝ”๋“œ ํฌ๋งทํŒ… (#19) --- .../server/{ => core}/config/JpaConfig.java | 2 +- .../server/core/exception/ErrorCode.java | 10 ++++ .../exception/GlobalExceptionHandler.java | 48 ++++++++++++++++ .../exception/GlobalRuntimeException.java | 14 +++++ .../server/core/response/ApiResponse.java | 56 +++++++++++++++++++ .../livable/server/entity/BaseTimeEntity.java | 2 - .../com/livable/server/entity/Building.java | 3 - .../server/entity/CafeteriaReview.java | 1 + .../livable/server/entity/CommonPlace.java | 3 + .../com/livable/server/entity/Evaluation.java | 3 +- .../com/livable/server/entity/Invitation.java | 3 - .../server/entity/InvitationPurpose.java | 3 +- .../livable/server/entity/LunchBoxReview.java | 6 +- .../com/livable/server/entity/Member.java | 1 - .../server/entity/MenuChoiceResult.java | 1 - .../java/com/livable/server/entity/Point.java | 1 - .../com/livable/server/entity/PointCode.java | 3 +- .../com/livable/server/entity/PointLog.java | 1 - .../livable/server/entity/Reservation.java | 1 - .../server/entity/RestaurantCategory.java | 3 +- .../server/entity/RestaurantReview.java | 5 +- .../com/livable/server/entity/Review.java | 1 - .../java/com/livable/server/entity/Role.java | 3 +- .../server/invitation/controller/.gitkeep | 0 .../livable/server/invitation/domain/.gitkeep | 0 .../livable/server/invitation/dto/.gitkeep | 0 .../server/invitation/repository/.gitkeep | 0 .../server/invitation/service/.gitkeep | 0 .../livable/server/member/controller/.gitkeep | 0 .../com/livable/server/member/dto/.gitkeep | 0 .../livable/server/member/exception/.gitkeep | 0 .../livable/server/member/repository/.gitkeep | 0 .../livable/server/member/service/.gitkeep | 0 .../livable/server/menu/controller/.gitkeep | 0 .../java/com/livable/server/menu/dto/.gitkeep | 0 .../livable/server/menu/exception/.gitkeep | 0 .../livable/server/menu/repository/.gitkeep | 0 .../com/livable/server/menu/service/.gitkeep | 0 .../livable/server/point/controller/.gitkeep | 0 .../com/livable/server/point/dto/.gitkeep | 0 .../livable/server/point/exception/.gitkeep | 0 .../livable/server/point/repository/.gitkeep | 0 .../com/livable/server/point/service/.gitkeep | 0 .../server/reservation/controller/.gitkeep | 0 .../server/reservation/domain/.gitkeep | 0 .../livable/server/reservation/dto/.gitkeep | 0 .../server/reservation/repository/.gitkeep | 0 .../server/reservation/service/.gitkeep | 0 .../server/restaurant/controller/.gitkeep | 0 .../livable/server/restaurant/domain/.gitkeep | 0 .../livable/server/restaurant/dto/.gitkeep | 0 .../server/restaurant/repository/.gitkeep | 0 .../server/restaurant/service/.gitkeep | 0 .../livable/server/review/controller/.gitkeep | 0 .../com/livable/server/review/domain/.gitkeep | 0 .../com/livable/server/review/dto/.gitkeep | 0 .../livable/server/review/repository/.gitkeep | 0 .../livable/server/review/service/.gitkeep | 0 .../server/visitation/controller/.gitkeep | 0 .../livable/server/visitation/domain/.gitkeep | 0 .../livable/server/visitation/dto/.gitkeep | 0 .../server/visitation/repository/.gitkeep | 0 .../server/visitation/service/.gitkeep | 0 .../server/LivableServerApplicationTests.java | 1 + 64 files changed, 148 insertions(+), 27 deletions(-) rename src/main/java/com/livable/server/{ => core}/config/JpaConfig.java (83%) create mode 100644 src/main/java/com/livable/server/core/exception/ErrorCode.java create mode 100644 src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java create mode 100644 src/main/java/com/livable/server/core/response/ApiResponse.java create mode 100644 src/main/java/com/livable/server/invitation/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/service/.gitkeep create mode 100644 src/main/java/com/livable/server/member/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/member/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/member/exception/.gitkeep create mode 100644 src/main/java/com/livable/server/member/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/member/service/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/exception/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/service/.gitkeep create mode 100644 src/main/java/com/livable/server/point/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/point/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/point/exception/.gitkeep create mode 100644 src/main/java/com/livable/server/point/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/point/service/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/service/.gitkeep create mode 100644 src/main/java/com/livable/server/restaurant/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/restaurant/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/restaurant/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/restaurant/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/restaurant/service/.gitkeep create mode 100644 src/main/java/com/livable/server/review/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/review/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/review/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/review/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/review/service/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/service/.gitkeep diff --git a/src/main/java/com/livable/server/config/JpaConfig.java b/src/main/java/com/livable/server/core/config/JpaConfig.java similarity index 83% rename from src/main/java/com/livable/server/config/JpaConfig.java rename to src/main/java/com/livable/server/core/config/JpaConfig.java index 906573a7..8bd76345 100644 --- a/src/main/java/com/livable/server/config/JpaConfig.java +++ b/src/main/java/com/livable/server/core/config/JpaConfig.java @@ -1,4 +1,4 @@ -package com.livable.server.config; +package com.livable.server.core.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/com/livable/server/core/exception/ErrorCode.java b/src/main/java/com/livable/server/core/exception/ErrorCode.java new file mode 100644 index 00000000..cadd9c54 --- /dev/null +++ b/src/main/java/com/livable/server/core/exception/ErrorCode.java @@ -0,0 +1,10 @@ +package com.livable.server.core.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getMessage(); + + HttpStatus getHttpStatus(); +} diff --git a/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..3f34db59 --- /dev/null +++ b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package com.livable.server.core.exception; + +import com.livable.server.core.response.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity methodMethodArgumentTypeMismatchExceptionHandle(MethodArgumentTypeMismatchException e) { + return ApiResponse.error(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(BindException.class) + public ResponseEntity bindException(BindException e) { + log.error("bindException", e); + + return ApiResponse.error(e.getBindingResult().getFieldError().getDefaultMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(GlobalRuntimeException.class) + public ResponseEntity globalRuntimeExceptionHandle(GlobalRuntimeException e) { + log.error("globalRuntimeExceptionHandle", e); + + return ApiResponse.error(e.getErrorCode()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity runtimeExceptionHandle(RuntimeException e) { + log.error("runtimeExceptionHandle", e); + + return ApiResponse.error(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity exceptionHandle(Exception e) { + log.error("exceptionHandle", e); + + return ApiResponse.error(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java new file mode 100644 index 00000000..b726228a --- /dev/null +++ b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java @@ -0,0 +1,14 @@ +package com.livable.server.core.exception; + +import lombok.Getter; + +@Getter +public class GlobalRuntimeException extends RuntimeException { + + private final ErrorCode errorCode; + + public GlobalRuntimeException(ErrorCode errorCode) { + super(); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/livable/server/core/response/ApiResponse.java b/src/main/java/com/livable/server/core/response/ApiResponse.java new file mode 100644 index 00000000..eeeafc81 --- /dev/null +++ b/src/main/java/com/livable/server/core/response/ApiResponse.java @@ -0,0 +1,56 @@ +package com.livable.server.core.response; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponse { + + public static ResponseEntity> success(@NonNull HttpStatus httpStatus) { + return ResponseEntity.status(httpStatus) + .build(); + } + + public static ResponseEntity> success(@NonNull T data, @NonNull HttpStatus httpStatus) { + return ResponseEntity.status(httpStatus) + .body(Success.of(data)); + } + + public static ResponseEntity error(@NonNull String message, @NonNull HttpStatus httpStatus) { + return ResponseEntity.status(httpStatus) + .body(Error.of(message)); + } + + public static ResponseEntity error(@NonNull ErrorCode errorCode) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(Error.of(errorCode.getMessage())); + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Error { + + private String message; + + public static Error of(@NonNull String errorMessage) { + return new Error(errorMessage); + } + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Success { + + private T data; + + public static Success of(@NonNull T data) { + return new Success<>(data); + } + } +} diff --git a/src/main/java/com/livable/server/entity/BaseTimeEntity.java b/src/main/java/com/livable/server/entity/BaseTimeEntity.java index 7f00ea0b..72f35efb 100644 --- a/src/main/java/com/livable/server/entity/BaseTimeEntity.java +++ b/src/main/java/com/livable/server/entity/BaseTimeEntity.java @@ -2,12 +2,10 @@ import lombok.Getter; -import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; import java.time.LocalDateTime; diff --git a/src/main/java/com/livable/server/entity/Building.java b/src/main/java/com/livable/server/entity/Building.java index fe6182f9..be0eafaa 100644 --- a/src/main/java/com/livable/server/entity/Building.java +++ b/src/main/java/com/livable/server/entity/Building.java @@ -1,11 +1,8 @@ package com.livable.server.entity; import lombok.*; -import org.hibernate.annotations.ColumnDefault; -import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; -import java.time.LocalDateTime; import java.time.LocalTime; @Getter diff --git a/src/main/java/com/livable/server/entity/CafeteriaReview.java b/src/main/java/com/livable/server/entity/CafeteriaReview.java index 0f60538e..8333eb18 100644 --- a/src/main/java/com/livable/server/entity/CafeteriaReview.java +++ b/src/main/java/com/livable/server/entity/CafeteriaReview.java @@ -17,6 +17,7 @@ public class CafeteriaReview extends Review { @Column(nullable = false) private Evaluation taste; + @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) private Building building; } diff --git a/src/main/java/com/livable/server/entity/CommonPlace.java b/src/main/java/com/livable/server/entity/CommonPlace.java index 741c52d7..5ee2aa00 100644 --- a/src/main/java/com/livable/server/entity/CommonPlace.java +++ b/src/main/java/com/livable/server/entity/CommonPlace.java @@ -19,9 +19,12 @@ public class CommonPlace extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Building building; + @Column(nullable = false) private String name; + @Column(nullable = false) private String floor; + @Column(nullable = false) private String roomNumber; } diff --git a/src/main/java/com/livable/server/entity/Evaluation.java b/src/main/java/com/livable/server/entity/Evaluation.java index a2885277..fdbdfec2 100644 --- a/src/main/java/com/livable/server/entity/Evaluation.java +++ b/src/main/java/com/livable/server/entity/Evaluation.java @@ -3,6 +3,5 @@ public enum Evaluation { GOOD, - BAD, - ; + BAD; } diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java index 796a72e1..ff8d6a33 100644 --- a/src/main/java/com/livable/server/entity/Invitation.java +++ b/src/main/java/com/livable/server/entity/Invitation.java @@ -1,11 +1,9 @@ package com.livable.server.entity; import lombok.*; -import org.hibernate.annotations.ColumnDefault; import javax.persistence.*; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; @Getter @@ -43,5 +41,4 @@ public class Invitation extends BaseTimeEntity { @Column(nullable = false) private LocalTime endTime; - } diff --git a/src/main/java/com/livable/server/entity/InvitationPurpose.java b/src/main/java/com/livable/server/entity/InvitationPurpose.java index ff58ffc7..4cafa97b 100644 --- a/src/main/java/com/livable/server/entity/InvitationPurpose.java +++ b/src/main/java/com/livable/server/entity/InvitationPurpose.java @@ -7,6 +7,5 @@ public enum InvitationPurpose { PERIOD_WORK, SEMINAR, AFTER_SERVICE, - ETC, - ; + ETC; } diff --git a/src/main/java/com/livable/server/entity/LunchBoxReview.java b/src/main/java/com/livable/server/entity/LunchBoxReview.java index 6c6f158a..a04f7bc8 100644 --- a/src/main/java/com/livable/server/entity/LunchBoxReview.java +++ b/src/main/java/com/livable/server/entity/LunchBoxReview.java @@ -1,9 +1,11 @@ package com.livable.server.entity; -import lombok.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import javax.persistence.*; +import javax.persistence.Entity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/livable/server/entity/Member.java b/src/main/java/com/livable/server/entity/Member.java index b0706ee9..32511131 100644 --- a/src/main/java/com/livable/server/entity/Member.java +++ b/src/main/java/com/livable/server/entity/Member.java @@ -1,7 +1,6 @@ package com.livable.server.entity; import lombok.*; -import org.hibernate.annotations.ColumnDefault; import javax.persistence.*; diff --git a/src/main/java/com/livable/server/entity/MenuChoiceResult.java b/src/main/java/com/livable/server/entity/MenuChoiceResult.java index 70ca60e9..eed7411a 100644 --- a/src/main/java/com/livable/server/entity/MenuChoiceResult.java +++ b/src/main/java/com/livable/server/entity/MenuChoiceResult.java @@ -1,7 +1,6 @@ package com.livable.server.entity; import lombok.*; -import org.hibernate.annotations.ColumnDefault; import javax.persistence.*; import java.time.LocalDate; diff --git a/src/main/java/com/livable/server/entity/Point.java b/src/main/java/com/livable/server/entity/Point.java index 6ab3d481..1dfdf0a8 100644 --- a/src/main/java/com/livable/server/entity/Point.java +++ b/src/main/java/com/livable/server/entity/Point.java @@ -1,7 +1,6 @@ package com.livable.server.entity; import lombok.*; -import org.hibernate.annotations.ColumnDefault; import javax.persistence.*; diff --git a/src/main/java/com/livable/server/entity/PointCode.java b/src/main/java/com/livable/server/entity/PointCode.java index 7b82116b..3c419a72 100644 --- a/src/main/java/com/livable/server/entity/PointCode.java +++ b/src/main/java/com/livable/server/entity/PointCode.java @@ -13,8 +13,7 @@ public enum PointCode { PA03("์˜ค์ ์™„ 7์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), PA04("์˜ค์ ์™„ 14์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), PA05("์˜ค์ ์™„ 21์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), - PM00("์ œํœด ์นดํŽ˜ ๋ฉ”๋‰ด ํ• ์ธ์— ๋Œ€ํ•œ ํฌ์ธํŠธ ์‚ฌ์šฉ"), - ; + PM00("์ œํœด ์นดํŽ˜ ๋ฉ”๋‰ด ํ• ์ธ์— ๋Œ€ํ•œ ํฌ์ธํŠธ ์‚ฌ์šฉ"); private final String description; } diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 9137f84c..0b0d9eb2 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -3,7 +3,6 @@ import lombok.*; import javax.persistence.*; -import java.time.LocalDate; @Getter @Builder diff --git a/src/main/java/com/livable/server/entity/Reservation.java b/src/main/java/com/livable/server/entity/Reservation.java index bfb71908..ec23e5d5 100644 --- a/src/main/java/com/livable/server/entity/Reservation.java +++ b/src/main/java/com/livable/server/entity/Reservation.java @@ -4,7 +4,6 @@ import javax.persistence.*; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; @Getter diff --git a/src/main/java/com/livable/server/entity/RestaurantCategory.java b/src/main/java/com/livable/server/entity/RestaurantCategory.java index 0765f4bd..b98fca14 100644 --- a/src/main/java/com/livable/server/entity/RestaurantCategory.java +++ b/src/main/java/com/livable/server/entity/RestaurantCategory.java @@ -3,6 +3,5 @@ public enum RestaurantCategory { RESTAURANT, - CAFE, - ; + CAFE; } diff --git a/src/main/java/com/livable/server/entity/RestaurantReview.java b/src/main/java/com/livable/server/entity/RestaurantReview.java index 0fb349bf..40a968bb 100644 --- a/src/main/java/com/livable/server/entity/RestaurantReview.java +++ b/src/main/java/com/livable/server/entity/RestaurantReview.java @@ -1,6 +1,9 @@ package com.livable.server.entity; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import javax.persistence.*; diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java index 8567da1a..00afb006 100644 --- a/src/main/java/com/livable/server/entity/Review.java +++ b/src/main/java/com/livable/server/entity/Review.java @@ -2,7 +2,6 @@ import lombok.*; import lombok.experimental.SuperBuilder; -import org.hibernate.annotations.DynamicInsert; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; diff --git a/src/main/java/com/livable/server/entity/Role.java b/src/main/java/com/livable/server/entity/Role.java index 827bdd37..307a887f 100644 --- a/src/main/java/com/livable/server/entity/Role.java +++ b/src/main/java/com/livable/server/entity/Role.java @@ -1,5 +1,6 @@ package com.livable.server.entity; public enum Role { - ADMIN, USER + ADMIN, + USER; } diff --git a/src/main/java/com/livable/server/invitation/controller/.gitkeep b/src/main/java/com/livable/server/invitation/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/invitation/domain/.gitkeep b/src/main/java/com/livable/server/invitation/domain/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/invitation/dto/.gitkeep b/src/main/java/com/livable/server/invitation/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/invitation/repository/.gitkeep b/src/main/java/com/livable/server/invitation/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/invitation/service/.gitkeep b/src/main/java/com/livable/server/invitation/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/member/controller/.gitkeep b/src/main/java/com/livable/server/member/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/member/dto/.gitkeep b/src/main/java/com/livable/server/member/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/member/exception/.gitkeep b/src/main/java/com/livable/server/member/exception/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/member/repository/.gitkeep b/src/main/java/com/livable/server/member/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/member/service/.gitkeep b/src/main/java/com/livable/server/member/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/menu/controller/.gitkeep b/src/main/java/com/livable/server/menu/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/menu/dto/.gitkeep b/src/main/java/com/livable/server/menu/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/menu/exception/.gitkeep b/src/main/java/com/livable/server/menu/exception/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/menu/repository/.gitkeep b/src/main/java/com/livable/server/menu/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/menu/service/.gitkeep b/src/main/java/com/livable/server/menu/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/point/controller/.gitkeep b/src/main/java/com/livable/server/point/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/point/dto/.gitkeep b/src/main/java/com/livable/server/point/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/point/exception/.gitkeep b/src/main/java/com/livable/server/point/exception/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/point/repository/.gitkeep b/src/main/java/com/livable/server/point/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/point/service/.gitkeep b/src/main/java/com/livable/server/point/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/reservation/controller/.gitkeep b/src/main/java/com/livable/server/reservation/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/reservation/domain/.gitkeep b/src/main/java/com/livable/server/reservation/domain/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/reservation/dto/.gitkeep b/src/main/java/com/livable/server/reservation/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/reservation/repository/.gitkeep b/src/main/java/com/livable/server/reservation/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/reservation/service/.gitkeep b/src/main/java/com/livable/server/reservation/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/restaurant/controller/.gitkeep b/src/main/java/com/livable/server/restaurant/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/restaurant/domain/.gitkeep b/src/main/java/com/livable/server/restaurant/domain/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/restaurant/dto/.gitkeep b/src/main/java/com/livable/server/restaurant/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/restaurant/repository/.gitkeep b/src/main/java/com/livable/server/restaurant/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/restaurant/service/.gitkeep b/src/main/java/com/livable/server/restaurant/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/review/controller/.gitkeep b/src/main/java/com/livable/server/review/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/review/domain/.gitkeep b/src/main/java/com/livable/server/review/domain/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/review/dto/.gitkeep b/src/main/java/com/livable/server/review/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/review/repository/.gitkeep b/src/main/java/com/livable/server/review/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/review/service/.gitkeep b/src/main/java/com/livable/server/review/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/visitation/controller/.gitkeep b/src/main/java/com/livable/server/visitation/controller/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/visitation/domain/.gitkeep b/src/main/java/com/livable/server/visitation/domain/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/visitation/dto/.gitkeep b/src/main/java/com/livable/server/visitation/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/visitation/repository/.gitkeep b/src/main/java/com/livable/server/visitation/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/livable/server/visitation/service/.gitkeep b/src/main/java/com/livable/server/visitation/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/com/livable/server/LivableServerApplicationTests.java b/src/test/java/com/livable/server/LivableServerApplicationTests.java index 6a40278c..52bd189e 100644 --- a/src/test/java/com/livable/server/LivableServerApplicationTests.java +++ b/src/test/java/com/livable/server/LivableServerApplicationTests.java @@ -1,5 +1,6 @@ package com.livable.server; +import com.livable.server.core.response.ApiResponse; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; From 9ad2b912dd962ea837170f7738b6ae28fb16a43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Mon, 18 Sep 2023 19:48:20 +0900 Subject: [PATCH 013/111] =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Style: ๋”๋ฏธ ํŒŒ์ผ ์‚ญ์ œ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ QueryDSL ๋กœ์ง ๊ตฌํ˜„ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต DTO ๊ตฌํ˜„ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๊ตฌํ˜„ (#23) * Feat: ์ตœ์‹  ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#23) * Modify: ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ์— ํŠธ๋žœ์žญ์…˜ ์„ค์ • (#23) * Modify: ์ตœ์‹  ๋ฆฌ๋ทฐ ์‘๋‹ต์— ํ‰๊ฐ€์š”์†Œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (#27) --- .../livable/server/review/controller/.gitkeep | 0 .../RestaurantReviewController.java | 34 +++++++++ .../com/livable/server/review/dto/.gitkeep | 0 .../review/dto/RestaurantReviewResponse.java | 32 +++++++++ .../livable/server/review/repository/.gitkeep | 0 .../RestaurantReviewCustomRepository.java | 10 +++ .../RestaurantReviewCustomRepositoryImpl.java | 65 +++++++++++++++++ .../RestaurantReviewRepository.java | 8 +++ .../livable/server/review/service/.gitkeep | 0 .../service/RestaurantReviewService.java | 21 ++++++ .../RestaurantReviewControllerTest.java | 72 +++++++++++++++++++ .../service/RestaurantReviewServiceTest.java | 72 +++++++++++++++++++ 12 files changed, 314 insertions(+) delete mode 100644 src/main/java/com/livable/server/review/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/review/controller/RestaurantReviewController.java delete mode 100644 src/main/java/com/livable/server/review/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java delete mode 100644 src/main/java/com/livable/server/review/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java create mode 100644 src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java create mode 100644 src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java delete mode 100644 src/main/java/com/livable/server/review/service/.gitkeep create mode 100644 src/main/java/com/livable/server/review/service/RestaurantReviewService.java create mode 100644 src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java create mode 100644 src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java diff --git a/src/main/java/com/livable/server/review/controller/.gitkeep b/src/main/java/com/livable/server/review/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java new file mode 100644 index 00000000..dbb3ba81 --- /dev/null +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -0,0 +1,34 @@ +package com.livable.server.review.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.service.RestaurantReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/reviews") +public class RestaurantReviewController { + + private final RestaurantReviewService restaurantReviewService; + + @GetMapping("/buildings/{buildingId}") + public ResponseEntity>> list( + @PathVariable Long buildingId, + @PageableDefault Pageable pageable) { + + Page list = + restaurantReviewService.getAllList(buildingId, pageable); + + return ApiResponse.success(list, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/review/dto/.gitkeep b/src/main/java/com/livable/server/review/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java new file mode 100644 index 00000000..52ec6b5d --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -0,0 +1,32 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.Evaluation; +import lombok.*; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class RestaurantReviewResponse { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ListDTO { + + private Long reviewId; + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private Long restaurantId; + private String restaurantName; + + private Long memberId; + private String memberName; + } +} diff --git a/src/main/java/com/livable/server/review/repository/.gitkeep b/src/main/java/com/livable/server/review/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java new file mode 100644 index 00000000..5f4c298f --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java @@ -0,0 +1,10 @@ +package com.livable.server.review.repository; + +import com.livable.server.review.dto.RestaurantReviewResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface RestaurantReviewCustomRepository { + + Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable); +} diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java new file mode 100644 index 00000000..2f6cbba8 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.livable.server.review.repository; + +import com.livable.server.entity.*; +import com.livable.server.review.dto.RestaurantReviewResponse; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@RequiredArgsConstructor +public class RestaurantReviewCustomRepositoryImpl implements RestaurantReviewCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable) { + + final QReview review = QReview.review; + final QRestaurantReview restaurantReview = QRestaurantReview.restaurantReview; + final QMember member = QMember.member; + final QRestaurant restaurant = QRestaurant.restaurant; + final QBuildingRestaurantMap buildingRestaurantMap = QBuildingRestaurantMap.buildingRestaurantMap; + + JPAQuery query = queryFactory + .select(Projections.constructor(RestaurantReviewResponse.ListDTO.class, + review.id, + review.createdAt, + review.description, + restaurantReview.taste, + restaurantReview.amount, + restaurantReview.service, + restaurantReview.speed, + restaurantReview.restaurant.id, + restaurant.name, + review.member.id, + member.name + )) + .from(review) + .innerJoin(restaurantReview).on(review.id.eq(restaurantReview.id)) + .innerJoin(member).on(review.member.id.eq(member.id)) + .innerJoin(restaurant).on(restaurantReview.restaurant.id.eq(restaurant.id)) + .where(restaurantReview.restaurant.id.in( + JPAExpressions + .select(buildingRestaurantMap.restaurant.id) + .from(buildingRestaurantMap) + .where(buildingRestaurantMap.building.id.eq(buildingId)) + )) + .orderBy(review.createdAt.desc()); + + List content = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchJoin().fetch(); + + long total = query.fetchCount(); + + return new PageImpl<>(content, pageable, total); + } +} diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java new file mode 100644 index 00000000..79425fca --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java @@ -0,0 +1,8 @@ +package com.livable.server.review.repository; + +import com.livable.server.entity.RestaurantReview; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantReviewRepository extends JpaRepository, RestaurantReviewCustomRepository { + +} diff --git a/src/main/java/com/livable/server/review/service/.gitkeep b/src/main/java/com/livable/server/review/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java new file mode 100644 index 00000000..c89df010 --- /dev/null +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -0,0 +1,21 @@ +package com.livable.server.review.service; + +import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.repository.RestaurantReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class RestaurantReviewService { + + private final RestaurantReviewRepository restaurantReviewRepository; + + @Transactional(readOnly = true) + public Page getAllList(Long buildingId, Pageable pageable) { + return restaurantReviewRepository.findRestaurantReviewByBuildingId(buildingId, pageable); + } +} diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java new file mode 100644 index 00000000..c823a1d6 --- /dev/null +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -0,0 +1,72 @@ +package com.livable.server.review.controller; + +import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.service.RestaurantReviewService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +@WebMvcTest(controllers = RestaurantReviewController.class) +class RestaurantReviewControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private RestaurantReviewService restaurantReviewService; + + @Nested + @DisplayName("๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class list { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/buildings/1"; + + List mockList = List.of( + RestaurantReviewResponse.ListDTO.builder().reviewId(1L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(2L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(3L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(4L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(5L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(6L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(7L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(8L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(9L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(10L).build() + ); + Pageable pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(mockList, pageable, 1); + + Mockito.when(restaurantReviewService + .getAllList(ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class))) + .thenReturn(mockPage); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.content").isArray()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java new file mode 100644 index 00000000..0acdf605 --- /dev/null +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -0,0 +1,72 @@ +package com.livable.server.review.service; + +import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.repository.RestaurantReviewRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class RestaurantReviewServiceTest { + + @Mock + private RestaurantReviewRepository restaurantReviewRepository; + + @InjectMocks + private RestaurantReviewService restaurantReviewService; + + @Nested + @DisplayName("๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class list { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + Long buildingId = 1L; + + List mockList = List.of( + RestaurantReviewResponse.ListDTO.builder().reviewId(1L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(2L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(3L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(4L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(5L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(6L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(7L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(8L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(9L).build(), + RestaurantReviewResponse.ListDTO.builder().reviewId(10L).build() + ); + Pageable pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(mockList, pageable, 1); + + Mockito.when(restaurantReviewRepository.findRestaurantReviewByBuildingId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(mockPage); + + // When + Page actual = + restaurantReviewService.getAllList(buildingId, pageable); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(10, actual.getSize()), + () -> Assertions.assertEquals(1,actual.getTotalPages()) + ); + } + } +} \ No newline at end of file From f8dfb31629fa02536f4f9a0c80e67332b4fff376 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Mon, 18 Sep 2023 19:58:11 +0900 Subject: [PATCH 014/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20=EC=9E=A5=EC=86=8C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20API=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ์˜ˆ์•ฝํ•œ ์žฅ์†Œ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#21) * Feat: QueryDsl์— ์‚ฌ์šฉ๋  JpaQueryFactory Bean๋“ฑ๋ก (#21) * Feat: ์˜ˆ์•ฝํ•œ ์žฅ์†Œ, ํšŒ์‚ฌ ์†Œ์œ  ์‚ฌ๋ฌด์‹ค ์‘๋‹ต์„ ์œ„ํ•œ DTO ๊ตฌํ˜„ (#21) * Feat: ์˜ˆ์•ฝ ๋‚ด์—ญ๊ณผ ๊ณต์šฉ ์žฅ์†Œ ์ฟผ๋ฆฌ์˜ ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์„ ํด๋ž˜์Šค ๊ตฌํ˜„ (#21) * Feat: ๋กœ์ง์— ํ•„์š”ํ•œ Member, Office, Reservation Repository ์ถ”๊ฐ€ (#21) * Feat: ReservationQueryRepository ๊ตฌํ˜„์ฒด ์ƒ์„ฑ, ์˜ˆ์•ฝ ๋ชฉ๋ก ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#21) * Feat: Invitation ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ InvitationErrorCode ๊ตฌํ˜„ (#21) * Feat: ์˜ˆ์•ฝ ์žฅ์†Œ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต์„ ์œ„ํ•œ Service ๋กœ์ง ๊ตฌํ˜„ (#21) - ๊ฐ™์€ ๊ณต๊ฐ„, ๊ฐ™์€ ๋‚ ์งœ์˜ ์—ฐ์†๋œ ์˜ˆ์•ฝ ๋‚ด์—ญ์„ ํ•˜๋‚˜๋กœ ๋ณ‘ํ•ฉ - ๋‚ด ํšŒ์‚ฌ์˜ ์‚ฌ๋ฌด์‹ค๊ณผ ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๊ณต๊ฐ„ ๋ฆฌ์ŠคํŠธ๋ฅผ ์‘๋‹ต * Chore: invitation ํŒจํ‚ค์ง€ ๋‚ด๋ถ€ gitkeep ์‚ญ์ œ (#21) * Modify: ๋ณ€์ˆ˜๋ช… ๋ณ€๊ฒฝ officeDTOList -> offices (#21) * Test: ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ Service ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#21) * Modify: ํ•„๋“œ, ์ƒ์„ฑ์ž ์œ„์น˜ ๋ณ€๊ฒฝ ๋ฐ ๊ณต๋ฐฑ ์ˆ˜์ • (#21) * Modify: @Test์™€ @DisplayName ์œ„์น˜ ๋ณ€๊ฒฝ (#21) * Refactor: DTO๊ฐ€ Builder๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ • (#21) * Test: ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ Controller ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#21) --- .../server/core/config/QueryDslConfig.java | 16 ++ .../server/invitation/controller/.gitkeep | 0 .../controller/InvitationController.java | 27 ++++ .../livable/server/invitation/domain/.gitkeep | 0 .../domain/InvitationErrorCode.java | 26 +++ .../livable/server/invitation/dto/.gitkeep | 0 .../invitation/dto/InvitationProjection.java | 39 +++++ .../invitation/dto/InvitationResponse.java | 63 ++++++++ .../server/invitation/repository/.gitkeep | 0 .../repository/MemberRepository.java | 7 + .../repository/OfficeRepository.java | 11 ++ .../ReservationQueryRepository.java | 9 ++ .../ReservationQueryRepositoryImpl.java | 47 ++++++ .../repository/ReservationRepository.java | 7 + .../server/invitation/service/.gitkeep | 0 .../invitation/service/InvitationService.java | 96 ++++++++++++ .../controller/InvitationControllerTest.java | 87 ++++++++++ .../service/InvitationServiceTest.java | 148 ++++++++++++++++++ .../service/data/InvitationBasicData.java | 63 ++++++++ 19 files changed, 646 insertions(+) create mode 100644 src/main/java/com/livable/server/core/config/QueryDslConfig.java delete mode 100644 src/main/java/com/livable/server/invitation/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/controller/InvitationController.java delete mode 100644 src/main/java/com/livable/server/invitation/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java delete mode 100644 src/main/java/com/livable/server/invitation/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/dto/InvitationProjection.java create mode 100644 src/main/java/com/livable/server/invitation/dto/InvitationResponse.java delete mode 100644 src/main/java/com/livable/server/invitation/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/repository/MemberRepository.java create mode 100644 src/main/java/com/livable/server/invitation/repository/OfficeRepository.java create mode 100644 src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java create mode 100644 src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java create mode 100644 src/main/java/com/livable/server/invitation/repository/ReservationRepository.java delete mode 100644 src/main/java/com/livable/server/invitation/service/.gitkeep create mode 100644 src/main/java/com/livable/server/invitation/service/InvitationService.java create mode 100644 src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java create mode 100644 src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java create mode 100644 src/test/java/com/livable/server/invitation/service/data/InvitationBasicData.java diff --git a/src/main/java/com/livable/server/core/config/QueryDslConfig.java b/src/main/java/com/livable/server/core/config/QueryDslConfig.java new file mode 100644 index 00000000..5f5b0518 --- /dev/null +++ b/src/main/java/com/livable/server/core/config/QueryDslConfig.java @@ -0,0 +1,16 @@ +package com.livable.server.core.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.persistence.EntityManager; + +@Configuration +public class QueryDslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} diff --git a/src/main/java/com/livable/server/invitation/controller/.gitkeep b/src/main/java/com/livable/server/invitation/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java new file mode 100644 index 00000000..07097c3c --- /dev/null +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -0,0 +1,27 @@ +package com.livable.server.invitation.controller; + +import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.invitation.dto.InvitationResponse; +import com.livable.server.invitation.service.InvitationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/invitation") +@RestController +public class InvitationController { + + private final InvitationService invitationService; + + @GetMapping("/places/available") + public ResponseEntity> getAvailablePlaces() { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.getAvailablePlaces(memberId); + } + +} diff --git a/src/main/java/com/livable/server/invitation/domain/.gitkeep b/src/main/java/com/livable/server/invitation/domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java new file mode 100644 index 00000000..332be1a4 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java @@ -0,0 +1,26 @@ +package com.livable.server.invitation.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum InvitationErrorCode implements ErrorCode { + MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/livable/server/invitation/dto/.gitkeep b/src/main/java/com/livable/server/invitation/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java b/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java new file mode 100644 index 00000000..b704c0fd --- /dev/null +++ b/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java @@ -0,0 +1,39 @@ +package com.livable.server.invitation.dto; + +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InvitationProjection { + + @Getter + @Setter + public static class ReservationDTO { + private Long commonPlaceId; + private String commonPlaceFloor; + private String commonPlaceRoomNumber; + private String commonPlaceName; + private LocalDate reservationDate; + private LocalTime reservationStartTime; + private LocalTime reservationEndTime; + + public ReservationDTO( + Long commonPlaceId, + String commonPlaceFloor, + String commonPlaceRoomNumber, + String commonPlaceName, + LocalDate reservationDate, + LocalTime reservationStartTime + ) { + this.commonPlaceId = commonPlaceId; + this.commonPlaceFloor = commonPlaceFloor; + this.commonPlaceRoomNumber = commonPlaceRoomNumber; + this.commonPlaceName = commonPlaceName; + this.reservationDate = reservationDate; + this.reservationStartTime = reservationStartTime; + this.reservationEndTime = reservationStartTime.plusMinutes(30); + } + } +} diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java new file mode 100644 index 00000000..33631c3c --- /dev/null +++ b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java @@ -0,0 +1,63 @@ +package com.livable.server.invitation.dto; + +import com.livable.server.entity.Office; +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InvitationResponse { + + + @Getter + @Builder + public static class AvailablePlacesDTO { + private List offices; + private List commonPlaces; + } + + @Getter + @Builder + public static class OfficeDTO { + private String officeName; + + public static OfficeDTO from(Office office) { + return new OfficeDTO(getFormattedPlaceName( + office.getName(), + office.getFloor(), + office.getRoomNumber() + )); + } + } + + @Getter + @Builder + public static class CommonPlaceDTO { + private Long commonPlaceId; + private String commonPlaceName; + private LocalDate date; + private LocalTime startTime; + private LocalTime endTime; + + public static CommonPlaceDTO from(InvitationProjection.ReservationDTO reservationDTO) { + return new CommonPlaceDTO( + reservationDTO.getCommonPlaceId(), + getFormattedPlaceName( + reservationDTO.getCommonPlaceName(), + reservationDTO.getCommonPlaceFloor(), + reservationDTO.getCommonPlaceRoomNumber() + ), + reservationDTO.getReservationDate(), + reservationDTO.getReservationStartTime(), + reservationDTO.getReservationEndTime() + ); + } + } + + private static String getFormattedPlaceName(String name, String floor, String roomNumber) { + return String.format("%s (%s์ธต %sํ˜ธ)", name, floor, roomNumber); + } + +} diff --git a/src/main/java/com/livable/server/invitation/repository/.gitkeep b/src/main/java/com/livable/server/invitation/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/invitation/repository/MemberRepository.java b/src/main/java/com/livable/server/invitation/repository/MemberRepository.java new file mode 100644 index 00000000..c715ddf4 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/MemberRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java b/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java new file mode 100644 index 00000000..ac4a8304 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java @@ -0,0 +1,11 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.entity.Office; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface OfficeRepository extends JpaRepository { + + List findAllByCompanyId(Long companyId); +} diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java new file mode 100644 index 00000000..8bc5c7d5 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java @@ -0,0 +1,9 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.invitation.dto.InvitationProjection; + +import java.util.List; + +public interface ReservationQueryRepository { + List findReservationsByCompanyId(Long companyId); +} diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java new file mode 100644 index 00000000..ad5a9b3d --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java @@ -0,0 +1,47 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.invitation.dto.InvitationProjection; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.livable.server.entity.QCommonPlace.commonPlace; +import static com.livable.server.entity.QInvitationReservationMap.invitationReservationMap; +import static com.livable.server.entity.QReservation.reservation; + +@RequiredArgsConstructor +@Repository +public class ReservationQueryRepositoryImpl implements ReservationQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findReservationsByCompanyId(Long companyId) { + return queryFactory + .select(Projections.constructor(InvitationProjection.ReservationDTO.class, + commonPlace.id, + commonPlace.floor, + commonPlace.roomNumber, + commonPlace.name, + reservation.date, + reservation.time + )) + .from(reservation) + .innerJoin(reservation.commonPlace, commonPlace) + .where( + reservation.company.id.eq(companyId), + reservation.id.notIn( + JPAExpressions + .select(invitationReservationMap.reservation.id) + .from(invitationReservationMap) + .where(invitationReservationMap.reservation.id.eq(reservation.id)) + ) + ) + .orderBy(reservation.commonPlace.id.asc(), reservation.date.asc(), reservation.time.asc()) + .fetch(); + } +} diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationRepository.java b/src/main/java/com/livable/server/invitation/repository/ReservationRepository.java new file mode 100644 index 00000000..b72b11ac --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/ReservationRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.entity.Reservation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReservationRepository extends JpaRepository, ReservationQueryRepository { +} diff --git a/src/main/java/com/livable/server/invitation/service/.gitkeep b/src/main/java/com/livable/server/invitation/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java new file mode 100644 index 00000000..2337a048 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -0,0 +1,96 @@ +package com.livable.server.invitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.entity.Member; +import com.livable.server.entity.Office; +import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.dto.InvitationProjection; +import com.livable.server.invitation.dto.InvitationResponse; +import com.livable.server.invitation.repository.MemberRepository; +import com.livable.server.invitation.repository.OfficeRepository; +import com.livable.server.invitation.repository.ReservationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class InvitationService { + + private final MemberRepository memberRepository; + private final OfficeRepository officeRepository; + private final ReservationRepository reservationRepository; + + @Transactional(readOnly = true) + public ResponseEntity> getAvailablePlaces(Long memberId) { + // 1. memberId๊ฐ€ ์†ํ•œ companyId๋ฅผ ๊ฐ€์ ธ์˜ด + Long companyId = getCompanyIdByMemberId(memberId); + + // 2. company ์— ์†ํ•ด์žˆ๋Š” ์‚ฌ๋ฌด์‹ค ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด + List officeEntities = officeRepository.findAllByCompanyId(companyId); + + // 3. company ๊ฐ€ ์˜ˆ์•ฝํ•œ ๊ณต์šฉ ๊ณต๊ฐ„ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด + List reservations = reservationRepository + .findReservationsByCompanyId(companyId); + + // 4. ReservationDTO ๋ฅผ ์—ฐ์†๋œ ์‹œ๊ฐ„์€ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” ์ž‘์—…์„ ์ง„ํ–‰ + combineConsecutiveReservation(reservations); + + // 5. DTO ๋ณ€ํ™˜ ์ž‘์—… + List offices = officeEntities.stream() + .map(InvitationResponse.OfficeDTO::from).collect(Collectors.toList()); + + List commonPlaces = reservations.stream() + .map(InvitationResponse.CommonPlaceDTO::from).collect(Collectors.toList()); + + InvitationResponse.AvailablePlacesDTO responseBody = InvitationResponse.AvailablePlacesDTO.builder() + .offices(offices) + .commonPlaces(commonPlaces) + .build(); + + return ApiResponse.success(responseBody, HttpStatus.OK); + } + + private void combineConsecutiveReservation(List reservations) { + Iterator reservationsIterator = reservations.iterator(); + InvitationProjection.ReservationDTO beforeReservation = null; + while (reservationsIterator.hasNext()) { + InvitationProjection.ReservationDTO currentReservation = reservationsIterator.next(); + + if (isNotCombineTarget(beforeReservation, currentReservation)) { + beforeReservation = currentReservation; + continue; + } + beforeReservation.setReservationEndTime(currentReservation.getReservationEndTime()); + reservationsIterator.remove(); + } + } + + private boolean isNotCombineTarget( + InvitationProjection.ReservationDTO before, + InvitationProjection.ReservationDTO current + ) { + // null ์ด๊ฑฐ๋‚˜ commonPlaceId๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ๋‚ ์งœ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ์—ฐ์†๋œ ์‹œ๊ฐ„์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋Š” ์‹œ๊ฐ„์„ ํ•ฉ์น˜๋Š” ๋ชฉํ‘œ๊ฐ€ ์•„๋‹˜ + return before == null + || !before.getCommonPlaceId().equals(current.getCommonPlaceId()) + || !before.getReservationDate().equals(current.getReservationDate()) + || !before.getReservationEndTime().equals(current.getReservationStartTime()); + } + + private Long getCompanyIdByMemberId(Long memberId) { + Optional memberOptional = memberRepository.findById(memberId); + Member member = memberOptional.orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); + + return member.getCompany().getId(); + } + +} diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java new file mode 100644 index 00000000..88cd490f --- /dev/null +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -0,0 +1,87 @@ +package com.livable.server.invitation.controller; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.response.ApiResponse; +import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.dto.InvitationResponse; +import com.livable.server.invitation.service.InvitationService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(InvitationController.class) +class InvitationControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + private InvitationService invitationService; + + @DisplayName("[์„ฑ๊ณต] ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก - ์ •์ƒ ์‘๋‹ต") + @Test + void getAvailablePlacesSuccess_01() throws Exception { + // Given + Long memberId = 1L; + given(invitationService.getAvailablePlaces(memberId)) + .willReturn(new ResponseEntity<>( + ApiResponse.Success.of( + InvitationResponse.AvailablePlacesDTO.builder() + .offices(createOfficeDTOList()) + .commonPlaces(createCommonPlaceDTOList()) + .build() + ), + HttpStatus.OK + )); + + // When & Then + mockMvc.perform(get("/api/invitation/places/available")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data['offices']").isArray()) + .andExpect(jsonPath("$.data['offices'][0]['officeName']").value("์‚ฌ๋ฌด์‹ค A")) + .andExpect(jsonPath("$.data['commonPlaces']").isArray()) + .andExpect(jsonPath("$.data['commonPlaces'][0]['commonPlaceId']").value(1)); + } + + private List createOfficeDTOList() { + return List.of( + InvitationResponse.OfficeDTO.builder().officeName("์‚ฌ๋ฌด์‹ค A").build(), + InvitationResponse.OfficeDTO.builder().officeName("์‚ฌ๋ฌด์‹ค B").build(), + InvitationResponse.OfficeDTO.builder().officeName("์‚ฌ๋ฌด์‹ค C").build() + ); + } + + private List createCommonPlaceDTOList() { + return List.of( + InvitationResponse.CommonPlaceDTO.builder().commonPlaceId(1L).build(), + InvitationResponse.CommonPlaceDTO.builder().commonPlaceId(2L).build(), + InvitationResponse.CommonPlaceDTO.builder().commonPlaceId(3L).build() + ); + } + + @DisplayName("[์‹คํŒจ] ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก - GlobalException ๋ฐœ์ƒ, 400") + @Test + void getAvailablePlacesFail_01() throws Exception { + // Given + given(invitationService.getAvailablePlaces(ArgumentMatchers.anyLong())) + .willThrow(new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); + + // When & Then + mockMvc.perform(get("/api/invitation/places/available")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationErrorCode.MEMBER_NOT_EXIST.getMessage())); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java new file mode 100644 index 00000000..7b1f3b87 --- /dev/null +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -0,0 +1,148 @@ +package com.livable.server.invitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.dto.InvitationResponse.AvailablePlacesDTO; +import com.livable.server.invitation.dto.InvitationResponse.CommonPlaceDTO; +import com.livable.server.invitation.repository.MemberRepository; +import com.livable.server.invitation.repository.OfficeRepository; +import com.livable.server.invitation.repository.ReservationRepository; +import com.livable.server.invitation.service.data.InvitationBasicData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.livable.server.invitation.dto.InvitationProjection.ReservationDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class InvitationServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private OfficeRepository officeRepository; + + @Mock + private ReservationRepository reservationRepository; + + @InjectMocks + private InvitationService invitationService; + + @DisplayName("[์‹คํŒจ] ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก - ์กด์žฌํ•˜์ง€ ์•Š๋Š” Member (400)") + @Test + void getAvailablePlacesFailTest_01() { + // Given + Long memberId = -1L; + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.getAvailablePlaces(memberId)); + + assertThat(exception.getErrorCode().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(exception.getErrorCode().getMessage()).isEqualTo(InvitationErrorCode.MEMBER_NOT_EXIST.getMessage()); + } + + @DisplayName("[์„ฑ๊ณต] ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก - ์˜ˆ์•ฝ ๋ชฉ๋ก์ด ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void getAvailablePlacesSuccess_01() { + // Given + InvitationBasicData basicData = InvitationBasicData.getInstance(); + + given(memberRepository.findById(basicData.getMember().getId())).willReturn(Optional.of(basicData.getMember())); + given(officeRepository.findAllByCompanyId(basicData.getCompany().getId())).willReturn(basicData.getOffices()); + given(reservationRepository.findReservationsByCompanyId(basicData.getCompany().getId())).willReturn(List.of()); + + // When + ResponseEntity> result = invitationService + .getAvailablePlaces(basicData.getMember().getId()); + + AvailablePlacesDTO data = result.getBody().getData(); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(data.getOffices().size()).isEqualTo(3); + assertThat(data.getCommonPlaces().size()).isEqualTo(0); + } + + @DisplayName("[์„ฑ๊ณต] ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก - ์˜ˆ์•ฝ ๋ชฉ๋ก์ด ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void getAvailablePlacesSuccess_02() { + // Given + InvitationBasicData basicData = InvitationBasicData.getInstance(); + + given(memberRepository.findById(basicData.getMember().getId())).willReturn(Optional.of(basicData.getMember())); + given(officeRepository.findAllByCompanyId(basicData.getCompany().getId())).willReturn(basicData.getOffices()); + given(reservationRepository.findReservationsByCompanyId(basicData.getCompany().getId())) + .willReturn(createReservations()); + + // When + ResponseEntity> result = invitationService + .getAvailablePlaces(basicData.getMember().getId()); + + AvailablePlacesDTO data = result.getBody().getData(); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(data.getOffices().size()).isEqualTo(3); + assertThat(data.getCommonPlaces().size()).isEqualTo(3); + + CommonPlaceDTO combinedItem = data.getCommonPlaces().get(2); + assertThat(combinedItem.getStartTime()).isEqualTo(LocalTime.of(10, 30, 0)); + assertThat(combinedItem.getEndTime()).isEqualTo(LocalTime.of(11, 30, 0)); + } + + private List createReservations() { + return new ArrayList<>(List.of( + new ReservationDTO( + 1L, + "1", + "101", + "๊ณต์šฉ A", + LocalDate.of(2023, 10, 29), + LocalTime.of(10, 0, 0) + ), + new ReservationDTO( + 1L, + "1", + "101", + "๊ณต์šฉ A", + LocalDate.of(2023, 10, 30), + LocalTime.of(10, 0, 0) + ), + new ReservationDTO( + 2L, + "2", + "201", + "๊ณต์šฉ B", + LocalDate.of(2023, 10, 30), + LocalTime.of(10, 30, 0) + ), + new ReservationDTO( + 2L, + "2", + "201", + "๊ณต์šฉ B", + LocalDate.of(2023, 10, 30), + LocalTime.of(11, 0, 0) + ) + )); + } + +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/data/InvitationBasicData.java b/src/test/java/com/livable/server/invitation/service/data/InvitationBasicData.java new file mode 100644 index 00000000..1a7769ab --- /dev/null +++ b/src/test/java/com/livable/server/invitation/service/data/InvitationBasicData.java @@ -0,0 +1,63 @@ +package com.livable.server.invitation.service.data; + +import com.livable.server.entity.Building; +import com.livable.server.entity.Company; +import com.livable.server.entity.Member; +import com.livable.server.entity.Office; + +import java.util.List; + +public class InvitationBasicData { + + private static InvitationBasicData instance = null; + + public static InvitationBasicData getInstance() { + if (instance == null) { + instance = new InvitationBasicData(); + } + return instance; + } + + private final Building building; + private final Company company; + private final Member member; + private final List offices; + + private InvitationBasicData() { + this.building = Building.builder() + .id(1L) + .build(); + + this.company = Company.builder() + .id(1L) + .building(building) + .build(); + + this.member = Member.builder() + .id(1L) + .company(company) + .build(); + + this.offices = List.of( + Office.builder().id(1L).company(company).build(), + Office.builder().id(2L).company(company).build(), + Office.builder().id(3L).company(company).build() + ); + } + + public Building getBuilding() { + return building; + } + + public Company getCompany() { + return company; + } + + public Member getMember() { + return member; + } + + public List getOffices() { + return offices; + } +} From 22d8cb4e0e06fcd8072893e8f5fb7134a7128878 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:07:17 +0900 Subject: [PATCH 015/111] =?UTF-8?q?QR=20Code=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ํ•„์š”ํ•œ Repository ์ƒ์„ฑ (#22) Invitation, Visitor Repository ์ƒ์„ฑ * Chore: QR์ฝ”๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ ์ถ”๊ฐ€ (#22) zxing ์˜์กด์„ฑ ์ถ”๊ฐ€ * Feat: QR์ฝ”๋“œ ๋งค๋‹ˆ์ € ํด๋ž˜์Šค ๊ตฌํ˜„ (#22) QrCodeManager ํด๋ž˜์Šค ์ƒ์„ฑ ๋ฐ ๊ตฌํ˜„ QrPayload ํด๋ž˜์Šค ์ƒ์„ฑ * Feat: InvitationId๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#22) VisitorId๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๋ฐฉ๋ฌธ์ž์˜ ์ดˆ๋Œ€์žฅ Id๋ฅผ ์กฐํšŒ * Feat: ์ดˆ๋Œ€์žฅ ์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ ์‹œ๊ฐ„์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#22) QR ์ƒ์„ฑ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ์ดˆ๋Œ€์žฅ ์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ ์‹œ๊ฐ„์„ ์กฐํšŒ * Feat: QR์ƒ์„ฑ ๋ฐ validate ์œ„์ž„ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#22) VisitationService์—์„œ QrCodeManager๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ QR ๊ด€๋ฆฌ * Feat: VisitationFacadeService ๊ตฌํ˜„ (#22) Service, Repository์˜ ์˜์กด์„ฑ์„ 1๋Œ€1๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Facade ๊ฐ์ฒด ๊ตฌํ˜„ * Feat: VisitationErrorCode ๊ตฌํ˜„ (#22) HttpStatus ๋ฐ Message ๊ด€๋ฆฌ ๊ฐ์ฒด * Feat: VisitationController ๊ตฌํ˜„ (#22) * Modify: InvitationRepository ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ (#22) ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ * Typo: ๋ฉ”์„œ๋“œ ๋ช… ์ˆ˜์ • (#22) ์ปจ๋ฒค์…˜์— ๋งž๋„๋ก ๋ฉ”์„œ๋“œ ๋ช… ์ˆ˜์ • get -> create * Typo: ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ (#22) ์ปจ๋ฒค์…˜์— ๋งž๋„๋ก ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ * Test: QRCode ์ƒ์„ฑ ์š”์ฒญ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) VisitationController createQrCode์— ๋Œ€ํ•œ MVC ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ * Test: QRCode ์ƒ์„ฑ ์š”์ฒญ ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) VisitationController createQrCode์— ๋Œ€ํ•œ MVC ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ * Test: QRCode ์ƒ์„ฑ ์š”์ฒญ ์‹คํŒจ(400) ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: QRCode ์ƒ์„ฑ ์š”์ฒญ ์‹คํŒจ(500) ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Refactor: ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ปจํŠธ๋กค๋Ÿฌ์™€ ํ†ต์ผ (#22) * Test: ํ…Œ์ŠคํŠธ์šฉ ๋ชจํ‚น ๊ฐ์ฒด ์ƒ์„ฑ (#22) RequestBodyDto ๊ฐ์ฒด์— @Builder ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๋Š” ๋Œ€์‹  ์ƒ์† ๋ชจํ‚น ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋Œ€์‹  ์‚ฌ์šฉ * Test: QRCode ์ธ์ฆ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: QRCode ์ธ์ฆ ์‹คํŒจ(400) ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: findInvitationTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: findInvitationTime ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) VisitationService ํ…Œ์ŠคํŠธ * Test: VisitorService.findInvitationId ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Style: ๋ถˆํ•„์š”ํ•œ Import๋ฌธ ์ œ๊ฑฐ (#22) InvitationServiceTest ๋‚ด๋ถ€์— MockBean import๋ฌธ ์ œ๊ฑฐ * Test: VisitorService.findInvitationId ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Refactor: Qr์ƒ์„ฑ Validation ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ • (#22) * Fix: LocalDateTime ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ ์ถ”๊ฐ€ (#22) objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(expirationPeriodMap)๋กœ ์ˆ˜์ • * Test: QrCodeManager.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_1 ๊ตฌํ˜„ (#22) * Test: QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_2 ๊ตฌํ˜„ (#22) * Refactor: Controller ๋ฉ”์„œ๋“œ์—์„œ๋Š” Facade ๊ฐ์ฒด๋กœ ์œ„์ž„๋งŒ ํ•˜๋„๋ก ๋ฆฌํŒฉํ„ฐ๋ง (#22) Facade๊ฐ์ฒด์—์„œ ๋‹ค์–‘ํ•œ Service์— ๋Œ€ํ•œ ์˜์กด์„ ํ†ตํ•ด ๋กœ์ง ์ฒ˜๋ฆฌ * Refactor: ๋กœ์ง ์ˆ˜์ •์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง (#22) * Modify: ๋ถˆํ•„์š”ํ•œ ์˜ˆ์™ธ ํ…Œ์ŠคํŠธ ์ œ๊ฑฐ (#22) * Test: VisitationFacadeService.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Test: VisitationFacadeService.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#22) * Feat: ์ฟผ๋ฆฌ ์ค„์ด๊ธฐ ์œ„ํ•ด JPQL ์ง์ ‘ ์ž‘์„ฑ (#22) * Fix: Repository ํ…Œ์ŠคํŠธ ์‹คํŒจ ์ œ๊ฑฐ (#22) --- build.gradle | 2 + .../controller/VisitationController.java | 35 ++++ .../visitation/domain/QrCodeManager.java | 155 ++++++++++++++++++ .../server/visitation/domain/QrPayload.java | 12 ++ .../domain/VisitationErrorCode.java | 23 +++ .../visitation/dto/VisitationRequest.java | 14 ++ .../visitation/dto/VisitationResponse.java | 41 +++++ .../repository/InvitationRepository.java | 7 + .../repository/VisitorRepository.java | 7 + .../visitation/service/InvitationService.java | 23 +++ .../service/VisitationFacadeService.java | 29 ++++ .../visitation/service/VisitationService.java | 22 +++ .../visitation/service/VisitorService.java | 21 +++ .../controller/VisitationControllerTest.java | 106 ++++++++++++ .../visitation/domain/QrCodeManagerTest.java | 74 +++++++++ .../ValidateQrCodeSuccessMockRequest.java | 19 +++ .../service/InvitationServiceTest.java | 92 +++++++++++ .../service/VisitationFacadeServiceTest.java | 75 +++++++++ .../service/VisitationServiceTest.java | 59 +++++++ .../service/VisitorServiceTest.java | 78 +++++++++ 20 files changed, 894 insertions(+) create mode 100644 src/main/java/com/livable/server/visitation/controller/VisitationController.java create mode 100644 src/main/java/com/livable/server/visitation/domain/QrCodeManager.java create mode 100644 src/main/java/com/livable/server/visitation/domain/QrPayload.java create mode 100644 src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java create mode 100644 src/main/java/com/livable/server/visitation/dto/VisitationRequest.java create mode 100644 src/main/java/com/livable/server/visitation/dto/VisitationResponse.java create mode 100644 src/main/java/com/livable/server/visitation/repository/InvitationRepository.java create mode 100644 src/main/java/com/livable/server/visitation/repository/VisitorRepository.java create mode 100644 src/main/java/com/livable/server/visitation/service/InvitationService.java create mode 100644 src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java create mode 100644 src/main/java/com/livable/server/visitation/service/VisitationService.java create mode 100644 src/main/java/com/livable/server/visitation/service/VisitorService.java create mode 100644 src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java create mode 100644 src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java create mode 100644 src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java create mode 100644 src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java create mode 100644 src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java create mode 100644 src/test/java/com/livable/server/visitation/service/VisitationServiceTest.java create mode 100644 src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java diff --git a/build.gradle b/build.gradle index 698962e2..98f2c98d 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,8 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + implementation 'com.google.zxing:core:3.5.2' + implementation 'com.google.zxing:javase:3.5.2' } tasks.named('test') { diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java new file mode 100644 index 00000000..33f61f8d --- /dev/null +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -0,0 +1,35 @@ +package com.livable.server.visitation.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.visitation.service.VisitationFacadeService; +import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.dto.VisitationResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/visitation") +public class VisitationController { + + private final VisitationFacadeService visitationFacadeService; + + @GetMapping("/qr") + public ResponseEntity> createQrCode() { + + Long visitorId = 1L; + String base64QrCode = visitationFacadeService.createQrCode(visitorId); + + return ApiResponse.success(base64QrCode, HttpStatus.OK); + } + + @PostMapping("/qr") + public ResponseEntity> validateQrCode(@RequestBody VisitationRequest.ValidateQrDto validateQrDto) { + + visitationFacadeService.validateQrCode(validateQrDto.getQr()); + + return ApiResponse.success(HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java new file mode 100644 index 00000000..c5c167fb --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java @@ -0,0 +1,155 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.zxing.*; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; +import com.google.zxing.qrcode.QRCodeWriter; +import com.livable.server.core.exception.GlobalRuntimeException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Component +public class QrCodeManager { + + private static final int DEFAULT_WIDTH = 170; + private static final int DEFAULT_HEIGHT = 170; + private static final String DEFAULT_FORMAT = "png"; + private static final int DEFAULT_QR_CODE_COLOR = 0xFFFFFFFF; + private static final int DEFAULT_BACKGROUND_COLOR = 0xFF2563EA; + private static final String EXPIRATION_START_DATE_KEY = "startDate"; + private static final String EXPIRATION_END_DATE_KEY = "endDate"; + private static final String DEFAULT_CHARSET = "UTF-8"; + + private final ObjectMapper objectMapper; + + public String createQrCode(LocalDateTime startDate, LocalDateTime endDate) { + + validatePeriod(startDate, endDate); + + BufferedImage qrCode = createQrCodeImage(startDate, endDate); + + return encodeQrcodeToBase64(qrCode); + } + + private void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) { + if (startDate.isAfter(endDate) || endDate.isBefore(startDate)) { + throw new GlobalRuntimeException(VisitationErrorCode.INVALID_PERIOD); + } + + if (!(startDate.isBefore(LocalDateTime.now()) && endDate.isAfter(LocalDateTime.now()))) { + throw new GlobalRuntimeException(VisitationErrorCode.INVALID_QR_PERIOD); + } + } + + private BufferedImage createQrCodeImage(LocalDateTime startDateTime, LocalDateTime endDateTime) { + try { + + HashMap expirationPeriodMap = getExpirationPeriodMap(startDateTime, endDateTime); + String contents = objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(expirationPeriodMap); + + Map encodeHints = getEncodeHints(); + + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + + BitMatrix bitMatrix = qrCodeWriter.encode(contents, BarcodeFormat.QR_CODE, DEFAULT_WIDTH, DEFAULT_HEIGHT, encodeHints); + + return MatrixToImageWriter.toBufferedImage(bitMatrix); + } catch (JsonProcessingException | WriterException e) { + throw new RuntimeException(e); + } + } + + private HashMap getExpirationPeriodMap(LocalDateTime startDate, LocalDateTime endDate) { + return new HashMap<>() {{ + put(EXPIRATION_START_DATE_KEY, startDate); + put(EXPIRATION_END_DATE_KEY, endDate); + }}; + } + + private Map getEncodeHints() { + return new EnumMap<>(EncodeHintType.class) {{ + put(EncodeHintType.CHARACTER_SET, DEFAULT_CHARSET); + }}; + } + + private String encodeQrcodeToBase64(BufferedImage bufferedImage) { + try { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + ImageIO.write(bufferedImage, DEFAULT_FORMAT, outputStream); + + byte[] imageBytes = outputStream.toByteArray(); + return Base64.getEncoder().encodeToString(imageBytes); + + } catch (IOException e) { + log.error("QrCodeManager.encodeQrcodeToBase64", e); + throw new GlobalRuntimeException(VisitationErrorCode.IO); + } + } + + public void validateQrCode(String base64QrCode) { + + QrPayload qrPayload = getQrPayload(base64QrCode); + validatePeriod(qrPayload.getStartDate(), qrPayload.getEndDate()); + } + + private QrPayload getQrPayload(String base64QrCode) { + + Map hints = getDecodeHints(); + String decodeQrContent = getDecodeQrContent(base64QrCode, hints); + + try { + return objectMapper.readValue(decodeQrContent, QrPayload.class); + } catch (JsonProcessingException e) { + log.error("QrCodeManager.getQrPayload", e); + throw new GlobalRuntimeException(VisitationErrorCode.OBJECTMAPPER); + } + } + + private Map getDecodeHints() { + return new EnumMap<>(DecodeHintType.class) {{ + put(DecodeHintType.CHARACTER_SET, DEFAULT_CHARSET); + }}; + } + + private String getDecodeQrContent(String base64QrCode, Map hints) { + + Base64.Decoder decoder = Base64.getDecoder(); + byte[] imageBytes = decoder.decode(base64QrCode); + + try { + BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes)); + BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); + Result decode = new QRCodeReader().decode(binaryBitmap, hints); + + return decode.getText(); + } catch (IOException e) { + log.error("QrCodeManager.getDecodeQrContent", e); + throw new GlobalRuntimeException(VisitationErrorCode.IO); + } catch (ChecksumException | NotFoundException | FormatException e) { + log.error("QrCodeManager.getDecodeQrContent", e); + throw new GlobalRuntimeException(VisitationErrorCode.QR_DECODE); + } + } +} diff --git a/src/main/java/com/livable/server/visitation/domain/QrPayload.java b/src/main/java/com/livable/server/visitation/domain/QrPayload.java new file mode 100644 index 00000000..e14f614a --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/QrPayload.java @@ -0,0 +1,12 @@ +package com.livable.server.visitation.domain; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class QrPayload { + + private LocalDateTime startDate; + private LocalDateTime endDate; +} diff --git a/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java new file mode 100644 index 00000000..43388cfb --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java @@ -0,0 +1,23 @@ +package com.livable.server.visitation.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum VisitationErrorCode implements ErrorCode { + + OBJECTMAPPER(HttpStatus.INTERNAL_SERVER_ERROR, "์ง๋ ฌํ™” ๊ณผ์ •์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + QR_ENCODE(HttpStatus.INTERNAL_SERVER_ERROR, "QR์ฝ”๋“œ ์ƒ์„ฑ ์ค‘ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + QR_DECODE(HttpStatus.INTERNAL_SERVER_ERROR, "QR์ฝ”๋“œ๋ฅผ ํ‘ธ๋Š” ๊ณผ์ •์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + IO(HttpStatus.INTERNAL_SERVER_ERROR, "I/O๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ณผ์ •์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + INVALID_QR_PERIOD(HttpStatus.BAD_REQUEST, "QR์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค."), + INVALID_PERIOD(HttpStatus.BAD_REQUEST, "์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ์‹œ๊ฐ„์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + NOT_FOUND(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java new file mode 100644 index 00000000..aa6c5c01 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java @@ -0,0 +1,14 @@ +package com.livable.server.visitation.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class VisitationRequest { + + @Getter + public static class ValidateQrDto { + private String qr; + } +} diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java new file mode 100644 index 00000000..9da585c2 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -0,0 +1,41 @@ +package com.livable.server.visitation.dto; + +import com.beust.ah.A; +import com.livable.server.entity.Invitation; +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class VisitationResponse { + + @Getter + @Builder + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor(access = AccessLevel.PROTECTED) + public static class InvitationTimeDto { + private LocalDate startDate; + private LocalDate endDate; + private LocalTime startTime; + private LocalTime endTime; + + public LocalDateTime getStartDateTime() { + return LocalDateTime.of(startDate, startTime); + } + + public LocalDateTime getEndDateTime() { + return LocalDateTime.of(endDate, endTime); + } + + public static InvitationTimeDto from(Invitation invitation) { + return InvitationTimeDto.builder() + .startTime(invitation.getStartTime()) + .endTime(invitation.getEndTime()) + .startDate(invitation.getStartDate()) + .endDate(invitation.getEndDate()) + .build(); + } + } +} diff --git a/src/main/java/com/livable/server/visitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/visitation/repository/InvitationRepository.java new file mode 100644 index 00000000..89b6bcbf --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/InvitationRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.entity.Invitation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InvitationRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java new file mode 100644 index 00000000..c48ec52d --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.entity.Visitor; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VisitorRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/visitation/service/InvitationService.java b/src/main/java/com/livable/server/visitation/service/InvitationService.java new file mode 100644 index 00000000..b3d81b24 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/InvitationService.java @@ -0,0 +1,23 @@ +package com.livable.server.visitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Invitation; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.repository.InvitationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class InvitationService { + + private final InvitationRepository invitationRepository; + + public VisitationResponse.InvitationTimeDto findInvitationTime(Long invitationId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + + return VisitationResponse.InvitationTimeDto.from(invitation); + } +} diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java new file mode 100644 index 00000000..077001b3 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -0,0 +1,29 @@ +package com.livable.server.visitation.service; + +import com.livable.server.visitation.dto.VisitationResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class VisitationFacadeService { + + private final VisitationService visitationService; + private final InvitationService invitationService; + private final VisitorService visitorService; + + public String createQrCode(Long visitorId) { + Long invitationId = visitorService.findInvitationId(visitorId); + VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(invitationId); + + return visitationService.createQrCode(invitationTime.getStartDateTime(), invitationTime.getEndDateTime()); + } + + public void validateQrCode(String qr) { + visitationService.validateQrCode(qr); + } +} diff --git a/src/main/java/com/livable/server/visitation/service/VisitationService.java b/src/main/java/com/livable/server/visitation/service/VisitationService.java new file mode 100644 index 00000000..70a6905a --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/VisitationService.java @@ -0,0 +1,22 @@ +package com.livable.server.visitation.service; + +import com.livable.server.visitation.domain.QrCodeManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@RequiredArgsConstructor +@Service +public class VisitationService { + + private final QrCodeManager qrCodeManager; + + public String createQrCode(LocalDateTime startDate, LocalDateTime endDate) { + return qrCodeManager.createQrCode(startDate, endDate); + } + + public void validateQrCode(String qr) { + qrCodeManager.validateQrCode(qr); + } +} diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java new file mode 100644 index 00000000..0084daa5 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -0,0 +1,21 @@ +package com.livable.server.visitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.repository.VisitorRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class VisitorService { + + private final VisitorRepository visitorRepository; + + public Long findInvitationId(Long visitorId) { + return visitorRepository.findById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)) + .getInvitation() + .getId(); + } +} diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java new file mode 100644 index 00000000..932044ce --- /dev/null +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -0,0 +1,106 @@ +package com.livable.server.visitation.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.livable.server.core.exception.ErrorCode; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.mock.ValidateQrCodeSuccessMockRequest; +import com.livable.server.visitation.service.VisitationFacadeService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(VisitationController.class) +class VisitationControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @MockBean + private VisitationFacadeService visitationFacadeService; + + @DisplayName("[GET][/api/visitation/qr] - QR์„ ์ƒ์„ฑ ์ •์ƒ ์‘๋‹ต") + @Test + void createQrCodeSuccessTest() throws Exception { + String base64QrCode = "base64QrCode์ž„ ใ…‹ใ…‹"; + + // given + given(visitationFacadeService.createQrCode(1L)).willReturn(base64QrCode); + + // when + ResultActions resultActions = mockMvc.perform( + get("/api/visitation/qr") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data").value(base64QrCode)); + + then(visitationFacadeService).should(times(1)).createQrCode(1L); + } + + @DisplayName("[POST][/api/visitation/qr] - QR์ธ์ฆ ์„ฑ๊ณต") + @Test + void validateQrCodeSuccess() throws Exception { + // given + String qr = "qr"; + VisitationRequest.ValidateQrDto validateQrCodeSuccessMockRequest = new ValidateQrCodeSuccessMockRequest(qr); + + willDoNothing().given(visitationFacadeService).validateQrCode(anyString()); + + // when + ResultActions resultActions = mockMvc.perform( + post("/api/visitation/qr") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validateQrCodeSuccessMockRequest)) + ); + + // then + resultActions.andExpect(status().isOk()); + + then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); + } + + @DisplayName("[POST][/api/visitation/qr] - QR์ธ์ฆ ์„ฑ๊ณต") + @Test + void validateQrCodeFail() throws Exception { + // given + String qr = "qr"; + ErrorCode errorCode = VisitationErrorCode.INVALID_QR_PERIOD; + String errorMessage = errorCode.getMessage(); + VisitationRequest.ValidateQrDto validateQrCodeSuccessMockRequest = new ValidateQrCodeSuccessMockRequest(qr); + + + willThrow(new GlobalRuntimeException(errorCode)).given(visitationFacadeService).validateQrCode(anyString()); + + // when + ResultActions resultActions = mockMvc.perform( + post("/api/visitation/qr") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validateQrCodeSuccessMockRequest)) + ); + + // then + resultActions.andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message").value(errorMessage)); + + then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java new file mode 100644 index 00000000..87cbcde5 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java @@ -0,0 +1,74 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.livable.server.core.exception.GlobalRuntimeException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class QrCodeManagerTest { + + @InjectMocks + QrCodeManager qrCodeManager; + + @Spy + ObjectMapper objectMapper; + + @DisplayName("QrCodeManager.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void createQrCodeSuccessTest() throws JsonProcessingException { + + // Given + // When + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + String qrCode = qrCodeManager.createQrCode(startDate, endDate); + + // Then + assertThat(qrCode).isNotNull(); + then(objectMapper).should(times(1)).writeValueAsString(any()); + } + + @DisplayName("QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_1") + @Test + void createQrCodeFailTest_1() throws JsonProcessingException { + + // Given + LocalDateTime startDate = LocalDateTime.now().plusDays(1); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> qrCodeManager.createQrCode(startDate, endDate)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_PERIOD); + } + + @DisplayName("QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_2") + @Test + void createQrCodeFailTest_2() throws JsonProcessingException { + + // Given + LocalDateTime startDate = LocalDateTime.now().minusDays(2); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> qrCodeManager.createQrCode(startDate, endDate)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_QR_PERIOD); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java b/src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java new file mode 100644 index 00000000..a2212233 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java @@ -0,0 +1,19 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.visitation.dto.VisitationRequest; + +public class ValidateQrCodeSuccessMockRequest extends VisitationRequest.ValidateQrDto { + private String qr; + + public ValidateQrCodeSuccessMockRequest() { + } + + public ValidateQrCodeSuccessMockRequest(String qr) { + this.qr = qr; + } + + @Override + public String getQr() { + return qr; + } +} diff --git a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java new file mode 100644 index 00000000..ab585b5f --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java @@ -0,0 +1,92 @@ +package com.livable.server.visitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Invitation; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.repository.InvitationRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class InvitationServiceTest { + + @InjectMocks + InvitationService invitationService; + + @Mock + InvitationRepository invitationRepository; + + @DisplayName("InvitationService.findInvitationTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findInvitationTimeSuccessTest() { + + // Given + Invitation invitation = Invitation.builder() + .startTime(LocalTime.of(1, 1)) + .endTime(LocalTime.of(1, 1)) + .startDate(LocalDate.of(2023, 9, 18)) + .endDate(LocalDate.of(2023, 9, 18)) + .build(); + + VisitationResponse.InvitationTimeDto invitationTimeDto = VisitationResponse.InvitationTimeDto.builder() + .startTime(LocalTime.of(1, 1)) + .endTime(LocalTime.of(1, 1)) + .startDate(LocalDate.of(2023, 9, 18)) + .endDate(LocalDate.of(2023, 9, 18)) + .build(); + + given(invitationRepository.findById(anyLong())).willReturn(Optional.of(invitation)); + + // When + VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(anyLong()); + + // Then + then(invitationRepository).should(times(1)).findById(anyLong()); + assertThat(invitationTime).usingRecursiveComparison().isEqualTo(invitationTimeDto); + } + + @DisplayName("InvitationService.findInvitationTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findInvitationTimeFailTest() { + + // Given + Invitation invitation = Invitation.builder() + .startTime(LocalTime.of(1, 1)) + .endTime(LocalTime.of(1, 1)) + .startDate(LocalDate.of(2023, 9, 18)) + .endDate(LocalDate.of(2023, 9, 18)) + .build(); + + VisitationResponse.InvitationTimeDto invitationTimeDto = VisitationResponse.InvitationTimeDto.builder() + .startTime(LocalTime.of(1, 1)) + .endTime(LocalTime.of(1, 1)) + .startDate(LocalDate.of(2023, 9, 18)) + .endDate(LocalDate.of(2023, 9, 18)) + .build(); + + given(invitationRepository.findById(anyLong())).willReturn(Optional.empty()); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> invitationService.findInvitationTime(anyLong())); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(invitationRepository).should(times(1)).findById(anyLong()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java new file mode 100644 index 00000000..4089ed71 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -0,0 +1,75 @@ +package com.livable.server.visitation.service; + +import com.livable.server.visitation.dto.VisitationResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class VisitationFacadeServiceTest { + + private final static String QR_CODE = "QR_CODE"; + + @InjectMocks + VisitationFacadeService visitationFacadeService; + + @Mock + VisitationService visitationService; + @Mock + InvitationService invitationService; + @Mock + VisitorService visitorService; + + @DisplayName("VisitationFacadeService.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void createQrCodeSuccessTest() { + // Given + VisitationResponse.InvitationTimeDto invitationTimeDto = VisitationResponse.InvitationTimeDto.builder() + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .startTime(LocalTime.now()) + .endTime(LocalTime.now()) + .build(); + + given(visitorService.findInvitationId(anyLong())).willReturn(1L); + given(invitationService.findInvitationTime(anyLong())).willReturn(invitationTimeDto); + given(visitationService.createQrCode(any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(QR_CODE); + + // When + String qrCode = visitationFacadeService.createQrCode(1L); + + // Then + assertThat(qrCode).isEqualTo(QR_CODE); + then(visitorService).should(times(1)).findInvitationId(anyLong()); + then(invitationService).should(times(1)).findInvitationTime(anyLong()); + then(visitationService).should(times(1)).createQrCode(any(LocalDateTime.class), any(LocalDateTime.class)); + } + + @DisplayName("VisitationFacadeService.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void validateQrCodeSuccessTest() { + + // Given + willDoNothing().given(visitationService).validateQrCode(anyString()); + + // When + visitationFacadeService.validateQrCode(QR_CODE); + + // Then + then(visitationService).should(times(1)).validateQrCode(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationServiceTest.java new file mode 100644 index 00000000..5ad11779 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/VisitationServiceTest.java @@ -0,0 +1,59 @@ +package com.livable.server.visitation.service; + +import com.livable.server.visitation.domain.QrCodeManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class VisitationServiceTest { + + @InjectMocks + VisitationService visitationService; + + @Mock + QrCodeManager qrCodeManager; + + @DisplayName("VisitationService.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void createQrCodeSuccessTest() { + // Given + String qrCode = "qrCode"; + LocalDateTime startDate = LocalDateTime.of(2023, 9, 18, 1, 10); + LocalDateTime endDate = LocalDateTime.of(2023, 9, 18, 1, 11); + + given(qrCodeManager.createQrCode(any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(qrCode); + +// When + String resultQrCode = visitationService.createQrCode(startDate, endDate); + + // Then + assertThat(qrCode).isEqualTo(resultQrCode); + then(qrCodeManager).should(times(1)).createQrCode(any(LocalDateTime.class), any(LocalDateTime.class)); + } + + @DisplayName("VisitationService.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void validateQrCodeSuccessTest() { + // Given + String qrCode = "qrCode"; + + willDoNothing().given(qrCodeManager).validateQrCode(anyString()); + + // When + visitationService.validateQrCode(anyString()); + + // Then + then(qrCodeManager).should(times(1)).validateQrCode(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java new file mode 100644 index 00000000..2180d7b3 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java @@ -0,0 +1,78 @@ +package com.livable.server.visitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Invitation; +import com.livable.server.entity.Visitor; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.repository.VisitorRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class VisitorServiceTest { + + @InjectMocks + VisitorService visitorService; + + @Mock + VisitorRepository visitorRepository; + + @DisplayName("VisitorService.findInvitationId ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findInvitationIdSuccessTest() { + // Given + Invitation invitation = Invitation.builder() + .id(1L) + .build(); + + Visitor visitor = Visitor.builder() + .name("ํƒœ์œค์ดˆ์ด") + .invitation(invitation) + .build(); + + given(visitorRepository.findById(anyLong())).willReturn(Optional.of(visitor)); + + // When + Long invitationId = visitorService.findInvitationId(1L); + + // Then + assertThat(invitationId).isEqualTo(1L); + then(visitorRepository).should(times(1)).findById(anyLong()); + } + + @DisplayName("VisitorService.findInvitationId ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findInvitationIdFailTest() { + // Given + Invitation invitation = Invitation.builder() + .id(1L) + .build(); + + Visitor visitor = Visitor.builder() + .name("ํƒœ์œค์ดˆ์ด") + .invitation(invitation) + .build(); + + given(visitorRepository.findById(anyLong())).willReturn(Optional.empty()); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> visitorService.findInvitationId(1L)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(visitorRepository).should(times(1)).findById(anyLong()); + } +} \ No newline at end of file From 3144bd0d8079c583f23eba1c1ddb78ecb597f730 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Mon, 18 Sep 2023 20:21:03 +0900 Subject: [PATCH 016/111] =?UTF-8?q?=EB=B6=84=EB=A6=AC=EB=90=9C=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ๋ถ„๋ฆฌ๋œ ๋„๋ฉ”์ธ ๋กœ์ง ํ†ตํ•ฉ (#29) --- .../repository/InvitationRepository.java | 2 +- .../invitation/service/InvitationService.java | 16 +++++++++++-- .../repository/MemberRepository.java | 2 +- .../ReservationQueryRepository.java | 2 +- .../ReservationQueryRepositoryImpl.java | 2 +- .../repository/ReservationRepository.java | 2 +- .../visitation/service/InvitationService.java | 23 ------------------- .../service/VisitationFacadeService.java | 3 +-- .../service/InvitationServiceTest.java | 4 ++-- .../service/InvitationServiceTest.java | 3 ++- .../service/VisitationFacadeServiceTest.java | 1 + 11 files changed, 25 insertions(+), 35 deletions(-) rename src/main/java/com/livable/server/{visitation => invitation}/repository/InvitationRepository.java (79%) rename src/main/java/com/livable/server/{invitation => member}/repository/MemberRepository.java (78%) rename src/main/java/com/livable/server/{invitation => reservation}/repository/ReservationQueryRepository.java (81%) rename src/main/java/com/livable/server/{invitation => reservation}/repository/ReservationQueryRepositoryImpl.java (97%) rename src/main/java/com/livable/server/{invitation => reservation}/repository/ReservationRepository.java (81%) delete mode 100644 src/main/java/com/livable/server/visitation/service/InvitationService.java diff --git a/src/main/java/com/livable/server/visitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java similarity index 79% rename from src/main/java/com/livable/server/visitation/repository/InvitationRepository.java rename to src/main/java/com/livable/server/invitation/repository/InvitationRepository.java index 89b6bcbf..176c6ffb 100644 --- a/src/main/java/com/livable/server/visitation/repository/InvitationRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java @@ -1,4 +1,4 @@ -package com.livable.server.visitation.repository; +package com.livable.server.invitation.repository; import com.livable.server.entity.Invitation; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 2337a048..9cf913bb 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -3,14 +3,18 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse; import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.entity.Invitation; import com.livable.server.entity.Member; import com.livable.server.entity.Office; import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.dto.InvitationProjection; import com.livable.server.invitation.dto.InvitationResponse; -import com.livable.server.invitation.repository.MemberRepository; +import com.livable.server.invitation.repository.InvitationRepository; +import com.livable.server.member.repository.MemberRepository; import com.livable.server.invitation.repository.OfficeRepository; -import com.livable.server.invitation.repository.ReservationRepository; +import com.livable.server.reservation.repository.ReservationRepository; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -29,6 +33,14 @@ public class InvitationService { private final MemberRepository memberRepository; private final OfficeRepository officeRepository; private final ReservationRepository reservationRepository; + private final InvitationRepository invitationRepository; + + public VisitationResponse.InvitationTimeDto findInvitationTime(Long invitationId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + + return VisitationResponse.InvitationTimeDto.from(invitation); + } @Transactional(readOnly = true) public ResponseEntity> getAvailablePlaces(Long memberId) { diff --git a/src/main/java/com/livable/server/invitation/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java similarity index 78% rename from src/main/java/com/livable/server/invitation/repository/MemberRepository.java rename to src/main/java/com/livable/server/member/repository/MemberRepository.java index c715ddf4..0f6d8b11 100644 --- a/src/main/java/com/livable/server/invitation/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -1,4 +1,4 @@ -package com.livable.server.invitation.repository; +package com.livable.server.member.repository; import com.livable.server.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java similarity index 81% rename from src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java rename to src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index 8bc5c7d5..61e063bb 100644 --- a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -1,4 +1,4 @@ -package com.livable.server.invitation.repository; +package com.livable.server.reservation.repository; import com.livable.server.invitation.dto.InvitationProjection; diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java similarity index 97% rename from src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java rename to src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index ad5a9b3d..abfcf12c 100644 --- a/src/main/java/com/livable/server/invitation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.livable.server.invitation.repository; +package com.livable.server.reservation.repository; import com.livable.server.invitation.dto.InvitationProjection; import com.querydsl.core.types.Projections; diff --git a/src/main/java/com/livable/server/invitation/repository/ReservationRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationRepository.java similarity index 81% rename from src/main/java/com/livable/server/invitation/repository/ReservationRepository.java rename to src/main/java/com/livable/server/reservation/repository/ReservationRepository.java index b72b11ac..7b1ad92b 100644 --- a/src/main/java/com/livable/server/invitation/repository/ReservationRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationRepository.java @@ -1,4 +1,4 @@ -package com.livable.server.invitation.repository; +package com.livable.server.reservation.repository; import com.livable.server.entity.Reservation; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/livable/server/visitation/service/InvitationService.java b/src/main/java/com/livable/server/visitation/service/InvitationService.java deleted file mode 100644 index b3d81b24..00000000 --- a/src/main/java/com/livable/server/visitation/service/InvitationService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.livable.server.visitation.service; - -import com.livable.server.core.exception.GlobalRuntimeException; -import com.livable.server.entity.Invitation; -import com.livable.server.visitation.domain.VisitationErrorCode; -import com.livable.server.visitation.dto.VisitationResponse; -import com.livable.server.visitation.repository.InvitationRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class InvitationService { - - private final InvitationRepository invitationRepository; - - public VisitationResponse.InvitationTimeDto findInvitationTime(Long invitationId) { - Invitation invitation = invitationRepository.findById(invitationId) - .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); - - return VisitationResponse.InvitationTimeDto.from(invitation); - } -} diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 077001b3..c45e142b 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -1,12 +1,11 @@ package com.livable.server.visitation.service; +import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.dto.VisitationResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - @RequiredArgsConstructor @Service @Transactional(readOnly = true) diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 7b1f3b87..d2cb1219 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -5,9 +5,9 @@ import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.dto.InvitationResponse.AvailablePlacesDTO; import com.livable.server.invitation.dto.InvitationResponse.CommonPlaceDTO; -import com.livable.server.invitation.repository.MemberRepository; +import com.livable.server.member.repository.MemberRepository; import com.livable.server.invitation.repository.OfficeRepository; -import com.livable.server.invitation.repository.ReservationRepository; +import com.livable.server.reservation.repository.ReservationRepository; import com.livable.server.invitation.service.data.InvitationBasicData; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java index ab585b5f..02fdc7df 100644 --- a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java @@ -2,9 +2,10 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Invitation; +import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; -import com.livable.server.visitation.repository.InvitationRepository; +import com.livable.server.invitation.repository.InvitationRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 4089ed71..2cb55a4a 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -1,5 +1,6 @@ package com.livable.server.visitation.service; +import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.dto.VisitationResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 5dd926bef20b1649b783bc97a3c43193560177b3 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Tue, 19 Sep 2023 13:09:14 +0900 Subject: [PATCH 017/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ์ดˆ๋Œ€ ๋ชฉ์  ๊ด€๋ จ Enum ํด๋ž˜์Šค ํŒจํ‚ค์ง€ ์ด๋™ ํ›„ value ์ถ”๊ฐ€ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ DTO, Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ €์žฅ์— ํ•„์š”ํ•œ Repository ๊ตฌํ˜„ ๋ฐ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ์ž‘์„ฑ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ €์žฅ Service ๋กœ์ง์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ ์ฝ”๋“œ ๊ตฌํ˜„ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ €์žฅ์— ๋Œ€ํ•œ Service ๋กœ์ง ๊ตฌํ˜„ (#31) * Test: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ ๊ธฐ๋Šฅ Service ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ DTO ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฉ”์‹œ์ง€, ์ฝ”๋“œ ์ถ”๊ฐ€ (#31) * Test: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ Controller ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#31) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ ์ž…๋ ฅ ์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง ์ถ”๊ฐ€ (#31) * Test: ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ ์ž…๋ ฅ ์‹œ๊ฐ„ ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#31) --- .../server/entity/InvitationPurpose.java | 11 - .../controller/InvitationController.java | 16 +- .../domain/InvitationErrorCode.java | 7 +- .../invitation/domain/InvitationPurpose.java | 19 ++ .../domain/InvitationValidationMessage.java | 7 + .../invitation/dto/InvitationRequest.java | 80 +++++ .../InvitationReservationMapRepository.java | 7 + .../invitation/service/InvitationService.java | 125 +++++++- .../ReservationQueryRepository.java | 4 + .../ReservationQueryRepositoryImpl.java | 30 ++ .../controller/InvitationControllerTest.java | 96 +++++- .../service/InvitationServiceTest.java | 286 +++++++++++++++++- 12 files changed, 664 insertions(+), 24 deletions(-) delete mode 100644 src/main/java/com/livable/server/entity/InvitationPurpose.java create mode 100644 src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java create mode 100644 src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java create mode 100644 src/main/java/com/livable/server/invitation/dto/InvitationRequest.java create mode 100644 src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java diff --git a/src/main/java/com/livable/server/entity/InvitationPurpose.java b/src/main/java/com/livable/server/entity/InvitationPurpose.java deleted file mode 100644 index 4cafa97b..00000000 --- a/src/main/java/com/livable/server/entity/InvitationPurpose.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.livable.server.entity; - -public enum InvitationPurpose { - - MEETING, - INTERVIEW, - PERIOD_WORK, - SEMINAR, - AFTER_SERVICE, - ETC; -} diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index 07097c3c..0a0b88c4 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -1,13 +1,14 @@ package com.livable.server.invitation.controller; import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.service.InvitationService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; @RequiredArgsConstructor @RequestMapping("/api/invitation") @@ -24,4 +25,13 @@ public ResponseEntity> getAvailab return invitationService.getAvailablePlaces(memberId); } + @PostMapping + public ResponseEntity createInvitation( + @Valid @RequestBody InvitationRequest.CreateDTO dto) { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.createInvitation(dto, memberId); + } + } diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java index 332be1a4..d0c47f9e 100644 --- a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java +++ b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java @@ -9,7 +9,12 @@ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum InvitationErrorCode implements ErrorCode { - MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + INVALID_INTERVIEW_MAXIMUM_NUMBER(HttpStatus.BAD_REQUEST, "๋ฉด์ ‘ ์ดˆ๋Œ€ ๊ฐ€๋Šฅ ์ธ์›์ˆ˜๋Š” 1๋ช…์ž…๋‹ˆ๋‹ค."), + INVALID_DATE(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์‹œ์ž‘ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_TIME(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ์‹œ๊ฐ„์ด ์‹œ์ž‘ ์‹œ๊ฐ„๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_TIME_UNIT(HttpStatus.BAD_REQUEST, "์‹œ๊ฐ„์˜ ๋ถ„ ๋‹จ์œ„๋Š” 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_RESERVATION_COUNT(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋‚ ์งœ ๋˜๋Š” ์‹œ๊ฐ„์— ์˜ˆ์•ฝ๋œ ์žฅ์†Œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java new file mode 100644 index 00000000..0e4049ef --- /dev/null +++ b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java @@ -0,0 +1,19 @@ +package com.livable.server.invitation.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum InvitationPurpose { + + MEETING("ํšŒ์˜"), + INTERVIEW("๋ฉด์ ‘"), + PERIOD_WORK("๊ธฐ๊ฐ„ ๊ทผ๋ฌด"), + SEMINAR("์„ธ๋ฏธ๋‚˜"), + AFTER_SERVICE("AS/์ ๊ฒ€"), + ETC("๊ธฐํƒ€"); + + private final String value; +} diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java new file mode 100644 index 00000000..e463a50a --- /dev/null +++ b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java @@ -0,0 +1,7 @@ +package com.livable.server.invitation.domain; + +public interface InvitationValidationMessage { + String REQUIRED_FUTURE_DATE = "์„ ํƒ๋œ ์‹œ๊ฐ„์ด ํ˜„์žฌ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + String REQUIRED_VISITOR_COUNT = "๋ฐฉ๋ฌธ์ž๋Š” ์ตœ์†Œ 1๋ช… ์ด์ƒ 30๋ช… ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (๋ฉด์ ‘์€ 1๋ช…)"; + String NOT_NULL = "๊ฐ’์ด Null ์ผ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; +} diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java new file mode 100644 index 00000000..ca67b194 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java @@ -0,0 +1,80 @@ +package com.livable.server.invitation.dto; + +import com.livable.server.entity.Invitation; +import com.livable.server.entity.Member; +import com.livable.server.entity.Visitor; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.FutureOrPresent; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.List; + +import static com.livable.server.invitation.domain.InvitationValidationMessage.*; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InvitationRequest { + + @Getter + @Builder + public static class CreateDTO { + + @NotNull(message = NOT_NULL) + private String purpose; + + private Long commonPlaceId; + + @NotNull(message = NOT_NULL) + private String officeName; + + private String description; + + @NotNull(message = NOT_NULL) + @FutureOrPresent(message = REQUIRED_FUTURE_DATE) + private LocalDateTime startDate; + + @NotNull(message = NOT_NULL) + @FutureOrPresent(message = REQUIRED_FUTURE_DATE) + private LocalDateTime endDate; + + @Size(min = 1, max = 30, message = REQUIRED_VISITOR_COUNT) + private List visitors; + + public Invitation toEntity(Member member) { + return Invitation.builder() + .member(member) + .purpose(purpose) + .officeName(officeName) + .description(description) + .startDate(startDate.toLocalDate()) + .endDate(endDate.toLocalDate()) + .startTime(startDate.toLocalTime()) + .endTime(endDate.toLocalTime()) + .build(); + } + } + + @Getter + @Builder + public static class VisitorCreateDTO { + @NotNull + private String name; + + @NotNull + private String contact; + + public Visitor toEntity(Invitation invitation) { + return Visitor.builder() + .invitation(invitation) + .name(name) + .contact(contact) + .firstVisitedTime(null) + .build(); + } + } + +} diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java new file mode 100644 index 00000000..6acdb20e --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.entity.InvitationReservationMap; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InvitationReservationMapRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 9cf913bb..8ab682e7 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -3,24 +3,30 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse; import com.livable.server.core.response.ApiResponse.Success; -import com.livable.server.entity.Invitation; -import com.livable.server.entity.Member; -import com.livable.server.entity.Office; +import com.livable.server.entity.*; import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.domain.InvitationPurpose; import com.livable.server.invitation.dto.InvitationProjection; +import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.repository.InvitationRepository; +import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.repository.MemberRepository; import com.livable.server.invitation.repository.OfficeRepository; import com.livable.server.reservation.repository.ReservationRepository; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.repository.VisitorRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -29,11 +35,14 @@ @RequiredArgsConstructor @Service public class InvitationService { + private static final int INTERVIEW_MAXIMUM_COUNT = 1; private final MemberRepository memberRepository; private final OfficeRepository officeRepository; private final ReservationRepository reservationRepository; private final InvitationRepository invitationRepository; + private final VisitorRepository visitorRepository; + private final InvitationReservationMapRepository invitationReservationMapRepository; public VisitationResponse.InvitationTimeDto findInvitationTime(Long invitationId) { Invitation invitation = invitationRepository.findById(invitationId) @@ -99,10 +108,116 @@ private boolean isNotCombineTarget( } private Long getCompanyIdByMemberId(Long memberId) { - Optional memberOptional = memberRepository.findById(memberId); - Member member = memberOptional.orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); + Member member = findMemberById(memberId); return member.getCompany().getId(); } + @Transactional + public ResponseEntity createInvitation(InvitationRequest.CreateDTO dto, Long memberId) { + checkInterviewVisitorCount(dto); + + Member member = findMemberById(memberId); + Invitation invitation = createInvitation(dto, member); + createVisitors(dto.getVisitors(), invitation); + reserveCommonPlaces(dto, invitation); + + return ApiResponse.success(HttpStatus.CREATED); + } + + /* ๋ฉด์ ‘์˜ ๊ฒฝ์šฐ์—๋Š” 1๋ช…๋งŒ ์ดˆ๋Œ€ ๊ฐ€๋Šฅ */ + private void checkInterviewVisitorCount(InvitationRequest.CreateDTO dto) { + if (dto.getPurpose().equals(InvitationPurpose.INTERVIEW.getValue()) + && dto.getVisitors().size() > INTERVIEW_MAXIMUM_COUNT) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER); + } + } + + private Member findMemberById(Long memberId) { + Optional memberOptional = memberRepository.findById(memberId); + + return memberOptional.orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); + } + + private Invitation createInvitation(InvitationRequest.CreateDTO dto, Member member) { + Invitation invitation = dto.toEntity(member); + + return invitationRepository.save(invitation); + } + + private void createVisitors(List visitorCreateDTOS, Invitation invitation) { + for (InvitationRequest.VisitorCreateDTO visitorCreateDTO : visitorCreateDTOS) { + Visitor visitor = visitorCreateDTO.toEntity(invitation); + visitorRepository.save(visitor); + } + } + + private void reserveCommonPlaces(InvitationRequest.CreateDTO dto, Invitation invitation) { + LocalDateTime startDateTime = dto.getStartDate(); + LocalDateTime endDateTime = dto.getEndDate(); + checkDateTimeValidate(startDateTime, endDateTime); + + if (isReservedCommonPlace(dto.getCommonPlaceId())) { + int expectedReservationCount = getExpectedReservationCount(startDateTime, endDateTime); + List reservations = reservationRepository + .findReservationsByCommonPlaceIdAndStartDateAndEndDate(dto.getCommonPlaceId(), startDateTime, endDateTime); + + checkReservationCount(reservations, expectedReservationCount); + createInvitationReservationMap(reservations, invitation); + } + } + + private void checkDateTimeValidate(LocalDateTime startDateTime, LocalDateTime endDateTime) { + if (endDateTime.toLocalDate().isBefore(startDateTime.toLocalDate())) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_DATE); + } + if (!endDateTime.toLocalTime().isAfter(startDateTime.toLocalTime())) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_TIME); + } + checkTimeUnitValidation(startDateTime.toLocalTime(), endDateTime.toLocalTime()); + } + + private void checkTimeUnitValidation(LocalTime startTime, LocalTime endTime) { + int startMinute = startTime.getMinute(); + int endMinute = endTime.getMinute(); + + if ((startMinute % 30 != 0) || (endMinute % 30 != 0)) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_TIME_UNIT); + } + } + + private boolean isReservedCommonPlace(Long commonPlaceId) { + return commonPlaceId != null; + } + + /* ์ž…๋ ฅ๋œ ์‹œ๊ฐ„ ๋ฒ”์œ„ ๋‚ด์—์„œ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•ด์•ผ ํ•˜๋Š” ์˜ˆ์•ฝ ์ •๋ณด ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ */ + private int getExpectedReservationCount(LocalDateTime startDateTime, LocalDateTime endDateTime) { + LocalDate startDate = startDateTime.toLocalDate(); + LocalDate endDate = endDateTime.toLocalDate(); + LocalTime startTime = startDateTime.toLocalTime(); + LocalTime endTime = endDateTime.toLocalTime(); + + int dayCount = (int) Duration.between(startDate.atStartOfDay(), endDate.atStartOfDay()).toDays() + 1; + int timeCount = (endTime.toSecondOfDay() - startTime.toSecondOfDay()) / 1800; + + return dayCount * timeCount; + } + + /* ์ž…๋ ฅ๋œ ์‹œ์ž‘, ์ข…๋ฃŒ ๋‚ ์งœ์— ๋Œ€ํ•œ ์˜ˆ์•ฝ ์ •๋ณด ๊ฐœ์ˆ˜๊ฐ€ ์˜ˆ์ƒํ•œ ๊ฐ’๊ณผ ๋งž๋Š”์ง€ ํ™•์ธ */ + private void checkReservationCount(List reservations, int count) { + if (reservations.size() != count) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_RESERVATION_COUNT); + } + } + + private void createInvitationReservationMap(List reservations, Invitation invitation) { + for (Reservation reservation : reservations) { + InvitationReservationMap invitationReservationMap = InvitationReservationMap.builder() + .invitation(invitation) + .reservation(reservation) + .build(); + + invitationReservationMapRepository.save(invitationReservationMap); + } + } } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index 61e063bb..635b16bd 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -1,9 +1,13 @@ package com.livable.server.reservation.repository; +import com.livable.server.entity.Reservation; import com.livable.server.invitation.dto.InvitationProjection; +import java.time.LocalDateTime; import java.util.List; public interface ReservationQueryRepository { List findReservationsByCompanyId(Long companyId); + List findReservationsByCommonPlaceIdAndStartDateAndEndDate( + Long commonPlaceId, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index abfcf12c..1eb9f694 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -1,5 +1,6 @@ package com.livable.server.reservation.repository; +import com.livable.server.entity.Reservation; import com.livable.server.invitation.dto.InvitationProjection; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; @@ -7,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; import static com.livable.server.entity.QCommonPlace.commonPlace; @@ -44,4 +46,32 @@ public List findReservationsByCompanyId(Lon .orderBy(reservation.commonPlace.id.asc(), reservation.date.asc(), reservation.time.asc()) .fetch(); } + + @Override + public List findReservationsByCommonPlaceIdAndStartDateAndEndDate( + Long commonPlaceId, LocalDateTime startDateTime, LocalDateTime endDateTime + ) { + + return queryFactory + .selectFrom(reservation) + .where( + reservation.commonPlace.id.eq(commonPlaceId), + reservation.date.between( + startDateTime.toLocalDate(), + endDateTime.toLocalDate() + ), + reservation.time.between( + startDateTime.toLocalTime(), + endDateTime.toLocalTime().minusMinutes(30) + ), + reservation.id.notIn( + JPAExpressions + .select(invitationReservationMap.reservation.id) + .from(invitationReservationMap) + .where(invitationReservationMap.reservation.id.eq(reservation.id)) + ) + ) + .orderBy(reservation.date.asc(), reservation.time.asc()) + .fetch(); + } } diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index 88cd490f..393369a3 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -1,24 +1,31 @@ package com.livable.server.invitation.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse; import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.domain.InvitationValidationMessage; +import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.service.InvitationService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -28,6 +35,9 @@ class InvitationControllerTest { @Autowired MockMvc mockMvc; + @Autowired + ObjectMapper mapper; + @MockBean private InvitationService invitationService; @@ -76,7 +86,7 @@ private List createCommonPlaceDTOList() { @Test void getAvailablePlacesFail_01() throws Exception { // Given - given(invitationService.getAvailablePlaces(ArgumentMatchers.anyLong())) + given(invitationService.getAvailablePlaces(anyLong())) .willThrow(new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); // When & Then @@ -84,4 +94,86 @@ void getAvailablePlacesFail_01() throws Exception { .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(InvitationErrorCode.MEMBER_NOT_EXIST.getMessage())); } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (์‹œ์ž‘ ๋‚ ์งœ๊ฐ€ ์˜ค๋Š˜ ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_01() throws Exception { + // Given + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(2023, 9, 18, 10, 0, 0)) + .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(List.of( + InvitationRequest.VisitorCreateDTO.builder() + .name("ํ™๊ธธ๋™") + .contact("01012341234") + .build() + )) + .build(); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_FUTURE_DATE)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž๊ฐ€ ํ•œ ๋ช…๋„ ์—†๋Š” ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_02() throws Exception { + // Given + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(List.of()) + .build(); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_VISITOR_COUNT)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž๊ฐ€ 31๋ช…์ธ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_03() throws Exception { + // Given + List visitors = new ArrayList<>(); + for (int i = 0; i < 31; i++) { + visitors.add(InvitationRequest.VisitorCreateDTO.builder().build()); + } + + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(visitors) + .build(); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_VISITOR_COUNT)); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index d2cb1219..4be61aa6 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -2,13 +2,18 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.entity.*; import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse.AvailablePlacesDTO; import com.livable.server.invitation.dto.InvitationResponse.CommonPlaceDTO; -import com.livable.server.member.repository.MemberRepository; +import com.livable.server.invitation.repository.InvitationRepository; +import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.invitation.repository.OfficeRepository; -import com.livable.server.reservation.repository.ReservationRepository; import com.livable.server.invitation.service.data.InvitationBasicData; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.repository.ReservationRepository; +import com.livable.server.visitation.repository.VisitorRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,6 +24,7 @@ import org.springframework.http.ResponseEntity; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @@ -27,6 +33,8 @@ import static com.livable.server.invitation.dto.InvitationProjection.ReservationDTO; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) @@ -41,6 +49,15 @@ class InvitationServiceTest { @Mock private ReservationRepository reservationRepository; + @Mock + private InvitationRepository invitationRepository; + + @Mock + private VisitorRepository visitorRepository; + + @Mock + private InvitationReservationMapRepository invitationReservationMapRepository; + @InjectMocks private InvitationService invitationService; @@ -108,6 +125,271 @@ void getAvailablePlacesSuccess_02() { assertThat(combinedItem.getEndTime()).isEqualTo(LocalTime.of(11, 30, 0)); } + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ๋ฉด์ ‘ ์ดˆ๋Œ€ ์ธ์› 2๋ช…") + @Test + void createInvitationFail_01() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .visitors(List.of( + InvitationRequest.VisitorCreateDTO.builder().build(), + InvitationRequest.VisitorCreateDTO.builder().build() + )) + .build(); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์กด์žฌํ•˜์ง€ ์•Š๋Š” Member") + @Test + void createInvitationFail_02() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.empty()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.MEMBER_NOT_EXIST); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์‹œ์ž‘ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_03() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .startDate(LocalDateTime.of(2025, 10, 30, 0, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 29, 0, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_DATE); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์ข…๋ฃŒ ์‹œ๊ฐ„์ด ์‹œ์ž‘ ์‹œ๊ฐ„๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_04() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_TIME); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์ข…๋ฃŒ ์‹œ๊ฐ„์ด ์‹œ์ž‘ ์‹œ๊ฐ„๋ณด๋‹ค ๊ณผ๊ฑฐ์ธ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_05() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .startDate(LocalDateTime.of(2025, 10, 30, 10, 30, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_TIME); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์ž…๋ ฅ๋œ ์‹œ๊ฐ„ ๋ฒ”์œ„์˜ ์˜ˆ์ƒ ์˜ˆ์•ฝ ๊ฐœ์ˆ˜์™€ ์‹ค์ œ ์˜ˆ์•ฝ ๊ฐœ์ˆ˜๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_06() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(1L) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + given(reservationRepository.findReservationsByCommonPlaceIdAndStartDateAndEndDate( + anyLong(), + any(LocalDateTime.class), + any(LocalDateTime.class) + )).willReturn(List.of()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_RESERVATION_COUNT); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์‹œ์ž‘ ์‹œ๊ฐ„ ๋‹จ์œ„๊ฐ€ 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด ์•„๋‹Œ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_07() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(null) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 28, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_TIME_UNIT); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์ข…๋ฃŒ ์‹œ๊ฐ„ ๋‹จ์œ„๊ฐ€ 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด ์•„๋‹Œ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_08() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(null) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 14, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_TIME_UNIT); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์‹œ์ž‘, ์ข…๋ฃŒ ์‹œ๊ฐ„ ๋‹จ์œ„๊ฐ€ 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด ์•„๋‹Œ ๊ฒฝ์šฐ") + @Test + void createInvitationFail_09() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(null) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 28, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 38, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.createInvitation(dto, memberId)); + + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_TIME_UNIT); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์˜ˆ์•ฝ ์žฅ์†Œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void createInvitationSuccess_01() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(1L) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + given(reservationRepository.findReservationsByCommonPlaceIdAndStartDateAndEndDate( + anyLong(), + any(LocalDateTime.class), + any(LocalDateTime.class) + )).willReturn(List.of( + Reservation.builder().build(), // 2025-10-30T10:00:00 + Reservation.builder().build(), // 2025-10-30T10:30:00 + Reservation.builder().build(), // 2025-10-30T11:00:00 + Reservation.builder().build() // 2025-10-30T11:30:00 + )); + given(invitationReservationMapRepository.save(any(InvitationReservationMap.class))) + .willReturn(InvitationReservationMap.builder().build()); + + // When & Then + ResponseEntity result = invitationService.createInvitation(dto, memberId); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CREATED); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ์˜ˆ์•ฝ ์žฅ์†Œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void createInvitationSuccess_02() { + // Given + Long memberId = 1L; + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("๋ฉด์ ‘") + .commonPlaceId(null) + .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) + .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) + .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().build())); + given(invitationRepository.save(any(Invitation.class))).willReturn(Invitation.builder().build()); + given(visitorRepository.save(any(Visitor.class))).willReturn(Visitor.builder().build()); + + // When & Then + ResponseEntity result = invitationService.createInvitation(dto, memberId); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CREATED); + } + private List createReservations() { return new ArrayList<>(List.of( new ReservationDTO( From 2be2985fca11218296f2038c5ad15c403d0acecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Tue, 19 Sep 2023 13:51:42 +0900 Subject: [PATCH 018/111] =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#33) * Feat: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#33) * Feat: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ QueryDSL ๋กœ์ง ๊ตฌํ˜„ (#33) * Feat: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต DTO ๊ตฌํ˜„ (#33) * Feat: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๊ตฌํ˜„ (#33) * Test: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#33) --- .../RestaurantReviewController.java | 11 +++ .../review/dto/RestaurantReviewResponse.java | 22 ++++++ .../RestaurantReviewCustomRepository.java | 2 + .../RestaurantReviewCustomRepositoryImpl.java | 46 ++++++++++++ .../service/RestaurantReviewService.java | 5 ++ .../RestaurantReviewControllerTest.java | 40 +++++++++++ .../service/RestaurantReviewServiceTest.java | 70 +++++++++++++++---- 7 files changed, 183 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java index dbb3ba81..003db4d2 100644 --- a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -31,4 +31,15 @@ public ResponseEntity return ApiResponse.success(list, HttpStatus.OK); } + + @GetMapping("/menus/{menuId}") + public ResponseEntity>> listForMenu( + @PathVariable Long menuId, + @PageableDefault Pageable pageable) { + + Page allListForMenu = + restaurantReviewService.getAllListForMenu(menuId, pageable); + + return ApiResponse.success(allListForMenu, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java index 52ec6b5d..868d162b 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -29,4 +29,26 @@ public static class ListDTO { private Long memberId; private String memberName; } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ListForMenuDTO { + + private Long reviewId; + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private Long restaurantId; + private String restaurantName; + + private Long memberId; + private String memberName; + } } diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java index 5f4c298f..7c45bc3a 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java @@ -7,4 +7,6 @@ public interface RestaurantReviewCustomRepository { Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable); + + Page findRestaurantReviewByMenuId(Long menuId, Pageable pageable); } diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java index 2f6cbba8..5ad0f554 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java @@ -2,6 +2,7 @@ import com.livable.server.entity.*; import com.livable.server.review.dto.RestaurantReviewResponse; +import com.querydsl.core.QueryFactory; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; @@ -13,6 +14,12 @@ import java.util.List; +import static com.livable.server.entity.QMember.member; +import static com.livable.server.entity.QRestaurant.restaurant; +import static com.livable.server.entity.QRestaurantReview.restaurantReview; +import static com.livable.server.entity.QReview.review; +import static com.livable.server.entity.QReviewMenuMap.reviewMenuMap; + @RequiredArgsConstructor public class RestaurantReviewCustomRepositoryImpl implements RestaurantReviewCustomRepository { @@ -62,4 +69,43 @@ public Page findRestaurantReviewByBuildingId(L return new PageImpl<>(content, pageable, total); } + + @Override + public Page findRestaurantReviewByMenuId(Long menuId, Pageable pageable) { + + JPAQuery query = queryFactory + .select(Projections.constructor(RestaurantReviewResponse.ListForMenuDTO.class, + review.id, + review.createdAt, + review.description, + restaurantReview.taste, + restaurantReview.amount, + restaurantReview.service, + restaurantReview.speed, + restaurantReview.restaurant.id, + restaurant.name, + review.member.id, + member.name + )) + .from(review) + .innerJoin(restaurantReview).on(restaurantReview.id.eq(review.id)) + .innerJoin(member).on(review.member.id.eq(member.id)) + .innerJoin(restaurant).on(restaurantReview.restaurant.id.eq(restaurant.id)) + .where(review.id.in( + JPAExpressions + .select(reviewMenuMap.review.id) + .from(reviewMenuMap) + .where(reviewMenuMap.menu.id.eq(menuId)) + )) + .orderBy(review.createdAt.desc()); + + List content = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchJoin().fetch(); + + long total = query.fetchCount(); + + return new PageImpl<>(content, pageable, total); + } } diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java index c89df010..3bad9261 100644 --- a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -18,4 +18,9 @@ public class RestaurantReviewService { public Page getAllList(Long buildingId, Pageable pageable) { return restaurantReviewRepository.findRestaurantReviewByBuildingId(buildingId, pageable); } + + @Transactional(readOnly = true) + public Page getAllListForMenu(Long menuId, Pageable pageable) { + return restaurantReviewRepository.findRestaurantReviewByMenuId(menuId, pageable); + } } diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index c823a1d6..7d18751b 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -69,4 +69,44 @@ void success_Test() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); } } + + @Nested + @DisplayName("ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class listForMenu { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/menus/1"; + + List mockList = List.of( + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(1L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(2L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(3L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(4L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(6L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(5L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(7L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(8L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(9L).build(), + RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(10L).build() + ); + Pageable pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(mockList, pageable, 1); + + Mockito.when(restaurantReviewService + .getAllListForMenu(ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class))) + .thenReturn(mockPage); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.content").isArray()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); + } + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java index 0acdf605..e8f2b7fe 100644 --- a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -19,6 +19,8 @@ import java.util.List; +import static com.livable.server.review.dto.RestaurantReviewResponse.*; + @ExtendWith(MockitoExtension.class) class RestaurantReviewServiceTest { @@ -38,20 +40,20 @@ void success_Test() { // Given Long buildingId = 1L; - List mockList = List.of( - RestaurantReviewResponse.ListDTO.builder().reviewId(1L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(2L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(3L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(4L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(5L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(6L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(7L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(8L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(9L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(10L).build() + List mockList = List.of( + ListDTO.builder().reviewId(1L).build(), + ListDTO.builder().reviewId(2L).build(), + ListDTO.builder().reviewId(3L).build(), + ListDTO.builder().reviewId(4L).build(), + ListDTO.builder().reviewId(5L).build(), + ListDTO.builder().reviewId(6L).build(), + ListDTO.builder().reviewId(7L).build(), + ListDTO.builder().reviewId(8L).build(), + ListDTO.builder().reviewId(9L).build(), + ListDTO.builder().reviewId(10L).build() ); Pageable pageable = PageRequest.of(0, 10); - Page mockPage = new PageImpl<>(mockList, pageable, 1); + Page mockPage = new PageImpl<>(mockList, pageable, 1); Mockito.when(restaurantReviewRepository.findRestaurantReviewByBuildingId( ArgumentMatchers.anyLong(), @@ -59,7 +61,7 @@ void success_Test() { )).thenReturn(mockPage); // When - Page actual = + Page actual = restaurantReviewService.getAllList(buildingId, pageable); // Then @@ -69,4 +71,46 @@ void success_Test() { ); } } + + @Nested + @DisplayName("ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class listForMenu { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + Long menuId = 1L; + + List mockList = List.of( + ListForMenuDTO.builder().reviewId(1L).build(), + ListForMenuDTO.builder().reviewId(2L).build(), + ListForMenuDTO.builder().reviewId(3L).build(), + ListForMenuDTO.builder().reviewId(4L).build(), + ListForMenuDTO.builder().reviewId(5L).build(), + ListForMenuDTO.builder().reviewId(6L).build(), + ListForMenuDTO.builder().reviewId(7L).build(), + ListForMenuDTO.builder().reviewId(8L).build(), + ListForMenuDTO.builder().reviewId(9L).build(), + ListForMenuDTO.builder().reviewId(10L).build() + ); + Pageable pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(mockList, pageable, 1); + + Mockito.when(restaurantReviewRepository.findRestaurantReviewByMenuId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(mockPage); + + // When + Page actual = + restaurantReviewService.getAllListForMenu(menuId, pageable); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(10, actual.getSize()), + () -> Assertions.assertEquals(1,actual.getTotalPages()) + ); + } + } } \ No newline at end of file From c5fcfe597ea66d3662c233b581c9de89aaee92ef Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:50:20 +0900 Subject: [PATCH 019/111] =?UTF-8?q?Invitation=20Repository=20Query=20Test?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20QR=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EC=99=84=EB=A3=8C=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test: JPQL ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (#32) * Refactor: JPQL ์กฐ์ธ ์ฟผ๋ฆฌ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง (#32) ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ ํ…Œ์ŠคํŠธ ์ˆ˜์ • --- .../dto/InvitationDetailTimeDto.java | 16 +++ .../repository/InvitationRepository.java | 12 ++ .../invitation/service/InvitationService.java | 7 +- .../visitation/dto/VisitationResponse.java | 11 +- .../service/VisitationFacadeService.java | 4 +- .../repository/InvitationRepositoryTest.java | 108 ++++++++++++++++++ .../mock/MockInvitationDetailTimeDto.java | 29 +++++ .../service/InvitationServiceTest.java | 49 +++----- .../service/VisitationFacadeServiceTest.java | 2 - 9 files changed, 193 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java create mode 100644 src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java create mode 100644 src/test/java/com/livable/server/visitation/mock/MockInvitationDetailTimeDto.java diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java b/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java new file mode 100644 index 00000000..dcc04fe3 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java @@ -0,0 +1,16 @@ +package com.livable.server.invitation.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public interface InvitationDetailTimeDto { + + LocalDate getStartDate(); + + LocalDate getEndDate(); + + LocalTime getStartTime(); + + LocalTime getEndTime(); +} diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java index 176c6ffb..d19bda88 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java @@ -1,7 +1,19 @@ package com.livable.server.invitation.repository; import com.livable.server.entity.Invitation; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface InvitationRepository extends JpaRepository { + + @Query("select i.startTime as startTime, i.endTime as endTime, i.startDate as startDate, i.endDate as endDate" + + " from Visitor v" + + " join fetch Invitation i" + + " on v.invitation = i" + + " where v.id = :visitorId") + Optional findInvitationDetailTimeByVisitorId(@Param("visitorId") Long visitorId); } diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 8ab682e7..45aa712f 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -5,6 +5,7 @@ import com.livable.server.core.response.ApiResponse.Success; import com.livable.server.entity.*; import com.livable.server.invitation.domain.InvitationErrorCode; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; import com.livable.server.invitation.domain.InvitationPurpose; import com.livable.server.invitation.dto.InvitationProjection; import com.livable.server.invitation.dto.InvitationRequest; @@ -44,11 +45,11 @@ public class InvitationService { private final VisitorRepository visitorRepository; private final InvitationReservationMapRepository invitationReservationMapRepository; - public VisitationResponse.InvitationTimeDto findInvitationTime(Long invitationId) { - Invitation invitation = invitationRepository.findById(invitationId) + public VisitationResponse.InvitationTimeDto findInvitationTime(Long visitorId) { + InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); - return VisitationResponse.InvitationTimeDto.from(invitation); + return VisitationResponse.InvitationTimeDto.from(invitationDetailTimeDto); } @Transactional(readOnly = true) diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java index 9da585c2..ea3c830b 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -2,6 +2,7 @@ import com.beust.ah.A; import com.livable.server.entity.Invitation; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; import lombok.*; import java.time.LocalDate; @@ -29,12 +30,12 @@ public LocalDateTime getEndDateTime() { return LocalDateTime.of(endDate, endTime); } - public static InvitationTimeDto from(Invitation invitation) { + public static InvitationTimeDto from(InvitationDetailTimeDto invitationDetailTimeDto) { return InvitationTimeDto.builder() - .startTime(invitation.getStartTime()) - .endTime(invitation.getEndTime()) - .startDate(invitation.getStartDate()) - .endDate(invitation.getEndDate()) + .startTime(invitationDetailTimeDto.getStartTime()) + .endTime(invitationDetailTimeDto.getEndTime()) + .startDate(invitationDetailTimeDto.getStartDate()) + .endDate(invitationDetailTimeDto.getEndDate()) .build(); } } diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index c45e142b..055aa7bd 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -13,11 +13,9 @@ public class VisitationFacadeService { private final VisitationService visitationService; private final InvitationService invitationService; - private final VisitorService visitorService; public String createQrCode(Long visitorId) { - Long invitationId = visitorService.findInvitationId(visitorId); - VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(invitationId); + VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(visitorId); return visitationService.createQrCode(invitationTime.getStartDateTime(), invitationTime.getEndDateTime()); } diff --git a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java new file mode 100644 index 00000000..cddf4570 --- /dev/null +++ b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java @@ -0,0 +1,108 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.*; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.time.LocalTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DataJpaTest +@Import(QueryDslConfig.class) +class InvitationRepositoryTest { + + public static final LocalDate START_DATE = LocalDate.now(); + public static final LocalTime START_TIME = LocalTime.of(1, 10); + public static final LocalTime END_TIME = LocalTime.of(1, 20); + public static final LocalDate END_DATE = LocalDate.now(); + @Autowired + EntityManager entityManager; + + @Autowired + InvitationRepository invitationRepository; + + @BeforeEach + void dataInit() { + Building building = Building.builder() + .name("63๋นŒ๋”ฉ") + .scale("์ง€ํ•˜ 3์ธต, ์ง€์ƒ 63์ธต") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10๋ถ„๋‹น 1000์›") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 61๊ธธ 7, 392") + .subwayStation("์„์ดŒ์—ญ") + .build(); + + entityManager.persist(building); + + Company company = Company.builder() + .name("ํŒจ์ŠคํŠธ์บ ํผ์Šค") + .building(building) + .build(); + + entityManager.persist(company); + + Member member = Member.builder() + .company(company) + .contact("01012345678") + .name("๊น€ํ›ˆ์„ญ") + .email("test@naver.com") + .employeeNumber("9q0mavfdmmpoaskp") + .profileImageUrl("./profileImageUrl") + .password("1234") + .role(Role.USER) + .build(); + + entityManager.persist(member); + + Invitation invitation = Invitation.builder() + .member(member) + .endDate(END_DATE) + .startDate(START_DATE) + .startTime(START_TIME) + .endTime(END_TIME) + .description("์•Œ์•„์„œ ์™€") + .purpose("INTERVIEW") + .officeName("ํŒจ์ŠคํŠธ์บ ํผ์Šค ์‚ฌ๋ฌด์‹ค") + .build(); + + entityManager.persist(invitation); + + Visitor visitor = Visitor.builder() + .invitation(invitation) + .name("์ตœํƒœ์œค") + .contact("01034567811") + .build(); + + entityManager.persist(visitor); + } + + @DisplayName("InvitationRepository.findInvitationDetailTimeByVisitorId ์ฟผ๋ฆฌ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findInvitationDetailTimeByVisitorIdSuccessTest() { + InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(1L) + .get(); + + assertAll( + () -> assertThat(START_TIME).isEqualTo(invitationDetailTimeDto.getStartTime()), + () -> assertThat(END_TIME).isEqualTo(invitationDetailTimeDto.getEndTime()), + () -> assertThat(START_DATE).isEqualTo(invitationDetailTimeDto.getStartDate()), + () -> assertThat(END_DATE).isEqualTo(invitationDetailTimeDto.getEndDate()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/mock/MockInvitationDetailTimeDto.java b/src/test/java/com/livable/server/visitation/mock/MockInvitationDetailTimeDto.java new file mode 100644 index 00000000..1322280d --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockInvitationDetailTimeDto.java @@ -0,0 +1,29 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.invitation.dto.InvitationDetailTimeDto; + +import java.time.LocalDate; +import java.time.LocalTime; + +public class MockInvitationDetailTimeDto implements InvitationDetailTimeDto { + + @Override + public LocalDate getStartDate() { + return LocalDate.now(); + } + + @Override + public LocalDate getEndDate() { + return LocalDate.now(); + } + + @Override + public LocalTime getStartTime() { + return LocalTime.of(1, 10); + } + + @Override + public LocalTime getEndTime() { + return LocalTime.of(1, 20); + } +} diff --git a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java index 02fdc7df..b1e67432 100644 --- a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java @@ -2,10 +2,12 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Invitation; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; import com.livable.server.invitation.repository.InvitationRepository; +import com.livable.server.visitation.mock.MockInvitationDetailTimeDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,28 +40,23 @@ class InvitationServiceTest { void findInvitationTimeSuccessTest() { // Given + MockInvitationDetailTimeDto mockInvitationDetailTimeDto = new MockInvitationDetailTimeDto(); Invitation invitation = Invitation.builder() - .startTime(LocalTime.of(1, 1)) - .endTime(LocalTime.of(1, 1)) - .startDate(LocalDate.of(2023, 9, 18)) - .endDate(LocalDate.of(2023, 9, 18)) + .startTime(mockInvitationDetailTimeDto.getStartTime()) + .endTime(mockInvitationDetailTimeDto.getEndTime()) + .startDate(mockInvitationDetailTimeDto.getStartDate()) + .endDate(mockInvitationDetailTimeDto.getEndDate()) .build(); - VisitationResponse.InvitationTimeDto invitationTimeDto = VisitationResponse.InvitationTimeDto.builder() - .startTime(LocalTime.of(1, 1)) - .endTime(LocalTime.of(1, 1)) - .startDate(LocalDate.of(2023, 9, 18)) - .endDate(LocalDate.of(2023, 9, 18)) - .build(); - - given(invitationRepository.findById(anyLong())).willReturn(Optional.of(invitation)); + given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())) + .willReturn(Optional.of(mockInvitationDetailTimeDto)); // When - VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(anyLong()); + VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(1L); // Then - then(invitationRepository).should(times(1)).findById(anyLong()); - assertThat(invitationTime).usingRecursiveComparison().isEqualTo(invitationTimeDto); + then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); + assertThat(invitationTime).usingRecursiveComparison().isEqualTo(invitation); } @DisplayName("InvitationService.findInvitationTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") @@ -67,27 +64,15 @@ void findInvitationTimeSuccessTest() { void findInvitationTimeFailTest() { // Given - Invitation invitation = Invitation.builder() - .startTime(LocalTime.of(1, 1)) - .endTime(LocalTime.of(1, 1)) - .startDate(LocalDate.of(2023, 9, 18)) - .endDate(LocalDate.of(2023, 9, 18)) - .build(); - - VisitationResponse.InvitationTimeDto invitationTimeDto = VisitationResponse.InvitationTimeDto.builder() - .startTime(LocalTime.of(1, 1)) - .endTime(LocalTime.of(1, 1)) - .startDate(LocalDate.of(2023, 9, 18)) - .endDate(LocalDate.of(2023, 9, 18)) - .build(); - - given(invitationRepository.findById(anyLong())).willReturn(Optional.empty()); + given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())).willReturn(Optional.empty()); // When - GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> invitationService.findInvitationTime(anyLong())); + GlobalRuntimeException globalRuntimeException = assertThrows( + GlobalRuntimeException.class, () -> invitationService.findInvitationTime(anyLong()) + ); // Then assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); - then(invitationRepository).should(times(1)).findById(anyLong()); + then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 2cb55a4a..41c7e688 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -46,7 +46,6 @@ void createQrCodeSuccessTest() { .endTime(LocalTime.now()) .build(); - given(visitorService.findInvitationId(anyLong())).willReturn(1L); given(invitationService.findInvitationTime(anyLong())).willReturn(invitationTimeDto); given(visitationService.createQrCode(any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(QR_CODE); @@ -55,7 +54,6 @@ void createQrCodeSuccessTest() { // Then assertThat(qrCode).isEqualTo(QR_CODE); - then(visitorService).should(times(1)).findInvitationId(anyLong()); then(invitationService).should(times(1)).findInvitationTime(anyLong()); then(visitationService).should(times(1)).createQrCode(any(LocalDateTime.class), any(LocalDateTime.class)); } From 762506888323f242882a148c6e47034ac713a3de Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:50:52 +0900 Subject: [PATCH 020/111] =?UTF-8?q?=EC=A3=BC=EC=B0=A8=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ์ฃผ์ฐจ๋“ฑ๋ก API ์ปจํŠธ๋กค๋Ÿฌ ๋ฐ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Test: ์ฃผ์ฐจ๋“ฑ๋ก ์ •๊ทœ์‹ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Feat: ParkingLogRepository ๊ตฌํ˜„ (#28) * Test: ParkingLogRepository ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) ์ž‘์„ฑํ•œ JPQL ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ * Modify: ParkingLog์— Visitor์— ๋Œ€ํ•œ ๋งคํ•‘ ์–ด๋…ธํ…Œ์ด์…˜ ์ˆ˜์ • (#28) @ManyToOne -> @OneToOne * Feat: ์ฃผ์ฐจ ๋“ฑ๋ก ๋กœ์ง ๊ตฌํ˜„ (#28) * Test: VisitationFacadeService.registerParking ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Test: VisitationFacadeService.registerParking ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Test: ParkingLogService.findParkingLogByVisitorId ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Test: ParkingLogService.registerParkingLog ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#28) * Feat: Validation Error Message ์ถ”๊ฐ€ (#28) --- .../com/livable/server/entity/ParkingLog.java | 2 +- .../InvitationValidationGuideMessage.java | 6 + .../controller/VisitationController.java | 13 ++ .../domain/VisitationErrorCode.java | 3 +- .../visitation/dto/VisitationRequest.java | 9 ++ .../repository/ParkingLogRepository.java | 15 +++ .../visitation/service/ParkingLogService.java | 29 +++++ .../service/VisitationFacadeService.java | 21 ++++ .../visitation/service/VisitorService.java | 6 + .../controller/VisitationControllerTest.java | 27 ++++ .../visitation/dto/VisitationRequestTest.java | 51 ++++++++ .../mock/MockRegisterParkingDto.java | 17 +++ .../repository/ParkingLogRepositoryTest.java | 118 ++++++++++++++++++ .../service/ParkingLogServiceTest.java | 69 ++++++++++ .../service/VisitationFacadeServiceTest.java | 50 ++++++++ 15 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java create mode 100644 src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java create mode 100644 src/main/java/com/livable/server/visitation/service/ParkingLogService.java create mode 100644 src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java create mode 100644 src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java create mode 100644 src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java create mode 100644 src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java diff --git a/src/main/java/com/livable/server/entity/ParkingLog.java b/src/main/java/com/livable/server/entity/ParkingLog.java index 044ea298..4bf5b9a7 100644 --- a/src/main/java/com/livable/server/entity/ParkingLog.java +++ b/src/main/java/com/livable/server/entity/ParkingLog.java @@ -17,7 +17,7 @@ public class ParkingLog extends BaseTimeEntity { private Long id; @JoinColumn(nullable = false, unique = true) - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) private Visitor visitor; @Column(nullable = false) diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java new file mode 100644 index 00000000..6f8df1ec --- /dev/null +++ b/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java @@ -0,0 +1,6 @@ +package com.livable.server.invitation.domain; + +public interface InvitationValidationGuideMessage { + + String INVALID_CAR_NUMBER = "์ฐจ๋Ÿ‰ ๋ฒˆํ˜ธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."; +} diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index 33f61f8d..c413446e 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -9,6 +9,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; + @RequiredArgsConstructor @RestController @RequestMapping("/api/visitation") @@ -32,4 +34,15 @@ public ResponseEntity> validateQrCode(@RequestBody V return ApiResponse.success(HttpStatus.OK); } + + @PostMapping("/parking") + public ResponseEntity> registerParking( + @RequestBody @Valid VisitationRequest.RegisterParkingDto registerParkingDto + ) { + Long visitorId = 1L; + visitationFacadeService.registerParking(visitorId, registerParkingDto.getCarNumber()); + + return ApiResponse.success(HttpStatus.CREATED); + } + } diff --git a/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java index 43388cfb..a66bf63e 100644 --- a/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java +++ b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java @@ -15,7 +15,8 @@ public enum VisitationErrorCode implements ErrorCode { IO(HttpStatus.INTERNAL_SERVER_ERROR, "I/O๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ณผ์ •์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), INVALID_QR_PERIOD(HttpStatus.BAD_REQUEST, "QR์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค."), INVALID_PERIOD(HttpStatus.BAD_REQUEST, "์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ์‹œ๊ฐ„์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), - NOT_FOUND(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + NOT_FOUND(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + ALREADY_REGISTER_PARKING(HttpStatus.BAD_REQUEST, "์ด๋ฏธ ์ฃผ์ฐจ ๋“ฑ๋ก์„ ์™„๋ฃŒํ•˜์˜€์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java index aa6c5c01..6ea73d65 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java @@ -1,9 +1,12 @@ package com.livable.server.visitation.dto; +import com.livable.server.invitation.domain.InvitationValidationGuideMessage; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.constraints.Pattern; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VisitationRequest { @@ -11,4 +14,10 @@ public class VisitationRequest { public static class ValidateQrDto { private String qr; } + + @Getter + public static class RegisterParkingDto { + @Pattern(regexp = "^\\d{2,3}[๊ฐ€-ํžฃ]{1}\\d{4}$", message = InvitationValidationGuideMessage.INVALID_CAR_NUMBER) + private String carNumber; + } } diff --git a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java new file mode 100644 index 00000000..019495dd --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java @@ -0,0 +1,15 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.entity.ParkingLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface ParkingLogRepository extends JpaRepository { + + @Query("select p from ParkingLog p" + + " where p.visitor.id = :visitorId") + Optional findParkingLogByVisitorId(@Param("visitorId") Long visitorId); +} diff --git a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java new file mode 100644 index 00000000..9382a9df --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java @@ -0,0 +1,29 @@ +package com.livable.server.visitation.service; + +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.visitation.repository.ParkingLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class ParkingLogService { + + private final ParkingLogRepository parkingLogRepository; + + public Optional findParkingLogByVisitorId(Long visitorId) { + return parkingLogRepository.findParkingLogByVisitorId(visitorId); + } + + public void registerParkingLog(Visitor visitor, String carNumber) { + ParkingLog parkingLog = ParkingLog.builder() + .carNumber(carNumber) + .visitor(visitor) + .build(); + + parkingLogRepository.save(parkingLog); + } +} diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 055aa7bd..1e0d79f4 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -1,11 +1,17 @@ package com.livable.server.visitation.service; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; import com.livable.server.invitation.service.InvitationService; +import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Service @Transactional(readOnly = true) @@ -13,6 +19,8 @@ public class VisitationFacadeService { private final VisitationService visitationService; private final InvitationService invitationService; + private final VisitorService visitorService; + private final ParkingLogService parkingLogService; public String createQrCode(Long visitorId) { VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(visitorId); @@ -23,4 +31,17 @@ public String createQrCode(Long visitorId) { public void validateQrCode(String qr) { visitationService.validateQrCode(qr); } + + @Transactional + public void registerParking(Long visitorId, String carNumber) { + validateDuplicationRegister(visitorId); + Visitor visitor = visitorService.findById(visitorId); + parkingLogService.registerParkingLog(visitor, carNumber); + } + + private void validateDuplicationRegister(Long visitorId) { + if (parkingLogService.findParkingLogByVisitorId(visitorId).isPresent()) { + throw new GlobalRuntimeException(VisitationErrorCode.ALREADY_REGISTER_PARKING); + } + } } diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java index 0084daa5..da55c4c3 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitorService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -1,6 +1,7 @@ package com.livable.server.visitation.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Visitor; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; import lombok.RequiredArgsConstructor; @@ -18,4 +19,9 @@ public Long findInvitationId(Long visitorId) { .getInvitation() .getId(); } + + public Visitor findById(Long visitorId) { + return visitorRepository.findById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + } } diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 932044ce..237a48ce 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -5,6 +5,7 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.mock.MockRegisterParkingDto; import com.livable.server.visitation.mock.ValidateQrCodeSuccessMockRequest; import com.livable.server.visitation.service.VisitationFacadeService; import org.junit.jupiter.api.DisplayName; @@ -103,4 +104,30 @@ void validateQrCodeFail() throws Exception { then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); } + + @DisplayName("[POST][/api/visitation/parking] - ์ฐจ๋Ÿ‰ ๋“ฑ๋ก ์„ฑ๊ณต") + @Test + void registerParking() throws Exception { + // given + String carNumber = "12๊ฐ€1234"; + MockRegisterParkingDto mockRegisterParkingDto = new MockRegisterParkingDto(carNumber); + Long visitorId = 1L; + + + willDoNothing().given(visitationFacadeService).registerParking(visitorId, mockRegisterParkingDto.getCarNumber()); + + // when + ResultActions resultActions = mockMvc.perform( + post("/api/visitation/parking") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(mockRegisterParkingDto)) + ); + + // then + resultActions.andExpect(status().isCreated()); + + then(visitationFacadeService) + .should(times(1)) + .registerParking(visitorId, mockRegisterParkingDto.getCarNumber()); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java new file mode 100644 index 00000000..fb8fd399 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java @@ -0,0 +1,51 @@ +package com.livable.server.visitation.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class VisitationRequestTest { + + private static ValidatorFactory factory; + private static Validator validator; + + private static ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeAll + public static void init() { + factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber ์ •๊ทœ์‹ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @CsvSource({"12๊ฐ€1234", "12ํซ3456", "123ํ•ก0000"}) + @ParameterizedTest(name = "[{index}] ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ: {0}") + void carNumberPatternSuccessTest(String carNumber) throws JsonProcessingException { + String requestDto = "{\n" + + " \"carNumber\": \"" + carNumber + "\"\n" + + " }"; + + VisitationRequest.RegisterParkingDto registerParkingDto = + objectMapper.readValue(requestDto, VisitationRequest.RegisterParkingDto.class); + + Set> validate = validator.validate(registerParkingDto); + + assertThat(validate.size()).isEqualTo(0); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java b/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java new file mode 100644 index 00000000..fb70b815 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java @@ -0,0 +1,17 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.visitation.dto.VisitationRequest; + +public class MockRegisterParkingDto extends VisitationRequest.RegisterParkingDto { + + private final String carNumber; + + public MockRegisterParkingDto(String carNumber) { + this.carNumber = carNumber; + } + + @Override + public String getCarNumber() { + return carNumber; + } +} diff --git a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java new file mode 100644 index 00000000..3be1a726 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java @@ -0,0 +1,118 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@Import(QueryDslConfig.class) +class ParkingLogRepositoryTest { + + public static final LocalDate START_DATE = LocalDate.now(); + public static final LocalTime START_TIME = LocalTime.of(1, 10); + public static final LocalTime END_TIME = LocalTime.of(1, 20); + public static final LocalDate END_DATE = LocalDate.now(); + + @Autowired + ParkingLogRepository parkingLogRepository; + + @Autowired + VisitorRepository visitorRepository; + + @Autowired + EntityManager entityManager; + + @BeforeEach + void dataInit() { + Building building = Building.builder() + .name("63๋นŒ๋”ฉ") + .scale("์ง€ํ•˜ 3์ธต, ์ง€์ƒ 63์ธต") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10๋ถ„๋‹น 1000์›") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 61๊ธธ 7, 392") + .subwayStation("์„์ดŒ์—ญ") + .build(); + + entityManager.persist(building); + + Company company = Company.builder() + .name("ํŒจ์ŠคํŠธ์บ ํผ์Šค") + .building(building) + .build(); + + entityManager.persist(company); + + Member member = Member.builder() + .company(company) + .contact("01012345678") + .name("๊น€ํ›ˆ์„ญ") + .email("test@naver.com") + .employeeNumber("9q0mavfdmmpoaskp") + .profileImageUrl("./profileImageUrl") + .password("1234") + .role(Role.USER) + .build(); + + entityManager.persist(member); + + Invitation invitation = Invitation.builder() + .member(member) + .endDate(END_DATE) + .startDate(START_DATE) + .startTime(START_TIME) + .endTime(END_TIME) + .description("์•Œ์•„์„œ ์™€") + .purpose("INTERVIEW") + .officeName("ํŒจ์ŠคํŠธ์บ ํผ์Šค ์‚ฌ๋ฌด์‹ค") + .build(); + + entityManager.persist(invitation); + + Visitor visitor = Visitor.builder() + .invitation(invitation) + .name("์ตœํƒœ์œค") + .contact("01034567811") + .build(); + + entityManager.persist(visitor); + + ParkingLog parkingLog = ParkingLog.builder() + .visitor(visitor) + .carNumber("12๊ฐ€1234") + .build(); + + entityManager.persist(parkingLog); + } + + @DisplayName("ParkingLogRepository.findParkingLogByVisitorId ์ฟผ๋ฆฌ ํ™•์ธ์šฉ ํ…Œ์ŠคํŠธ") + @Test + void test() { + ParkingLog parkingLog = parkingLogRepository.findParkingLogByVisitorId(1L).get(); + Visitor visitor = visitorRepository.findById(1L).get(); + + assertAll( + () -> assertThat(parkingLog.getId()).isEqualTo(1L), + () -> assertThat(parkingLog.getVisitor()).isEqualTo(visitor), + () -> assertThat(parkingLog.getCarNumber()).isEqualTo("12๊ฐ€1234") + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java new file mode 100644 index 00000000..9d573cf0 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java @@ -0,0 +1,69 @@ +package com.livable.server.visitation.service; + +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.visitation.repository.ParkingLogRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class ParkingLogServiceTest { + + @InjectMocks + ParkingLogService parkingLogService; + + @Mock + ParkingLogRepository parkingLogRepository; + + @DisplayName("ParkingLogService.findParkingLogByVisitorId ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findParkingLogByVisitorIdSuccessTest() { + + // Given + ParkingLog parkingLog = ParkingLog.builder() + .build(); + + Optional expectedOptionalParkingLog = Optional.of(parkingLog); + + given(parkingLogRepository.findParkingLogByVisitorId(anyLong())).willReturn(Optional.of(parkingLog)); + + // When + Optional optionalParkingLog = parkingLogService.findParkingLogByVisitorId(1L); + + // Then + assertThat(optionalParkingLog).isEqualTo(expectedOptionalParkingLog); + then(parkingLogRepository).should(times(1)).findParkingLogByVisitorId(anyLong()); + } + + @DisplayName("ParkingLogService.registerParkingLog ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void registerParkingLogSuccessTest() { + ParkingLog parkingLog = ParkingLog.builder() + .build(); + + Visitor visitor = Visitor.builder() + .build(); + + String carNumber = "testCarNumber"; + + // Given + given(parkingLogRepository.save(any())).willReturn(parkingLog); + + // When + parkingLogService.registerParkingLog(visitor, carNumber); + + // Then + then(parkingLogRepository).should(times(1)).save(any()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 41c7e688..f41fed85 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -1,6 +1,11 @@ package com.livable.server.visitation.service; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.service.InvitationService; +import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,6 +17,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -30,11 +36,16 @@ class VisitationFacadeServiceTest { @Mock VisitationService visitationService; + @Mock InvitationService invitationService; + @Mock VisitorService visitorService; + @Mock + ParkingLogService parkingLogService; + @DisplayName("VisitationFacadeService.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void createQrCodeSuccessTest() { @@ -71,4 +82,43 @@ void validateQrCodeSuccessTest() { // Then then(visitationService).should(times(1)).validateQrCode(anyString()); } + + @DisplayName("VisitationFacadeService.registerParking ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void registerParkingSuccessTest() { + // Given + Visitor visitor = Visitor.builder() + .id(1L) + .build(); + given(visitorService.findById(anyLong())).willReturn(visitor); + given(parkingLogService.findParkingLogByVisitorId(any())).willReturn(Optional.empty()); + willDoNothing().given(parkingLogService).registerParkingLog(any(), anyString()); + + // When + visitationFacadeService.registerParking(visitor.getId(), "12๊ฐ€1234"); + + // Then + then(visitorService).should(times(1)).findById(anyLong()); + then(parkingLogService).should(times(1)).findParkingLogByVisitorId(any()); + then(parkingLogService).should(times(1)).registerParkingLog(any(), anyString()); + } + + @DisplayName("VisitationFacadeService.registerParking ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void registerParkingFailTest() { + // Given + ParkingLog parkingLog = ParkingLog.builder() + .build(); + given(parkingLogService.findParkingLogByVisitorId(any())).willReturn(Optional.of(parkingLog)); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows( + GlobalRuntimeException.class, + () -> visitationFacadeService.registerParking(any(), "12๊ฐ€1234") + ); + + // Then + then(parkingLogService).should(times(1)).findParkingLogByVisitorId(any()); + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.ALREADY_REGISTER_PARKING); + } } \ No newline at end of file From 642ef6d6fe8762055a2dd5706c7176b6c4fbe123 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:55:52 +0900 Subject: [PATCH 021/111] =?UTF-8?q?GlobalRuntimeException=20=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=20=EC=83=9D=EC=84=B1=EC=9E=90=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify: ๋ถ€๋ชจ ์ƒ์„ฑ์ž ํ˜ธ์ถœ ์‹œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ (#43) --- .../livable/server/core/exception/GlobalRuntimeException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java index b726228a..7b27b17d 100644 --- a/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java +++ b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java @@ -8,7 +8,7 @@ public class GlobalRuntimeException extends RuntimeException { private final ErrorCode errorCode; public GlobalRuntimeException(ErrorCode errorCode) { - super(); + super(errorCode.getMessage()); this.errorCode = errorCode; } } From b45267e7ae3ebf00550ffe68f50d1bc6c89f05f9 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:22:43 +0900 Subject: [PATCH 022/111] =?UTF-8?q?@DataJpaTest=20PersistContext=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=8C=A8=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: ํ…Œ์ŠคํŠธ ์‹œ PersistContext ๊ณต์œ ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ ํ•ด๊ฒฐ (#47) --- .../server/invitation/repository/InvitationRepositoryTest.java | 2 ++ .../server/visitation/repository/ParkingLogRepositoryTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java index cddf4570..f6c9b694 100644 --- a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java +++ b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; import javax.persistence.EntityManager; import java.time.LocalDate; @@ -20,6 +21,7 @@ @DataJpaTest @Import(QueryDslConfig.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) class InvitationRepositoryTest { public static final LocalDate START_DATE = LocalDate.now(); diff --git a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java index 3be1a726..5ed63af3 100644 --- a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java +++ b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; import javax.persistence.EntityManager; @@ -20,6 +21,7 @@ @DataJpaTest @Import(QueryDslConfig.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) class ParkingLogRepositoryTest { public static final LocalDate START_DATE = LocalDate.now(); From f3e4a9103e0c2573b409b7721d7a33434bc0de39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Tue, 19 Sep 2023 19:10:00 +0900 Subject: [PATCH 023/111] =?UTF-8?q?Amazon=20S3=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: Amazon S3 ์˜์กด์„ฑ ์ถ”๊ฐ€ (#40) * Feat: Amazon S3 ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค ๊ตฌํ˜„ (#40) * Feat: Amazon S3 ํŒŒ์ผ ์ €์žฅ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ (#40) --- build.gradle | 1 + .../livable/server/core/config/S3Config.java | 34 +++++++++ .../livable/server/core/util/S3Uploader.java | 75 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/main/java/com/livable/server/core/config/S3Config.java create mode 100644 src/main/java/com/livable/server/core/util/S3Uploader.java diff --git a/build.gradle b/build.gradle index 98f2c98d..a35c6a2d 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' implementation 'com.google.zxing:core:3.5.2' implementation 'com.google.zxing:javase:3.5.2' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/src/main/java/com/livable/server/core/config/S3Config.java b/src/main/java/com/livable/server/core/config/S3Config.java new file mode 100644 index 00000000..2a633de0 --- /dev/null +++ b/src/main/java/com/livable/server/core/config/S3Config.java @@ -0,0 +1,34 @@ +package com.livable.server.core.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} diff --git a/src/main/java/com/livable/server/core/util/S3Uploader.java b/src/main/java/com/livable/server/core/util/S3Uploader.java new file mode 100644 index 00000000..75d4a427 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/S3Uploader.java @@ -0,0 +1,75 @@ +package com.livable.server.core.util; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@Component +public class S3Uploader { + + private static final List ALLOWED_EXTENSIONS = List.of("jpg", "png", "gif", "jpeg"); + private static final String EXTENSION_SEPARATOR = "."; + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public List saveFile(List files) throws IOException { + + List accessUrls = new ArrayList<>(); + + for (MultipartFile file : files) { + String accessUrl = saveFile(file); + accessUrls.add(accessUrl); + } + return accessUrls; + } + + public String saveFile(MultipartFile file) throws IOException { + + final String originalFileName = file.getOriginalFilename(); + assert originalFileName != null; + final String fileExtension = getFileExtension(originalFileName); + + validationAllowedFileExtension(fileExtension); + String randomFileName = generateRandomFileName(originalFileName, fileExtension); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + amazonS3.putObject(bucket, randomFileName, file.getInputStream(), metadata); + + return amazonS3.getUrl(bucket, randomFileName).toString(); + } + + // ๋žœ๋คํ•œ ํŒŒ์ผ ์ด๋ฆ„์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + private String generateRandomFileName(String originalFileName, String fileExtension) { + return UUID.randomUUID() + originalFileName + EXTENSION_SEPARATOR + fileExtension; + } + + // ํŒŒ์ผ ์ด๋ฆ„์—์„œ ํŒŒ์ผ ํ™•์žฅ์ž๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฉ”์„œ๋“œ + private String getFileExtension(String originalFileName) { + return originalFileName + .substring(originalFileName.lastIndexOf(EXTENSION_SEPARATOR) + 1) + .toLowerCase(); + + } + + // ํŒŒ์ผ ํ™•์žฅ์ž๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฉ”์„œ๋“œ + private void validationAllowedFileExtension(String fileExtension) { + if (!ALLOWED_EXTENSIONS.contains(fileExtension)) { + throw new IllegalArgumentException("์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ™•์žฅ์ž ์ž…๋‹ˆ๋‹ค."); + } + } +} From 100ba27c4042ae26f5240b412e4e4d81a74c6b48 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Tue, 19 Sep 2023 23:01:43 +0900 Subject: [PATCH 024/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#41) * Feat: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก ์š”์ฒญ DTO ๊ตฌํ˜„ (#41) * Feat: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก Repository ์ƒ์„ฑ ๋ฐ QueryDsl ๊ตฌํ˜„ (#41) * Feat: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก Service ๋กœ์ง ๊ตฌํ˜„ (#41) * Refactor: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก ์ฟผ๋ฆฌ ๋‚ ์งœ, ์ •๋ ฌ ์กฐ๊ฑด ์ถ”๊ฐ€ (#41) --- .../controller/InvitationController.java | 9 +++ .../invitation/dto/InvitationResponse.java | 14 +++++ .../repository/InvitationQueryRepository.java | 9 +++ .../InvitationQueryRepositoryImpl.java | 56 +++++++++++++++++++ .../repository/InvitationRepository.java | 3 +- .../invitation/service/InvitationService.java | 8 +++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java create mode 100644 src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index 0a0b88c4..eb3c74c1 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.util.List; @RequiredArgsConstructor @RequestMapping("/api/invitation") @@ -34,4 +35,12 @@ public ResponseEntity createInvitation( return invitationService.createInvitation(dto, memberId); } + @GetMapping + public ResponseEntity>> getInvitations() { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.getInvitations(memberId); + } + } diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java index 33631c3c..12accd08 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java @@ -1,5 +1,6 @@ package com.livable.server.invitation.dto; +import com.livable.server.entity.Invitation; import com.livable.server.entity.Office; import lombok.*; @@ -60,4 +61,17 @@ private static String getFormattedPlaceName(String name, String floor, String ro return String.format("%s (%s์ธต %sํ˜ธ)", name, floor, roomNumber); } + @Getter + @AllArgsConstructor + public static class ListDTO { + private Long invitationId; + private String visitorName; + private Long visitorCount; + private String purpose; + private String officeName; + private LocalDate startDate; + private LocalTime startTime; + private LocalTime endTime; + } + } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java new file mode 100644 index 00000000..05b3977b --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java @@ -0,0 +1,9 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.invitation.dto.InvitationResponse; + +import java.util.List; + +public interface InvitationQueryRepository { + List findInvitationsByMemberId(Long memberId); +} diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java new file mode 100644 index 00000000..592774f9 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java @@ -0,0 +1,56 @@ +package com.livable.server.invitation.repository; + +import com.livable.server.entity.QVisitor; +import com.livable.server.invitation.dto.InvitationResponse; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +import static com.livable.server.entity.QInvitation.invitation; +import static com.livable.server.entity.QVisitor.visitor; + +@RequiredArgsConstructor +public class InvitationQueryRepositoryImpl implements InvitationQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findInvitationsByMemberId(Long memberId) { + QVisitor subVisitorForMaxId = new QVisitor("subVisitor"); + QVisitor subVisitor = new QVisitor("subVisitor"); + + return queryFactory + .select(Projections.constructor(InvitationResponse.ListDTO.class, + visitor.invitation.id, + JPAExpressions + .select(subVisitor.name) + .from(subVisitor) + .where(subVisitor.id.eq( + JPAExpressions + .select(subVisitorForMaxId.id.min()) + .from(subVisitorForMaxId) + .where(subVisitorForMaxId.invitation.id.eq(visitor.invitation.id)) + )), + visitor.invitation.id.count(), + invitation.purpose, + invitation.officeName, + invitation.startDate, + invitation.startTime, + invitation.endTime + )) + .from(visitor) + .innerJoin(invitation) + .on( + visitor.invitation.id.eq(invitation.id), + invitation.member.id.eq(memberId), + invitation.startDate.goe(LocalDate.now()) + ) + .groupBy(visitor.invitation.id) + .orderBy(invitation.startDate.asc(), invitation.startTime.asc()) + .fetch(); + } +} diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java index d19bda88..cc691193 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java @@ -8,7 +8,7 @@ import java.util.Optional; -public interface InvitationRepository extends JpaRepository { +public interface InvitationRepository extends JpaRepository, InvitationQueryRepository { @Query("select i.startTime as startTime, i.endTime as endTime, i.startDate as startDate, i.endDate as endDate" + " from Visitor v" + @@ -16,4 +16,5 @@ public interface InvitationRepository extends JpaRepository { " on v.invitation = i" + " where v.id = :visitorId") Optional findInvitationDetailTimeByVisitorId(@Param("visitorId") Long visitorId); + } diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 45aa712f..e65f57f5 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -221,4 +221,12 @@ private void createInvitationReservationMap(List reservations, Invi invitationReservationMapRepository.save(invitationReservationMap); } } + + @Transactional(readOnly = true) + public ResponseEntity>> getInvitations(Long memberId) { + Member member = findMemberById(memberId); + List invitationDTOs = invitationRepository.findInvitationsByMemberId(member.getId()); + + return ApiResponse.success(invitationDTOs, HttpStatus.OK); + } } From 2909b257bb22d99c567476d1b90acb0257fdc7aa Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:18:10 +0900 Subject: [PATCH 025/111] =?UTF-8?q?=EB=B0=A9=EB=AC=B8=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: ๋ถˆํ•„์š”ํ•œ .gitkeep ํŒŒ์ผ ์ œ๊ฑฐ (#44) * Modify: Validation Message ํŒจํ‚ค์ง€ ๋ฐ ํด๋ž˜์Šค ์ด๋ฆ„ ์ˆ˜์ • (#44) * Modify: @DirtiesContext ์ œ๊ฑฐ (#44) ๋ถˆํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ๋ณ€๊ฒฝ ์ œ๊ฑฐ * Feat: VisitorCustomRepository ๊ตฌํ˜„ (#44) findVisitationDetailInformationById ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ * Test: VisitorRepository Test ๊ตฌํ˜„ (#44) * Feat: ๋ฐฉ๋ฌธ์ฆ ์ •๋ณด ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง ๊ตฌํ˜„ (#44) * Modify: Response ํ•„๋“œ๋ช… ์ˆ˜์ • (#44) * Refactor: ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ถˆ๋ณ€์„ฑ์„ ์œ„ํ•œ final ํ‚ค์›Œ๋“œ ์ž‘์„ฑ (#44) * Refactor: CustomRepository ๋ฐ˜ํ™˜ ํƒ€์ž… Optional ๋ž˜ํ•‘ (#44) * Modify: ํด๋ž˜์Šค๋ช… ์ˆ˜์ • (#44) * Modify: ํด๋ž˜์Šค๋ช… ์ˆ˜์ • (#44) * Modify: ํด๋ž˜์Šค๋ช… ์ˆ˜์ • (#44) * Feat: ๋ฐฉ๋ฌธ์ฆ ๊ธฐ๋ณธ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ (#44) * Test: VisitationController.findVisitationDetailInformation ์ •์ƒ ์‘๋‹ต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#44) * Feat: ๋ฐฉ๋ฌธ์ฆ ์ •๋ณด ์‘๋‹ต ๋กœ์ง ๊ตฌํ˜„ (#44) * Test: VisitationFacadeService.findVisitationDetailInformation ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#44) * Test: VisitationService.findVisitationDetailInformation ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#44) * Test: VisitationService.findVisitationDetailInformation ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#44) --- .../InvitationValidationGuideMessage.java | 6 - .../server/visitation/controller/.gitkeep | 0 .../controller/VisitationController.java | 15 ++- .../livable/server/visitation/domain/.gitkeep | 0 .../visitation/domain/QrCodeManager.java | 16 +-- .../domain/VisitationValidationMessage.java | 6 + .../livable/server/visitation/dto/.gitkeep | 0 .../visitation/dto/VisitationRequest.java | 6 +- .../visitation/dto/VisitationResponse.java | 32 ++++- .../server/visitation/repository/.gitkeep | 0 .../repository/ParkingLogRepository.java | 2 +- .../repository/VisitorCustomRepository.java | 10 ++ .../VisitorCustomRepositoryImpl.java | 53 ++++++++ .../repository/VisitorRepository.java | 2 +- .../server/visitation/service/.gitkeep | 0 .../visitation/service/ParkingLogService.java | 4 +- .../service/VisitationFacadeService.java | 15 ++- .../visitation/service/VisitationService.java | 4 +- .../visitation/service/VisitorService.java | 10 +- .../repository/InvitationRepositoryTest.java | 12 +- .../controller/VisitationControllerTest.java | 29 ++++- .../mock/MockDetailInformationDto.java | 51 ++++++++ ...equest.java => MockValidateQrCodeDto.java} | 6 +- .../repository/ParkingLogRepositoryTest.java | 10 +- .../repository/VisitorRepositoryTest.java | 120 ++++++++++++++++++ .../service/VisitationFacadeServiceTest.java | 20 ++- .../service/VisitorServiceTest.java | 34 +++++ 27 files changed, 406 insertions(+), 57 deletions(-) delete mode 100644 src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java delete mode 100644 src/main/java/com/livable/server/visitation/controller/.gitkeep delete mode 100644 src/main/java/com/livable/server/visitation/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java delete mode 100644 src/main/java/com/livable/server/visitation/dto/.gitkeep delete mode 100644 src/main/java/com/livable/server/visitation/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java create mode 100644 src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java delete mode 100644 src/main/java/com/livable/server/visitation/service/.gitkeep create mode 100644 src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java rename src/test/java/com/livable/server/visitation/mock/{ValidateQrCodeSuccessMockRequest.java => MockValidateQrCodeDto.java} (54%) create mode 100644 src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java deleted file mode 100644 index 6f8df1ec..00000000 --- a/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.livable.server.invitation.domain; - -public interface InvitationValidationGuideMessage { - - String INVALID_CAR_NUMBER = "์ฐจ๋Ÿ‰ ๋ฒˆํ˜ธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."; -} diff --git a/src/main/java/com/livable/server/visitation/controller/.gitkeep b/src/main/java/com/livable/server/visitation/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index c413446e..beb72ff6 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -1,9 +1,9 @@ package com.livable.server.visitation.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.visitation.dto.VisitationResponse; import com.livable.server.visitation.service.VisitationFacadeService; import com.livable.server.visitation.dto.VisitationRequest; -import com.livable.server.visitation.dto.VisitationResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,6 +18,15 @@ public class VisitationController { private final VisitationFacadeService visitationFacadeService; + @GetMapping + public ResponseEntity> findVisitationDetailInformation() { + Long visitorId = 1L; + VisitationResponse.DetailInformationDto detailInformationDto = + visitationFacadeService.findVisitationDetailInformation(visitorId); + + return ApiResponse.success(detailInformationDto, HttpStatus.OK); + } + @GetMapping("/qr") public ResponseEntity> createQrCode() { @@ -28,9 +37,9 @@ public ResponseEntity> createQrCode() { } @PostMapping("/qr") - public ResponseEntity> validateQrCode(@RequestBody VisitationRequest.ValidateQrDto validateQrDto) { + public ResponseEntity> validateQrCode(@RequestBody VisitationRequest.ValidateQrCodeDto validateQrCodeDto) { - visitationFacadeService.validateQrCode(validateQrDto.getQr()); + visitationFacadeService.validateQrCode(validateQrCodeDto.getQr()); return ApiResponse.success(HttpStatus.OK); } diff --git a/src/main/java/com/livable/server/visitation/domain/.gitkeep b/src/main/java/com/livable/server/visitation/domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java index c5c167fb..849e89d5 100644 --- a/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java +++ b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java @@ -42,7 +42,7 @@ public class QrCodeManager { private final ObjectMapper objectMapper; - public String createQrCode(LocalDateTime startDate, LocalDateTime endDate) { + public String createQrCode(final LocalDateTime startDate, final LocalDateTime endDate) { validatePeriod(startDate, endDate); @@ -51,7 +51,7 @@ public String createQrCode(LocalDateTime startDate, LocalDateTime endDate) { return encodeQrcodeToBase64(qrCode); } - private void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) { + private void validatePeriod(final LocalDateTime startDate, final LocalDateTime endDate) { if (startDate.isAfter(endDate) || endDate.isBefore(startDate)) { throw new GlobalRuntimeException(VisitationErrorCode.INVALID_PERIOD); } @@ -61,7 +61,7 @@ private void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) { } } - private BufferedImage createQrCodeImage(LocalDateTime startDateTime, LocalDateTime endDateTime) { + private BufferedImage createQrCodeImage(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { try { HashMap expirationPeriodMap = getExpirationPeriodMap(startDateTime, endDateTime); @@ -79,7 +79,7 @@ private BufferedImage createQrCodeImage(LocalDateTime startDateTime, LocalDateTi } } - private HashMap getExpirationPeriodMap(LocalDateTime startDate, LocalDateTime endDate) { + private HashMap getExpirationPeriodMap(final LocalDateTime startDate, final LocalDateTime endDate) { return new HashMap<>() {{ put(EXPIRATION_START_DATE_KEY, startDate); put(EXPIRATION_END_DATE_KEY, endDate); @@ -92,7 +92,7 @@ private Map getEncodeHints() { }}; } - private String encodeQrcodeToBase64(BufferedImage bufferedImage) { + private String encodeQrcodeToBase64(final BufferedImage bufferedImage) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -108,13 +108,13 @@ private String encodeQrcodeToBase64(BufferedImage bufferedImage) { } } - public void validateQrCode(String base64QrCode) { + public void validateQrCode(final String base64QrCode) { QrPayload qrPayload = getQrPayload(base64QrCode); validatePeriod(qrPayload.getStartDate(), qrPayload.getEndDate()); } - private QrPayload getQrPayload(String base64QrCode) { + private QrPayload getQrPayload(final String base64QrCode) { Map hints = getDecodeHints(); String decodeQrContent = getDecodeQrContent(base64QrCode, hints); @@ -133,7 +133,7 @@ private Map getDecodeHints() { }}; } - private String getDecodeQrContent(String base64QrCode, Map hints) { + private String getDecodeQrContent(final String base64QrCode, final Map hints) { Base64.Decoder decoder = Base64.getDecoder(); byte[] imageBytes = decoder.decode(base64QrCode); diff --git a/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java b/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java new file mode 100644 index 00000000..17570633 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java @@ -0,0 +1,6 @@ +package com.livable.server.visitation.domain; + +public interface VisitationValidationMessage { + + String INVALID_CAR_NUMBER = "์ฐจ๋Ÿ‰ ๋ฒˆํ˜ธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."; +} diff --git a/src/main/java/com/livable/server/visitation/dto/.gitkeep b/src/main/java/com/livable/server/visitation/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java index 6ea73d65..eb303b8e 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java @@ -1,6 +1,6 @@ package com.livable.server.visitation.dto; -import com.livable.server.invitation.domain.InvitationValidationGuideMessage; +import com.livable.server.visitation.domain.VisitationValidationMessage; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,13 +11,13 @@ public class VisitationRequest { @Getter - public static class ValidateQrDto { + public static class ValidateQrCodeDto { private String qr; } @Getter public static class RegisterParkingDto { - @Pattern(regexp = "^\\d{2,3}[๊ฐ€-ํžฃ]{1}\\d{4}$", message = InvitationValidationGuideMessage.INVALID_CAR_NUMBER) + @Pattern(regexp = "^\\d{2,3}[๊ฐ€-ํžฃ]{1}\\d{4}$", message = VisitationValidationMessage.INVALID_CAR_NUMBER) private String carNumber; } } diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java index ea3c830b..a7bef65a 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -1,7 +1,5 @@ package com.livable.server.visitation.dto; -import com.beust.ah.A; -import com.livable.server.entity.Invitation; import com.livable.server.invitation.dto.InvitationDetailTimeDto; import lombok.*; @@ -12,6 +10,34 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VisitationResponse { + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class DetailInformationDto { + + private LocalDate invitationStartDate; + private LocalTime invitationStartTime; + private LocalDate invitationEndDate; + private LocalTime invitationEndTime; + private String invitationBuildingName; + private String invitationOfficeName; + + private String buildingRepresentativeImageUrl; + private String buildingName; + private String buildingAddress; + private String buildingParkingCostInformation; + private String buildingScale; + +// private String placeImageUrl; + private String invitationTip; + + private String hostName; + private String hostCompanyName; + private String hostContact; + private String hostBusinessCardImageUrl; + } + + @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -30,7 +56,7 @@ public LocalDateTime getEndDateTime() { return LocalDateTime.of(endDate, endTime); } - public static InvitationTimeDto from(InvitationDetailTimeDto invitationDetailTimeDto) { + public static InvitationTimeDto from(final InvitationDetailTimeDto invitationDetailTimeDto) { return InvitationTimeDto.builder() .startTime(invitationDetailTimeDto.getStartTime()) .endTime(invitationDetailTimeDto.getEndTime()) diff --git a/src/main/java/com/livable/server/visitation/repository/.gitkeep b/src/main/java/com/livable/server/visitation/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java index 019495dd..baecaf70 100644 --- a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java @@ -11,5 +11,5 @@ public interface ParkingLogRepository extends JpaRepository { @Query("select p from ParkingLog p" + " where p.visitor.id = :visitorId") - Optional findParkingLogByVisitorId(@Param("visitorId") Long visitorId); + Optional findParkingLogByVisitorId(@Param("visitorId") final Long visitorId); } diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java new file mode 100644 index 00000000..47f81bb3 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java @@ -0,0 +1,10 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.visitation.dto.VisitationResponse; + +import java.util.Optional; + +public interface VisitorCustomRepository { + + Optional findVisitationDetailInformationById(final Long visitorId); +} diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java new file mode 100644 index 00000000..3f69adcc --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java @@ -0,0 +1,53 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.entity.*; +import com.livable.server.visitation.dto.VisitationResponse; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; + +@RequiredArgsConstructor +public class VisitorCustomRepositoryImpl implements VisitorCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findVisitationDetailInformationById(final Long visitorId) { + final QInvitation invitation = QInvitation.invitation; + final QBuilding building = QBuilding.building; + final QCompany company = QCompany.company; + final QMember member = QMember.member; + final QVisitor visitor = QVisitor.visitor; + + JPAQuery query = queryFactory + .select(Projections.constructor(VisitationResponse.DetailInformationDto.class, + invitation.startDate, + invitation.startTime, + invitation.endDate, + invitation.endTime, + building.name, + invitation.officeName, + building.representativeImageUrl, + building.name, + building.address, + building.parkingCostInformation, + building.scale, + invitation.description, + member.name, + company.name, + member.contact, + member.businessCardImageUrl + )) + .from(visitor) + .innerJoin(invitation).on(visitor.invitation.id.eq(invitation.id)) + .innerJoin(member).on(invitation.member.id.eq(member.id)) + .innerJoin(company).on(member.company.id.eq(company.id)) + .innerJoin(building).on(company.building.id.eq(building.id)) + .where(visitor.id.eq(visitorId)); + + return Optional.ofNullable(query.fetchJoin().fetchOne()); + } +} diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java index c48ec52d..041bc8d7 100644 --- a/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java @@ -3,5 +3,5 @@ import com.livable.server.entity.Visitor; import org.springframework.data.jpa.repository.JpaRepository; -public interface VisitorRepository extends JpaRepository { +public interface VisitorRepository extends JpaRepository, VisitorCustomRepository { } diff --git a/src/main/java/com/livable/server/visitation/service/.gitkeep b/src/main/java/com/livable/server/visitation/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java index 9382a9df..094f3740 100644 --- a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java +++ b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java @@ -14,11 +14,11 @@ public class ParkingLogService { private final ParkingLogRepository parkingLogRepository; - public Optional findParkingLogByVisitorId(Long visitorId) { + public Optional findParkingLogByVisitorId(final Long visitorId) { return parkingLogRepository.findParkingLogByVisitorId(visitorId); } - public void registerParkingLog(Visitor visitor, String carNumber) { + public void registerParkingLog(final Visitor visitor, final String carNumber) { ParkingLog parkingLog = ParkingLog.builder() .carNumber(carNumber) .visitor(visitor) diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 1e0d79f4..82acad20 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -1,7 +1,6 @@ package com.livable.server.visitation.service; import com.livable.server.core.exception.GlobalRuntimeException; -import com.livable.server.entity.ParkingLog; import com.livable.server.entity.Visitor; import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.domain.VisitationErrorCode; @@ -10,8 +9,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @RequiredArgsConstructor @Service @Transactional(readOnly = true) @@ -22,24 +19,28 @@ public class VisitationFacadeService { private final VisitorService visitorService; private final ParkingLogService parkingLogService; - public String createQrCode(Long visitorId) { + public VisitationResponse.DetailInformationDto findVisitationDetailInformation(Long visitorId) { + return visitorService.findVisitationDetailInformation(visitorId); + } + + public String createQrCode(final Long visitorId) { VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(visitorId); return visitationService.createQrCode(invitationTime.getStartDateTime(), invitationTime.getEndDateTime()); } - public void validateQrCode(String qr) { + public void validateQrCode(final String qr) { visitationService.validateQrCode(qr); } @Transactional - public void registerParking(Long visitorId, String carNumber) { + public void registerParking(final Long visitorId, final String carNumber) { validateDuplicationRegister(visitorId); Visitor visitor = visitorService.findById(visitorId); parkingLogService.registerParkingLog(visitor, carNumber); } - private void validateDuplicationRegister(Long visitorId) { + private void validateDuplicationRegister(final Long visitorId) { if (parkingLogService.findParkingLogByVisitorId(visitorId).isPresent()) { throw new GlobalRuntimeException(VisitationErrorCode.ALREADY_REGISTER_PARKING); } diff --git a/src/main/java/com/livable/server/visitation/service/VisitationService.java b/src/main/java/com/livable/server/visitation/service/VisitationService.java index 70a6905a..5fb64b0b 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationService.java @@ -12,11 +12,11 @@ public class VisitationService { private final QrCodeManager qrCodeManager; - public String createQrCode(LocalDateTime startDate, LocalDateTime endDate) { + public String createQrCode(final LocalDateTime startDate, final LocalDateTime endDate) { return qrCodeManager.createQrCode(startDate, endDate); } - public void validateQrCode(String qr) { + public void validateQrCode(final String qr) { qrCodeManager.validateQrCode(qr); } } diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java index da55c4c3..261d6863 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitorService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -3,6 +3,7 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Visitor; import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; import com.livable.server.visitation.repository.VisitorRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -13,15 +14,20 @@ public class VisitorService { private final VisitorRepository visitorRepository; - public Long findInvitationId(Long visitorId) { + public Long findInvitationId(final Long visitorId) { return visitorRepository.findById(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)) .getInvitation() .getId(); } - public Visitor findById(Long visitorId) { + public Visitor findById(final Long visitorId) { return visitorRepository.findById(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); } + + public VisitationResponse.DetailInformationDto findVisitationDetailInformation(final Long visitorId) { + return visitorRepository.findVisitationDetailInformationById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + } } diff --git a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java index f6c9b694..88bd1e39 100644 --- a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java +++ b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java @@ -3,14 +3,13 @@ import com.livable.server.core.config.QueryDslConfig; import com.livable.server.entity.*; import com.livable.server.invitation.dto.InvitationDetailTimeDto; -import com.querydsl.jpa.impl.JPAQueryFactory; +import com.livable.server.visitation.repository.VisitorRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import org.springframework.test.annotation.DirtiesContext; import javax.persistence.EntityManager; import java.time.LocalDate; @@ -21,19 +20,22 @@ @DataJpaTest @Import(QueryDslConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) class InvitationRepositoryTest { public static final LocalDate START_DATE = LocalDate.now(); public static final LocalTime START_TIME = LocalTime.of(1, 10); public static final LocalTime END_TIME = LocalTime.of(1, 20); public static final LocalDate END_DATE = LocalDate.now(); + @Autowired EntityManager entityManager; @Autowired InvitationRepository invitationRepository; + @Autowired + VisitorRepository visitorRepository; + @BeforeEach void dataInit() { Building building = Building.builder() @@ -97,7 +99,9 @@ void dataInit() { @DisplayName("InvitationRepository.findInvitationDetailTimeByVisitorId ์ฟผ๋ฆฌ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void findInvitationDetailTimeByVisitorIdSuccessTest() { - InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(1L) + + Visitor visitor = visitorRepository.findAll().get(0); + InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(visitor.getId()) .get(); assertAll( diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 237a48ce..5d8d36df 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -5,8 +5,9 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.mock.MockDetailInformationDto; import com.livable.server.visitation.mock.MockRegisterParkingDto; -import com.livable.server.visitation.mock.ValidateQrCodeSuccessMockRequest; +import com.livable.server.visitation.mock.MockValidateQrCodeDto; import com.livable.server.visitation.service.VisitationFacadeService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -34,6 +35,28 @@ class VisitationControllerTest { @MockBean private VisitationFacadeService visitationFacadeService; + @DisplayName("[GET][/api/visitation] - ๋ฐฉ๋ฌธ์ฆ ์ƒ์„ธ ์ •๋ณด ์ •์ƒ ์‘๋‹ต") + @Test + void findVisitationDetailInformationSuccessTest() throws Exception { + + // Given + MockDetailInformationDto mockDetailInformationDto = new MockDetailInformationDto(); + given(visitationFacadeService.findVisitationDetailInformation(anyLong())).willReturn(mockDetailInformationDto); + + // When + ResultActions resultActions = mockMvc.perform( + get("/api/visitation") + .contentType(MediaType.APPLICATION_JSON) + ); + + // Then + resultActions.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data").exists()); + + then(visitationFacadeService).should(times(1)).findVisitationDetailInformation(anyLong()); + } + @DisplayName("[GET][/api/visitation/qr] - QR์„ ์ƒ์„ฑ ์ •์ƒ ์‘๋‹ต") @Test void createQrCodeSuccessTest() throws Exception { @@ -61,7 +84,7 @@ void createQrCodeSuccessTest() throws Exception { void validateQrCodeSuccess() throws Exception { // given String qr = "qr"; - VisitationRequest.ValidateQrDto validateQrCodeSuccessMockRequest = new ValidateQrCodeSuccessMockRequest(qr); + VisitationRequest.ValidateQrCodeDto validateQrCodeSuccessMockRequest = new MockValidateQrCodeDto(qr); willDoNothing().given(visitationFacadeService).validateQrCode(anyString()); @@ -85,7 +108,7 @@ void validateQrCodeFail() throws Exception { String qr = "qr"; ErrorCode errorCode = VisitationErrorCode.INVALID_QR_PERIOD; String errorMessage = errorCode.getMessage(); - VisitationRequest.ValidateQrDto validateQrCodeSuccessMockRequest = new ValidateQrCodeSuccessMockRequest(qr); + VisitationRequest.ValidateQrCodeDto validateQrCodeSuccessMockRequest = new MockValidateQrCodeDto(qr); willThrow(new GlobalRuntimeException(errorCode)).given(visitationFacadeService).validateQrCode(anyString()); diff --git a/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java b/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java new file mode 100644 index 00000000..56260a83 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java @@ -0,0 +1,51 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.visitation.dto.VisitationResponse; + +import java.time.LocalDate; +import java.time.LocalTime; + +public class MockDetailInformationDto extends VisitationResponse.DetailInformationDto { + + public MockDetailInformationDto() { + super(); + } + + public MockDetailInformationDto( + LocalDate invitationStartDate, + LocalTime invitationStartTime, + LocalDate invitationEndDate, + LocalTime invitationEndTime, + String invitationBuildingName, + String invitationOfficeName, + String buildingRepresentativeImageUrl, + String buildingName, + String buildingAddress, + String buildingParkingCostInformation, + String buildingScale, + String invitationTip, + String hostName, + String hostCompanyName, + String hostContact, + String hostBusinessCardImageUrl + ) { + super( + invitationStartDate, + invitationStartTime, + invitationEndDate, + invitationEndTime, + invitationBuildingName, + invitationOfficeName, + buildingRepresentativeImageUrl, + buildingName, + buildingAddress, + buildingParkingCostInformation, + buildingScale, + invitationTip, + hostName, + hostCompanyName, + hostContact, + hostBusinessCardImageUrl + ); + } +} diff --git a/src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java b/src/test/java/com/livable/server/visitation/mock/MockValidateQrCodeDto.java similarity index 54% rename from src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java rename to src/test/java/com/livable/server/visitation/mock/MockValidateQrCodeDto.java index a2212233..ea0084c9 100644 --- a/src/test/java/com/livable/server/visitation/mock/ValidateQrCodeSuccessMockRequest.java +++ b/src/test/java/com/livable/server/visitation/mock/MockValidateQrCodeDto.java @@ -2,13 +2,13 @@ import com.livable.server.visitation.dto.VisitationRequest; -public class ValidateQrCodeSuccessMockRequest extends VisitationRequest.ValidateQrDto { +public class MockValidateQrCodeDto extends VisitationRequest.ValidateQrCodeDto { private String qr; - public ValidateQrCodeSuccessMockRequest() { + public MockValidateQrCodeDto() { } - public ValidateQrCodeSuccessMockRequest(String qr) { + public MockValidateQrCodeDto(String qr) { this.qr = qr; } diff --git a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java index 5ed63af3..c4a1c170 100644 --- a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java +++ b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java @@ -8,20 +8,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import org.springframework.test.annotation.DirtiesContext; import javax.persistence.EntityManager; - import java.time.LocalDate; import java.time.LocalTime; -import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; @DataJpaTest @Import(QueryDslConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) class ParkingLogRepositoryTest { public static final LocalDate START_DATE = LocalDate.now(); @@ -108,8 +104,8 @@ void dataInit() { @DisplayName("ParkingLogRepository.findParkingLogByVisitorId ์ฟผ๋ฆฌ ํ™•์ธ์šฉ ํ…Œ์ŠคํŠธ") @Test void test() { - ParkingLog parkingLog = parkingLogRepository.findParkingLogByVisitorId(1L).get(); - Visitor visitor = visitorRepository.findById(1L).get(); + Visitor visitor = visitorRepository.findAll().get(0); + ParkingLog parkingLog = parkingLogRepository.findParkingLogByVisitorId(visitor.getId()).get(); assertAll( () -> assertThat(parkingLog.getId()).isEqualTo(1L), diff --git a/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java new file mode 100644 index 00000000..379c52b7 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java @@ -0,0 +1,120 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.*; +import com.livable.server.visitation.dto.VisitationResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.time.LocalTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DataJpaTest +@Import(QueryDslConfig.class) +class VisitorRepositoryTest { + + public static final LocalDate START_DATE = LocalDate.now(); + public static final LocalTime START_TIME = LocalTime.of(1, 10); + public static final LocalTime END_TIME = LocalTime.of(1, 20); + public static final LocalDate END_DATE = LocalDate.now(); + + @Autowired + VisitorRepository visitorRepository; + + @Autowired + EntityManager entityManager; + + @BeforeEach + void dataInit() { + Building building = Building.builder() + .name("63๋นŒ๋”ฉ") + .scale("์ง€ํ•˜ 3์ธต, ์ง€์ƒ 63์ธต") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10๋ถ„๋‹น 1000์›") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 61๊ธธ 7, 392") + .subwayStation("์„์ดŒ์—ญ") + .build(); + + entityManager.persist(building); + + Company company = Company.builder() + .name("ํŒจ์ŠคํŠธ์บ ํผ์Šค") + .building(building) + .build(); + + entityManager.persist(company); + + Member member = Member.builder() + .company(company) + .contact("01012345678") + .name("๊น€ํ›ˆ์„ญ") + .email("test@naver.com") + .employeeNumber("9q0mavfdmmpoaskp") + .profileImageUrl("./profileImageUrl") + .password("1234") + .role(Role.USER) + .build(); + + entityManager.persist(member); + + Invitation invitation = Invitation.builder() + .member(member) + .endDate(END_DATE) + .startDate(START_DATE) + .startTime(START_TIME) + .endTime(END_TIME) + .description("์•Œ์•„์„œ ์™€") + .purpose("INTERVIEW") + .officeName("ํŒจ์ŠคํŠธ์บ ํผ์Šค ์‚ฌ๋ฌด์‹ค") + .build(); + + entityManager.persist(invitation); + + Visitor visitor = Visitor.builder() + .invitation(invitation) + .name("์ตœํƒœ์œค") + .contact("01034567811") + .build(); + + entityManager.persist(visitor); + } + + @DisplayName("test") + @Test + void test() { + Visitor visitor = visitorRepository.findAll().get(0); + Invitation invitation = visitor.getInvitation(); + Member member = invitation.getMember(); + Company company = member.getCompany(); + Building building = company.getBuilding(); + + VisitationResponse.DetailInformationDto detailInformationDto = + visitorRepository.findVisitationDetailInformationById(visitor.getId()).get(); + + assertAll( + () -> assertThat(detailInformationDto.getInvitationStartDate()).isEqualTo(visitor.getInvitation().getStartDate()), + () -> assertThat(detailInformationDto.getInvitationEndDate()).isEqualTo(visitor.getInvitation().getEndDate()), + () -> assertThat(detailInformationDto.getInvitationStartTime()).isEqualTo(visitor.getInvitation().getStartTime()), + () -> assertThat(detailInformationDto.getInvitationEndTime()).isEqualTo(visitor.getInvitation().getEndTime()), + () -> assertThat(detailInformationDto.getHostName()).isEqualTo(member.getName()), + () -> assertThat(detailInformationDto.getHostContact()).isEqualTo(member.getContact()), + () -> assertThat(detailInformationDto.getBuildingAddress()).isEqualTo(building.getAddress()), + () -> assertThat(detailInformationDto.getBuildingName()).isEqualTo(building.getName()), + () -> assertThat(detailInformationDto.getBuildingParkingCostInformation()).isEqualTo(building.getParkingCostInformation()), + () -> assertThat(detailInformationDto.getBuildingRepresentativeImageUrl()).isEqualTo(building.getRepresentativeImageUrl()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index f41fed85..0cc6aae6 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -3,10 +3,10 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.ParkingLog; import com.livable.server.entity.Visitor; -import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.service.InvitationService; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.mock.MockDetailInformationDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,7 +20,7 @@ import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.*; @@ -46,6 +46,22 @@ class VisitationFacadeServiceTest { @Mock ParkingLogService parkingLogService; + @DisplayName("VisitationFacadeService.findVisitationDetailInformation ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findVisitationDetailInformationSuccessTest() { + // Given + MockDetailInformationDto mockDetailInformationDto = new MockDetailInformationDto(); + given(visitorService.findVisitationDetailInformation(anyLong())).willReturn(mockDetailInformationDto); + + // When + VisitationResponse.DetailInformationDto detailInformationDto = + visitationFacadeService.findVisitationDetailInformation(1L); + + // Then + assertThat(detailInformationDto).isEqualTo(mockDetailInformationDto); + then(visitorService).should(times(1)).findVisitationDetailInformation(anyLong()); + } + @DisplayName("VisitationFacadeService.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void createQrCodeSuccessTest() { diff --git a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java index 2180d7b3..23267dc8 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java @@ -4,6 +4,8 @@ import com.livable.server.entity.Invitation; import com.livable.server.entity.Visitor; import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.mock.MockDetailInformationDto; import com.livable.server.visitation.repository.VisitorRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -75,4 +77,36 @@ void findInvitationIdFailTest() { assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); then(visitorRepository).should(times(1)).findById(anyLong()); } + + @DisplayName("VisitorService.findVisitationDetailInformation ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findVisitationDetailInformationByIdSuccessTest() { + // Given + MockDetailInformationDto mockDetailInformationDto = new MockDetailInformationDto(); + given(visitorRepository.findVisitationDetailInformationById(anyLong())).willReturn(Optional.of(mockDetailInformationDto)); + + // When + VisitationResponse.DetailInformationDto detailInformationDto = visitorService.findVisitationDetailInformation(1L); + + // Then + assertThat(detailInformationDto).isEqualTo(mockDetailInformationDto); + then(visitorRepository).should(times(1)).findVisitationDetailInformationById(anyLong()); + } + + @DisplayName("VisitorService.findVisitationDetailInformation ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findVisitationDetailInformationByIdFailTest() { + // Given + given(visitorRepository.findVisitationDetailInformationById(anyLong())).willReturn(Optional.empty()); + + // When + GlobalRuntimeException globalRuntimeException = + assertThrows( + GlobalRuntimeException.class, () -> visitorService.findVisitationDetailInformation(1L) + ); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(visitorRepository).should(times(1)).findVisitationDetailInformationById(anyLong()); + } } \ No newline at end of file From 70803067b811ecdacc4431bacf2cfe9a1097682e Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Wed, 20 Sep 2023 14:37:48 +0900 Subject: [PATCH 026/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ํ”„๋กœ์ ์…˜์„ ์œ„ํ•œ ReservationDTO ์œ„์น˜ ๋ณ€๊ฒฝ (#53) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต DTO ๊ตฌํ˜„ (#53) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#53) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต Repository์— ํ•„์š”ํ•œ ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#53) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต ์ดˆ๋Œ€์žฅ ์ฃผ์ธ ํ™•์ธ ์˜ค๋ฅ˜ ErrorCode ๊ตฌํ˜„ (#53) * Feat: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต Service ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#53) * Test: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต Service ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#53) * Refactor: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ DTO ๋นŒ๋” ์ถ”๊ฐ€ (#53) * Test: ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์‘๋‹ต Controller ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#53) --- .../controller/InvitationController.java | 8 ++ .../domain/InvitationErrorCode.java | 3 +- .../invitation/dto/InvitationProjection.java | 39 --------- .../invitation/dto/InvitationResponse.java | 79 ++++++++++++++++++- .../repository/InvitationQueryRepository.java | 1 + .../InvitationQueryRepositoryImpl.java | 38 +++++++++ .../repository/InvitationRepository.java | 2 + .../invitation/service/InvitationService.java | 32 ++++++-- .../ReservationQueryRepository.java | 4 +- .../ReservationQueryRepositoryImpl.java | 6 +- .../controller/InvitationControllerTest.java | 28 +++++++ .../service/InvitationServiceTest.java | 48 +++++++++-- 12 files changed, 227 insertions(+), 61 deletions(-) delete mode 100644 src/main/java/com/livable/server/invitation/dto/InvitationProjection.java diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index eb3c74c1..60823bad 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -43,4 +43,12 @@ public ResponseEntity>> getInvitations( return invitationService.getInvitations(memberId); } + @GetMapping("/{invitationId}") + public ResponseEntity> getInvitation(@PathVariable Long invitationId) { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.getInvitation(invitationId, memberId); + } + } diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java index d0c47f9e..7b1c5356 100644 --- a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java +++ b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java @@ -14,7 +14,8 @@ public enum InvitationErrorCode implements ErrorCode { INVALID_DATE(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์‹œ์ž‘ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), INVALID_TIME(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ์‹œ๊ฐ„์ด ์‹œ์ž‘ ์‹œ๊ฐ„๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), INVALID_TIME_UNIT(HttpStatus.BAD_REQUEST, "์‹œ๊ฐ„์˜ ๋ถ„ ๋‹จ์œ„๋Š” 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - INVALID_RESERVATION_COUNT(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋‚ ์งœ ๋˜๋Š” ์‹œ๊ฐ„์— ์˜ˆ์•ฝ๋œ ์žฅ์†Œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); + INVALID_RESERVATION_COUNT(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋‚ ์งœ ๋˜๋Š” ์‹œ๊ฐ„์— ์˜ˆ์•ฝ๋œ ์žฅ์†Œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_INVITATION_OWNER(HttpStatus.FORBIDDEN, "์ดˆ๋Œ€์žฅ์„ ์ž‘์„ฑํ•œ ํšŒ์›๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java b/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java deleted file mode 100644 index b704c0fd..00000000 --- a/src/main/java/com/livable/server/invitation/dto/InvitationProjection.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.livable.server.invitation.dto; - -import lombok.*; - -import java.time.LocalDate; -import java.time.LocalTime; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class InvitationProjection { - - @Getter - @Setter - public static class ReservationDTO { - private Long commonPlaceId; - private String commonPlaceFloor; - private String commonPlaceRoomNumber; - private String commonPlaceName; - private LocalDate reservationDate; - private LocalTime reservationStartTime; - private LocalTime reservationEndTime; - - public ReservationDTO( - Long commonPlaceId, - String commonPlaceFloor, - String commonPlaceRoomNumber, - String commonPlaceName, - LocalDate reservationDate, - LocalTime reservationStartTime - ) { - this.commonPlaceId = commonPlaceId; - this.commonPlaceFloor = commonPlaceFloor; - this.commonPlaceRoomNumber = commonPlaceRoomNumber; - this.commonPlaceName = commonPlaceName; - this.reservationDate = reservationDate; - this.reservationStartTime = reservationStartTime; - this.reservationEndTime = reservationStartTime.plusMinutes(30); - } - } -} diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java index 12accd08..5bfb5cb1 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java @@ -1,6 +1,5 @@ package com.livable.server.invitation.dto; -import com.livable.server.entity.Invitation; import com.livable.server.entity.Office; import lombok.*; @@ -42,7 +41,7 @@ public static class CommonPlaceDTO { private LocalTime startTime; private LocalTime endTime; - public static CommonPlaceDTO from(InvitationProjection.ReservationDTO reservationDTO) { + public static CommonPlaceDTO from(ReservationDTO reservationDTO) { return new CommonPlaceDTO( reservationDTO.getCommonPlaceId(), getFormattedPlaceName( @@ -61,6 +60,35 @@ private static String getFormattedPlaceName(String name, String floor, String ro return String.format("%s (%s์ธต %sํ˜ธ)", name, floor, roomNumber); } + @Getter + @Setter + public static class ReservationDTO { + private Long commonPlaceId; + private String commonPlaceFloor; + private String commonPlaceRoomNumber; + private String commonPlaceName; + private LocalDate reservationDate; + private LocalTime reservationStartTime; + private LocalTime reservationEndTime; + + public ReservationDTO( + Long commonPlaceId, + String commonPlaceFloor, + String commonPlaceRoomNumber, + String commonPlaceName, + LocalDate reservationDate, + LocalTime reservationStartTime + ) { + this.commonPlaceId = commonPlaceId; + this.commonPlaceFloor = commonPlaceFloor; + this.commonPlaceRoomNumber = commonPlaceRoomNumber; + this.commonPlaceName = commonPlaceName; + this.reservationDate = reservationDate; + this.reservationStartTime = reservationStartTime; + this.reservationEndTime = reservationStartTime.plusMinutes(30); + } + } + @Getter @AllArgsConstructor public static class ListDTO { @@ -74,4 +102,51 @@ public static class ListDTO { private LocalTime endTime; } + @Getter + @Builder + @AllArgsConstructor + public static class DetailDTO { + private Long commonPlaceId; + private String officeName; + private String purpose; + private String description; + private LocalDate startDate; + private LocalDate endDate; + private LocalTime startTime; + private LocalTime endTime; + private List visitors; + + public DetailDTO( + Long commonPlaceId, + String officeName, + String purpose, + String description, + LocalDate startDate, + LocalDate endDate, + LocalTime startTime, + LocalTime endTime + ) { + this.commonPlaceId = commonPlaceId; + this.officeName = officeName; + this.purpose = purpose; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.startTime = startTime; + this.endTime = endTime; + } + + public void setVisitors(List visitors) { + this.visitors = visitors; + } + } + + @Getter + @AllArgsConstructor + public static class VisitorForDetailDTO { + private Long visitorId; + private String name; + private String contact; + } + } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java index 05b3977b..79db2f9b 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java @@ -6,4 +6,5 @@ public interface InvitationQueryRepository { List findInvitationsByMemberId(Long memberId); + InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long invitationId); } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java index 592774f9..d77d2da3 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java @@ -11,6 +11,8 @@ import java.util.List; import static com.livable.server.entity.QInvitation.invitation; +import static com.livable.server.entity.QInvitationReservationMap.invitationReservationMap; +import static com.livable.server.entity.QReservation.reservation; import static com.livable.server.entity.QVisitor.visitor; @RequiredArgsConstructor @@ -53,4 +55,40 @@ public List findInvitationsByMemberId(Long memberId) .orderBy(invitation.startDate.asc(), invitation.startTime.asc()) .fetch(); } + + @Override + public InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long invitationId) { + InvitationResponse.DetailDTO invitationDetail = queryFactory + .select(Projections.constructor(InvitationResponse.DetailDTO.class, + reservation.commonPlace.id, + invitation.officeName, + invitation.purpose, + invitation.description, + invitation.startDate, + invitation.endDate, + invitation.startTime, + invitation.endTime + )) + .from(invitation) + .leftJoin(invitationReservationMap) + .on(invitationReservationMap.invitation.id.eq(invitation.id)) + .leftJoin(reservation) + .on(reservation.id.eq(invitationReservationMap.reservation.id)) + .where(invitation.id.eq(invitationId)) + .fetchFirst(); + + List visitors = queryFactory + .select(Projections.constructor(InvitationResponse.VisitorForDetailDTO.class, + visitor.id, + visitor.name, + visitor.contact + )) + .from(visitor) + .where(visitor.invitation.id.eq(invitationId)) + .fetch(); + + invitationDetail.setVisitors(visitors); + + return invitationDetail; + } } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java index cc691193..ff3339af 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java @@ -17,4 +17,6 @@ public interface InvitationRepository extends JpaRepository, I " where v.id = :visitorId") Optional findInvitationDetailTimeByVisitorId(@Param("visitorId") Long visitorId); + @Query("select count(i) from Invitation i where i.id = :invitationId and i.member.id = :memberId") + Long countByIdAndMemberId(@Param("invitationId") Long invitationId, @Param("memberId") Long memberId); } diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index e65f57f5..88b648c0 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -7,7 +7,6 @@ import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.dto.InvitationDetailTimeDto; import com.livable.server.invitation.domain.InvitationPurpose; -import com.livable.server.invitation.dto.InvitationProjection; import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.repository.InvitationRepository; @@ -61,7 +60,7 @@ public ResponseEntity> getAvailab List officeEntities = officeRepository.findAllByCompanyId(companyId); // 3. company ๊ฐ€ ์˜ˆ์•ฝํ•œ ๊ณต์šฉ ๊ณต๊ฐ„ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด - List reservations = reservationRepository + List reservations = reservationRepository .findReservationsByCompanyId(companyId); // 4. ReservationDTO ๋ฅผ ์—ฐ์†๋œ ์‹œ๊ฐ„์€ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” ์ž‘์—…์„ ์ง„ํ–‰ @@ -82,11 +81,11 @@ public ResponseEntity> getAvailab return ApiResponse.success(responseBody, HttpStatus.OK); } - private void combineConsecutiveReservation(List reservations) { - Iterator reservationsIterator = reservations.iterator(); - InvitationProjection.ReservationDTO beforeReservation = null; + private void combineConsecutiveReservation(List reservations) { + Iterator reservationsIterator = reservations.iterator(); + InvitationResponse.ReservationDTO beforeReservation = null; while (reservationsIterator.hasNext()) { - InvitationProjection.ReservationDTO currentReservation = reservationsIterator.next(); + InvitationResponse.ReservationDTO currentReservation = reservationsIterator.next(); if (isNotCombineTarget(beforeReservation, currentReservation)) { beforeReservation = currentReservation; @@ -98,8 +97,8 @@ private void combineConsecutiveReservation(List>> getInvitations( return ApiResponse.success(invitationDTOs, HttpStatus.OK); } + + @Transactional(readOnly = true) + public ResponseEntity> getInvitation(Long invitationId, Long memberId) { + Member member = findMemberById(memberId); + checkInvitationOwner(invitationId, member.getId()); + + InvitationResponse.DetailDTO invitationDTO + = invitationRepository.findInvitationAndVisitorsByInvitationId(invitationId); + + return ApiResponse.success(invitationDTO, HttpStatus.OK); + } + + private void checkInvitationOwner(Long invitationId, Long memberId) { + if (invitationRepository.countByIdAndMemberId(invitationId, memberId).equals(0L)) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER); + } + } } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index 635b16bd..66309497 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -1,13 +1,13 @@ package com.livable.server.reservation.repository; import com.livable.server.entity.Reservation; -import com.livable.server.invitation.dto.InvitationProjection; +import com.livable.server.invitation.dto.InvitationResponse; import java.time.LocalDateTime; import java.util.List; public interface ReservationQueryRepository { - List findReservationsByCompanyId(Long companyId); + List findReservationsByCompanyId(Long companyId); List findReservationsByCommonPlaceIdAndStartDateAndEndDate( Long commonPlaceId, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index 1eb9f694..baf0312b 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -1,7 +1,7 @@ package com.livable.server.reservation.repository; import com.livable.server.entity.Reservation; -import com.livable.server.invitation.dto.InvitationProjection; +import com.livable.server.invitation.dto.InvitationResponse; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -22,9 +22,9 @@ public class ReservationQueryRepositoryImpl implements ReservationQueryRepositor private final JPAQueryFactory queryFactory; @Override - public List findReservationsByCompanyId(Long companyId) { + public List findReservationsByCompanyId(Long companyId) { return queryFactory - .select(Projections.constructor(InvitationProjection.ReservationDTO.class, + .select(Projections.constructor(InvitationResponse.ReservationDTO.class, commonPlace.id, commonPlace.floor, commonPlace.roomNumber, diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index 393369a3..463ea710 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -176,4 +177,31 @@ void createInvitationFail_03() throws Exception { .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_VISITOR_COUNT)); } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ - ์ดˆ๋Œ€์žฅ ์ฃผ์ธ๊ณผ ์š”์ฒญํ•œ ์‚ฌ๋žŒ์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ") + @Test + void getInvitationFail_01() throws Exception { + // Given + Long invitationId = 1L; + given(invitationService.getInvitation(anyLong(), anyLong())) + .willThrow(new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER)); + + // When & Then + mockMvc.perform(get("/api/invitation/{invitationId}", invitationId)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").value(InvitationErrorCode.INVALID_INVITATION_OWNER.getMessage())); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ") + @Test + void getInvitationSuccess_01() throws Exception { + // Given + Long invitationId = 1L; + given(invitationService.getInvitation(anyLong(), anyLong())) + .willReturn(ApiResponse.success(InvitationResponse.DetailDTO.builder().build(), HttpStatus.OK)); + + // When & Then + mockMvc.perform(get("/api/invitation/{invitationId}", invitationId)) + .andExpect(status().isOk()); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 4be61aa6..34ce773b 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -5,6 +5,7 @@ import com.livable.server.entity.*; import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.dto.InvitationRequest; +import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.dto.InvitationResponse.AvailablePlacesDTO; import com.livable.server.invitation.dto.InvitationResponse.CommonPlaceDTO; import com.livable.server.invitation.repository.InvitationRepository; @@ -30,7 +31,6 @@ import java.util.List; import java.util.Optional; -import static com.livable.server.invitation.dto.InvitationProjection.ReservationDTO; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -390,9 +390,45 @@ void createInvitationSuccess_02() { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CREATED); } - private List createReservations() { + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ - ์ดˆ๋Œ€์žฅ ์ฃผ์ธ์ด ์•„๋‹Œ ์‚ฌ๋žŒ์ด ์š”์ฒญํ•œ ๊ฒฝ์šฐ") + @Test + void getInvitationFail_01() { + // Given + Long invitationId = 1L; + Long memberId = 1L; + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(0L); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.getInvitation(invitationId, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_INVITATION_OWNER); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ") + @Test + void getInvitationFail_02() { + // Given + Long invitationId = 1L; + Long memberId = 1L; + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findInvitationAndVisitorsByInvitationId(anyLong())) + .willReturn(any(InvitationResponse.DetailDTO.class)); + + // When + ResponseEntity> result + = invitationService.getInvitation(invitationId, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + private List createReservations() { return new ArrayList<>(List.of( - new ReservationDTO( + new InvitationResponse.ReservationDTO( 1L, "1", "101", @@ -400,7 +436,7 @@ private List createReservations() { LocalDate.of(2023, 10, 29), LocalTime.of(10, 0, 0) ), - new ReservationDTO( + new InvitationResponse.ReservationDTO( 1L, "1", "101", @@ -408,7 +444,7 @@ private List createReservations() { LocalDate.of(2023, 10, 30), LocalTime.of(10, 0, 0) ), - new ReservationDTO( + new InvitationResponse.ReservationDTO( 2L, "2", "201", @@ -416,7 +452,7 @@ private List createReservations() { LocalDate.of(2023, 10, 30), LocalTime.of(10, 30, 0) ), - new ReservationDTO( + new InvitationResponse.ReservationDTO( 2L, "2", "201", From ef3ac13fce717b4bf4f39d2c7c0728cdbeac77c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 21 Sep 2023 14:28:37 +0900 Subject: [PATCH 027/111] =?UTF-8?q?=EB=8B=AC=EB=A0=A5=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Style: ๋”๋ฏธ ํŒŒ์ผ ์‚ญ์ œ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ QueryDSL ๋กœ์ง ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์‘๋‹ต DTO ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ €์žฅ DTO ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ €์žฅ DTO ๋ž˜ํผ ํด๋ž˜์Šค ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ์ฝ”๋“œ ๊ตฌํ˜„ (#36) * Feat: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#36) * Test: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ €์žฅ DTO ๋ž˜ํผ ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#36) * Test: ๋ฆฌ๋ทฐ ํƒ€์ž… ๋ณ„ ๋‚˜์˜ ๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#36) * Test: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ ์‚ญ์ œ (#36) --- .../review/controller/MyReviewController.java | 51 ++++ .../com/livable/server/review/domain/.gitkeep | 0 .../server/review/domain/MyReview.java | 53 ++++ .../review/domain/MyReviewErrorCode.java | 17 ++ .../server/review/dto/MyReviewProjection.java | 29 ++ .../server/review/dto/MyReviewResponse.java | 26 ++ .../review/repository/MyReviewRepository.java | 16 + .../repository/MyReviewRepositoryImpl.java | 76 +++++ .../review/service/MyReviewService.java | 47 +++ .../controller/MyReviewControllerTest.java | 107 +++++++ .../server/review/domain/MyReviewTest.java | 134 +++++++++ .../review/service/MyReviewServiceTest.java | 283 ++++++++++++++++++ 12 files changed, 839 insertions(+) create mode 100644 src/main/java/com/livable/server/review/controller/MyReviewController.java delete mode 100644 src/main/java/com/livable/server/review/domain/.gitkeep create mode 100644 src/main/java/com/livable/server/review/domain/MyReview.java create mode 100644 src/main/java/com/livable/server/review/domain/MyReviewErrorCode.java create mode 100644 src/main/java/com/livable/server/review/dto/MyReviewProjection.java create mode 100644 src/main/java/com/livable/server/review/dto/MyReviewResponse.java create mode 100644 src/main/java/com/livable/server/review/repository/MyReviewRepository.java create mode 100644 src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java create mode 100644 src/main/java/com/livable/server/review/service/MyReviewService.java create mode 100644 src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java create mode 100644 src/test/java/com/livable/server/review/domain/MyReviewTest.java create mode 100644 src/test/java/com/livable/server/review/service/MyReviewServiceTest.java diff --git a/src/main/java/com/livable/server/review/controller/MyReviewController.java b/src/main/java/com/livable/server/review/controller/MyReviewController.java new file mode 100644 index 00000000..d2cb8c03 --- /dev/null +++ b/src/main/java/com/livable/server/review/controller/MyReviewController.java @@ -0,0 +1,51 @@ +package com.livable.server.review.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.review.dto.MyReviewResponse; +import com.livable.server.review.service.MyReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class MyReviewController { + + private final MyReviewService myReviewService; + + @GetMapping("/api/reviews/restaurant/{reviewId}/members") + public ResponseEntity> getMyRestaurantReview( + @PathVariable Long reviewId) { + + // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ + Long memberId = 1L; + + MyReviewResponse.DetailDTO myRestaurantReview = myReviewService.getMyRestaurantReview(reviewId, memberId); + return ApiResponse.success(myRestaurantReview, HttpStatus.OK); + } + + @GetMapping("/api/reviews/cafeteria/{reviewId}/members") + public ResponseEntity> getMyCafeteriaReviewDetail( + @PathVariable Long reviewId) { + + // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ + Long memberId = 1L; + + MyReviewResponse.DetailDTO myCafeteriaReview = myReviewService.getMyCafeteriaReview(reviewId, memberId); + return ApiResponse.success(myCafeteriaReview, HttpStatus.OK); + } + + @GetMapping("/api/reviews/lunchbox/{reviewId}/members") + public ResponseEntity> getMyLunchboxReviewDetail( + @PathVariable Long reviewId) { + + // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ + Long memberId = 1L; + + MyReviewResponse.DetailDTO myLunchBoxReview = myReviewService.getMyLunchBoxReview(reviewId, memberId); + return ApiResponse.success(myLunchBoxReview, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/review/domain/.gitkeep b/src/main/java/com/livable/server/review/domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/review/domain/MyReview.java b/src/main/java/com/livable/server/review/domain/MyReview.java new file mode 100644 index 00000000..7ffe6fc5 --- /dev/null +++ b/src/main/java/com/livable/server/review/domain/MyReview.java @@ -0,0 +1,53 @@ +package com.livable.server.review.domain; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.review.dto.MyReviewProjection; +import com.livable.server.review.dto.MyReviewResponse; + +import java.util.List; +import java.util.stream.Collectors; + +public class MyReview { + + private final List reviews; + + private MyReview(List reviews) { + validationReviews(reviews); + this.reviews = reviews; + } + + private void validationReviews(List reviews) { + if (reviews.isEmpty()) { + throw new GlobalRuntimeException(MyReviewErrorCode.REVIEW_NOT_EXIST); + } + } + + public static MyReview from(List reviews) { + return new MyReview(reviews); + } + + public MyReviewResponse.DetailDTO toResponseDTO() { + + MyReviewProjection myReviewDTO = this.getTopOne(); + List images = this.getImages(); + + return MyReviewResponse.DetailDTO.builder() + .reviewTitle(myReviewDTO.getReviewTitle()) + .reviewTaste(myReviewDTO.getReviewTaste()) + .reviewDescription(myReviewDTO.getReviewDescription()) + .reviewCreatedAt(myReviewDTO.getReviewCreatedAt()) + .location(myReviewDTO.getLocation()) + .reviewImg(images) + .build(); + } + + private MyReviewProjection getTopOne() { + return reviews.get(0); + } + + private List getImages() { + return reviews.stream() + .map(MyReviewProjection::getReviewImg) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/livable/server/review/domain/MyReviewErrorCode.java b/src/main/java/com/livable/server/review/domain/MyReviewErrorCode.java new file mode 100644 index 00000000..4c3baf89 --- /dev/null +++ b/src/main/java/com/livable/server/review/domain/MyReviewErrorCode.java @@ -0,0 +1,17 @@ +package com.livable.server.review.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum MyReviewErrorCode implements ErrorCode { + + REVIEW_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ๋ทฐ ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/review/dto/MyReviewProjection.java b/src/main/java/com/livable/server/review/dto/MyReviewProjection.java new file mode 100644 index 00000000..3d7c2864 --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/MyReviewProjection.java @@ -0,0 +1,29 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.Evaluation; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class MyReviewProjection { + + private String reviewTitle; + private Evaluation reviewTaste; + private String reviewDescription; + private LocalDateTime reviewCreatedAt; + private String location; + private String reviewImg; + + public MyReviewProjection(String reviewTitle, String reviewDescription, LocalDateTime reviewCreatedAt, String reviewImg) { + this.reviewTitle = reviewTitle; + this.reviewDescription = reviewDescription; + this.reviewCreatedAt = reviewCreatedAt; + this.reviewImg = reviewImg; + } +} diff --git a/src/main/java/com/livable/server/review/dto/MyReviewResponse.java b/src/main/java/com/livable/server/review/dto/MyReviewResponse.java new file mode 100644 index 00000000..36e2d6e9 --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/MyReviewResponse.java @@ -0,0 +1,26 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.Evaluation; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MyReviewResponse { + + @Getter + @Builder + public static class DetailDTO { + + private String reviewTitle; + private Evaluation reviewTaste; + private String reviewDescription; + private LocalDateTime reviewCreatedAt; + private List reviewImg; + private String location; + } +} diff --git a/src/main/java/com/livable/server/review/repository/MyReviewRepository.java b/src/main/java/com/livable/server/review/repository/MyReviewRepository.java new file mode 100644 index 00000000..42482a8e --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/MyReviewRepository.java @@ -0,0 +1,16 @@ +package com.livable.server.review.repository; + +import com.livable.server.review.dto.MyReviewProjection; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MyReviewRepository { + + List findRestaurantReviewByReviewId(Long reviewId, Long memberId); + + List findLunchBoxReviewByReviewId(Long reviewId, Long memberId); + + List findCafeteriaReviewByReviewId(Long reviewId, Long memberId); +} diff --git a/src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java new file mode 100644 index 00000000..4f7eba34 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java @@ -0,0 +1,76 @@ +package com.livable.server.review.repository; + +import com.livable.server.review.dto.MyReviewProjection; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.livable.server.entity.QBuilding.building; +import static com.livable.server.entity.QCafeteriaReview.cafeteriaReview; +import static com.livable.server.entity.QRestaurant.restaurant; +import static com.livable.server.entity.QRestaurantReview.restaurantReview; +import static com.livable.server.entity.QReview.review; +import static com.livable.server.entity.QReviewImage.reviewImage; + +@Component +@RequiredArgsConstructor +public class MyReviewRepositoryImpl implements MyReviewRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findRestaurantReviewByReviewId(Long reviewId, Long memberId) { + return jpaQueryFactory + .select(Projections.constructor(MyReviewProjection.class, + review.selectedDishes, + restaurantReview.taste, + review.description, + review.createdAt, + restaurant.name, + reviewImage.url + )) + .from(review) + .leftJoin(reviewImage).on(reviewImage.review.id.eq(review.id)) + .innerJoin(restaurantReview).on(restaurantReview.id.eq(review.id)) + .innerJoin(restaurant).on(restaurant.id.eq(restaurantReview.restaurant.id)) + .where(review.id.eq(reviewId).and(review.member.id.eq(memberId))) + .fetch(); + } + + @Override + public List findLunchBoxReviewByReviewId(Long reviewId, Long memberId) { + return jpaQueryFactory + .select(Projections.constructor(MyReviewProjection.class, + review.selectedDishes, + review.description, + review.createdAt, + reviewImage.url + )) + .from(review) + .leftJoin(reviewImage).on(reviewImage.review.id.eq(review.id)) + .where(review.id.eq(reviewId).and(review.member.id.eq(memberId))) + .fetch(); + } + + @Override + public List findCafeteriaReviewByReviewId(Long reviewId, Long memberId) { + return jpaQueryFactory + .select(Projections.constructor(MyReviewProjection.class, + review.selectedDishes, + cafeteriaReview.taste, + review.description, + review.createdAt, + building.name, + reviewImage.url + )) + .from(review) + .leftJoin(reviewImage).on(reviewImage.review.id.eq(review.id)) + .innerJoin(cafeteriaReview).on(cafeteriaReview.id.eq(review.id)) + .innerJoin(building).on(building.id.eq(cafeteriaReview.building.id)) + .where(review.id.eq(reviewId).and(review.member.id.eq(memberId))) + .fetch(); + } +} diff --git a/src/main/java/com/livable/server/review/service/MyReviewService.java b/src/main/java/com/livable/server/review/service/MyReviewService.java new file mode 100644 index 00000000..c2e650e5 --- /dev/null +++ b/src/main/java/com/livable/server/review/service/MyReviewService.java @@ -0,0 +1,47 @@ +package com.livable.server.review.service; + +import com.livable.server.review.domain.MyReview; +import com.livable.server.review.dto.MyReviewProjection; +import com.livable.server.review.dto.MyReviewResponse; +import com.livable.server.review.repository.MyReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class MyReviewService { + + private final MyReviewRepository myReviewRepository; + + public MyReviewResponse.DetailDTO getMyRestaurantReview(Long reviewId, Long memberId) { + + List myReviewProjections + = myReviewRepository.findRestaurantReviewByReviewId(reviewId, memberId); + + return this.convertToDTO(myReviewProjections); + } + + public MyReviewResponse.DetailDTO getMyCafeteriaReview(Long reviewId, Long memberId) { + + List myReviewProjections + = myReviewRepository.findCafeteriaReviewByReviewId(reviewId, memberId); + + return this.convertToDTO(myReviewProjections); + } + + public MyReviewResponse.DetailDTO getMyLunchBoxReview(Long reviewId, Long memberId) { + + List myReviewProjections + = myReviewRepository.findLunchBoxReviewByReviewId(reviewId, memberId); + + return this.convertToDTO(myReviewProjections); + } + + private MyReviewResponse.DetailDTO convertToDTO(List myReviewProjections) { + + MyReview myReview = MyReview.from(myReviewProjections); + return myReview.toResponseDTO(); + } +} diff --git a/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java new file mode 100644 index 00000000..753153f5 --- /dev/null +++ b/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java @@ -0,0 +1,107 @@ +package com.livable.server.review.controller; + +import com.livable.server.review.dto.MyReviewResponse; +import com.livable.server.review.service.MyReviewService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@WebMvcTest(MyReviewController.class) +class MyReviewControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MyReviewService myReviewService; + + @Nested + @DisplayName("๋‚˜์˜ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/restaurant/1/members"; + + MyReviewResponse.DetailDTO mockResponse + = MyReviewResponse.DetailDTO.builder().build(); + + Mockito.when(myReviewService.getMyRestaurantReview( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockResponse); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } + + @Nested + @DisplayName("๋‚˜์˜ ๊ตฌ๋‚ด์‹๋‹น ๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyCafeteriaReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/cafeteria/1/members"; + + MyReviewResponse.DetailDTO mockResponse + = MyReviewResponse.DetailDTO.builder().build(); + + Mockito.when(myReviewService.getMyCafeteriaReview( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockResponse); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } + + @Nested + @DisplayName("๋‚˜์˜ ๋„์‹œ๋ฝ ๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyLunchBoxReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/lunchbox/1/members"; + + MyReviewResponse.DetailDTO mockResponse + = MyReviewResponse.DetailDTO.builder().build(); + + Mockito.when(myReviewService.getMyLunchBoxReview( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockResponse); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/review/domain/MyReviewTest.java b/src/test/java/com/livable/server/review/domain/MyReviewTest.java new file mode 100644 index 00000000..06ce9ba5 --- /dev/null +++ b/src/test/java/com/livable/server/review/domain/MyReviewTest.java @@ -0,0 +1,134 @@ +package com.livable.server.review.domain; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Evaluation; +import com.livable.server.review.dto.MyReviewProjection; +import com.livable.server.review.dto.MyReviewResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("๋‚˜์˜ ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋ž˜ํผ ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ") +class MyReviewTest { + + + @DisplayName("์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ (์ƒ์„ฑ์ž ๋ฐ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ ๊ฐ„์ ‘ ํ…Œ์ŠคํŠธ)") + @Nested + class StaticFactoryMethod { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + List myReviewProjections = List.of( + MyReviewProjection.builder() + .reviewTitle("TestTitle") + .reviewDescription("TestDescription") + .reviewCreatedAt(LocalDateTime.now()) + .reviewTaste(Evaluation.GOOD) + .reviewImg("TestImage") + .location("TestLocation") + .build() + ); + + // When + // Then + MyReview.from(myReviewProjections); + } + + @DisplayName("์‹คํŒจ - ๋น„์–ด์žˆ๋Š” ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋กœ ์ƒ์„ฑ") + @Test + void failure_Test_constructedEmptyList() { + // Given + List myReviewProjections = List.of(); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + MyReview.from(myReviewProjections)); + } + } + + @DisplayName("DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (getTopOne, getImages ๊ฐ„์ ‘ ํ…Œ์ŠคํŠธ)") + @Nested + class ConvertToDTO { + + @DisplayName("์„ฑ๊ณต - ์‹ฑ๊ธ€ ์ด๋ฏธ์ง€") + @Test + void success_Test_SingleImage() { + // Given + List myReviewProjections = List.of( + MyReviewProjection.builder() + .reviewTitle("TestTitle") + .reviewDescription("TestDescription") + .reviewCreatedAt(LocalDateTime.now()) + .reviewTaste(Evaluation.GOOD) + .reviewImg("TestImage") + .location("TestLocation") + .build() + ); + + // When + MyReview myReview = MyReview.from(myReviewProjections); + MyReviewResponse.DetailDTO actual = myReview.toResponseDTO(); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals("TestTitle", actual.getReviewTitle()), + () -> Assertions.assertEquals(Evaluation.GOOD, actual.getReviewTaste()), + () -> Assertions.assertEquals(1, actual.getReviewImg().size()) + ); + } + + @DisplayName("์„ฑ๊ณต - ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€") + @Test + void success_Test_MultipleImage() { + // Given + List myReviewProjections = List.of( + MyReviewProjection.builder() + .reviewTitle("TestTitle") + .reviewDescription("TestDescription") + .reviewCreatedAt(LocalDateTime.now()) + .reviewTaste(Evaluation.GOOD) + .reviewImg("TestImage1") + .location("TestLocation") + .build(), + + MyReviewProjection.builder() + .reviewTitle("TestTitle") + .reviewDescription("TestDescription") + .reviewCreatedAt(LocalDateTime.now()) + .reviewTaste(Evaluation.GOOD) + .reviewImg("TestImage2") + .location("TestLocation") + .build(), + + MyReviewProjection.builder() + .reviewTitle("TestTitle") + .reviewDescription("TestDescription") + .reviewCreatedAt(LocalDateTime.now()) + .reviewTaste(Evaluation.GOOD) + .reviewImg("TestImage3") + .location("TestLocation") + .build() + ); + + // When + MyReview myReview = MyReview.from(myReviewProjections); + MyReviewResponse.DetailDTO actual = myReview.toResponseDTO(); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals("TestTitle", actual.getReviewTitle()), + () -> Assertions.assertEquals(Evaluation.GOOD, actual.getReviewTaste()), + () -> Assertions.assertEquals(3, actual.getReviewImg().size()) + ); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java b/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java new file mode 100644 index 00000000..ea702c26 --- /dev/null +++ b/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java @@ -0,0 +1,283 @@ +package com.livable.server.review.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.review.dto.MyReviewProjection; +import com.livable.server.review.dto.MyReviewResponse; +import com.livable.server.review.repository.MyReviewRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class MyReviewServiceTest { + + @Mock + private MyReviewRepository myReviewRepository; + + @InjectMocks + private MyReviewService myReviewService; + + @Nested + @DisplayName("๋‚˜์˜ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์‹ฑ๊ธ€ ์ด๋ฏธ์ง€)") + @Test + void success_Test_SingleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String reviewDescription = "๋ง›์žˆ์˜ค"; + String imgUrl = "mockImage.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewDescription(reviewDescription).reviewImg(imgUrl).build() + ); + + Mockito.when(myReviewRepository.findRestaurantReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyRestaurantReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(reviewDescription, actual.getReviewDescription()), + () -> Assertions.assertEquals(1, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl, actual.getReviewImg().get(0)) + ); + } + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€)") + @Test + void success_Test_MultipleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String imgUrl1 = "mockImage1.jpg"; + String imgUrl2 = "mockImage2.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewImg(imgUrl1).build(), + MyReviewProjection.builder().reviewImg(imgUrl2).build() + ); + + Mockito.when(myReviewRepository.findRestaurantReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyRestaurantReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(2, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl1, actual.getReviewImg().get(0)), + () -> Assertions.assertEquals(imgUrl2, actual.getReviewImg().get(1)) + ); + } + + @DisplayName("์‹คํŒจ - DTO๋ณ€ํ™˜ ์˜ค๋ฅ˜") + @Test + void failure_Test_FailedConvertToDTO() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + + Mockito.when(myReviewRepository.findRestaurantReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(new ArrayList<>()); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + myReviewService.getMyRestaurantReview(reviewId, memberId)); + } + } + + @Nested + @DisplayName("๋‚˜์˜ ๊ตฌ๋‚ด์‹๋‹น ๋ฆฌ๋ทฐ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyCafeteriaReview { + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์‹ฑ๊ธ€ ์ด๋ฏธ์ง€)") + @Test + void success_Test_SingleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String reviewDescription = "๋ง›์žˆ์˜ค"; + String imgUrl = "mockImage.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewDescription(reviewDescription).reviewImg(imgUrl).build() + ); + + Mockito.when(myReviewRepository.findCafeteriaReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyCafeteriaReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(reviewDescription, actual.getReviewDescription()), + () -> Assertions.assertEquals(1, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl, actual.getReviewImg().get(0)) + ); + } + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€)") + @Test + void success_Test_MultipleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String imgUrl1 = "mockImage1.jpg"; + String imgUrl2 = "mockImage2.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewImg(imgUrl1).build(), + MyReviewProjection.builder().reviewImg(imgUrl2).build() + ); + + Mockito.when(myReviewRepository.findCafeteriaReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyCafeteriaReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(2, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl1, actual.getReviewImg().get(0)), + () -> Assertions.assertEquals(imgUrl2, actual.getReviewImg().get(1)) + ); + } + + @DisplayName("์‹คํŒจ - DTO๋ณ€ํ™˜ ์˜ค๋ฅ˜") + @Test + void failure_Test_FailedConvertToDTO() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + + Mockito.when(myReviewRepository.findCafeteriaReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(new ArrayList<>()); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + myReviewService.getMyCafeteriaReview(reviewId, memberId)); + } + } + + @Nested + @DisplayName("๋‚˜์˜ ๋„์‹œ๋ฝ ๋ฆฌ๋ทฐ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyLunchBoxReview { + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์‹ฑ๊ธ€ ์ด๋ฏธ์ง€)") + @Test + void success_Test_SingleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String reviewDescription = "๋ง›์žˆ์˜ค"; + String imgUrl = "mockImage.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewDescription(reviewDescription).reviewImg(imgUrl).build() + ); + + Mockito.when(myReviewRepository.findLunchBoxReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyLunchBoxReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(reviewDescription, actual.getReviewDescription()), + () -> Assertions.assertEquals(1, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl, actual.getReviewImg().get(0)) + ); + } + + @DisplayName("์„ฑ๊ณต - DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ (์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€)") + @Test + void success_Test_MultipleImg() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + String imgUrl1 = "mockImage1.jpg"; + String imgUrl2 = "mockImage2.jpg"; + + List mockList = List.of( + MyReviewProjection.builder().reviewImg(imgUrl1).build(), + MyReviewProjection.builder().reviewImg(imgUrl2).build() + ); + + Mockito.when(myReviewRepository.findLunchBoxReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(mockList); + + + // When + MyReviewResponse.DetailDTO actual = myReviewService.getMyLunchBoxReview(reviewId, memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(2, actual.getReviewImg().size()), + () -> Assertions.assertEquals(imgUrl1, actual.getReviewImg().get(0)), + () -> Assertions.assertEquals(imgUrl2, actual.getReviewImg().get(1)) + ); + } + + @DisplayName("์‹คํŒจ - DTO๋ณ€ํ™˜ ์˜ค๋ฅ˜") + @Test + void failure_Test_FailedConvertToDTO() { + // Given + Long reviewId = 1L; + Long memberId = 1L; + + Mockito.when(myReviewRepository.findLunchBoxReviewByReviewId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong() + )).thenReturn(new ArrayList<>()); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + myReviewService.getMyLunchBoxReview(reviewId, memberId)); + } + } +} \ No newline at end of file From dde0e575ed8d07189da6e22522b4c9f576ae1d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 21 Sep 2023 16:27:32 +0900 Subject: [PATCH 028/111] =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Style: ๋”๋ฏธ ํŒŒ์ผ ์‚ญ์ œ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ €์žฅ DTO ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์‘๋‹ต DTO ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์‘๋‹ต์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ์ฝ”๋“œ ๊ตฌํ˜„ (#60) * Feat: ๋งˆ์ดํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#60) --- .../livable/server/member/controller/.gitkeep | 0 .../member/controller/MemberController.java | 26 +++++++ .../server/member/domain/MemberErrorCode.java | 17 +++++ .../com/livable/server/member/dto/.gitkeep | 0 .../server/member/dto/MemberResponse.java | 24 ++++++ .../server/member/dto/MyPageProjection.java | 14 ++++ .../livable/server/member/exception/.gitkeep | 0 .../livable/server/member/repository/.gitkeep | 0 .../member/repository/MemberRepository.java | 12 +++ .../livable/server/member/service/.gitkeep | 0 .../server/member/service/MemberService.java | 27 +++++++ .../controller/MemberControllerTest.java | 54 ++++++++++++++ .../member/service/MemberServiceTest.java | 74 +++++++++++++++++++ 13 files changed, 248 insertions(+) delete mode 100644 src/main/java/com/livable/server/member/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/member/controller/MemberController.java create mode 100644 src/main/java/com/livable/server/member/domain/MemberErrorCode.java delete mode 100644 src/main/java/com/livable/server/member/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/member/dto/MemberResponse.java create mode 100644 src/main/java/com/livable/server/member/dto/MyPageProjection.java delete mode 100644 src/main/java/com/livable/server/member/exception/.gitkeep delete mode 100644 src/main/java/com/livable/server/member/repository/.gitkeep delete mode 100644 src/main/java/com/livable/server/member/service/.gitkeep create mode 100644 src/main/java/com/livable/server/member/service/MemberService.java create mode 100644 src/test/java/com/livable/server/member/controller/MemberControllerTest.java create mode 100644 src/test/java/com/livable/server/member/service/MemberServiceTest.java diff --git a/src/main/java/com/livable/server/member/controller/.gitkeep b/src/main/java/com/livable/server/member/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/member/controller/MemberController.java b/src/main/java/com/livable/server/member/controller/MemberController.java new file mode 100644 index 00000000..dca66fc1 --- /dev/null +++ b/src/main/java/com/livable/server/member/controller/MemberController.java @@ -0,0 +1,26 @@ +package com.livable.server.member.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.member.dto.MemberResponse; +import com.livable.server.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class MemberController { + + private final MemberService memberService; + + @GetMapping("/api/members") + public ResponseEntity> myPage() { + + Long memberId = 1L; // TODO: JWT ๊ตฌํ˜„ ํ›„ ํ† ํฐ์—์„œ ๊ฐ’ ์ถ”์ถœ ์˜ˆ์ • + MemberResponse.MyPageDTO myPageDTO = memberService.getMyPageData(memberId); + + return ApiResponse.success(myPageDTO, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java new file mode 100644 index 00000000..49b54e75 --- /dev/null +++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java @@ -0,0 +1,17 @@ +package com.livable.server.member.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum MemberErrorCode implements ErrorCode { + + MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/member/dto/.gitkeep b/src/main/java/com/livable/server/member/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/member/dto/MemberResponse.java b/src/main/java/com/livable/server/member/dto/MemberResponse.java new file mode 100644 index 00000000..ff1e757d --- /dev/null +++ b/src/main/java/com/livable/server/member/dto/MemberResponse.java @@ -0,0 +1,24 @@ +package com.livable.server.member.dto; + +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberResponse { + + @Getter + @Builder + public static class MyPageDTO { + + String memberName; + String companyName; + Integer pointValance; + + public static MyPageDTO from(MyPageProjection myPageProjection) { + return MyPageDTO.builder() + .memberName(myPageProjection.getMemberName()) + .companyName(myPageProjection.getCompanyName()) + .pointValance(myPageProjection.getPointValance()) + .build(); + } + } +} diff --git a/src/main/java/com/livable/server/member/dto/MyPageProjection.java b/src/main/java/com/livable/server/member/dto/MyPageProjection.java new file mode 100644 index 00000000..a76af197 --- /dev/null +++ b/src/main/java/com/livable/server/member/dto/MyPageProjection.java @@ -0,0 +1,14 @@ +package com.livable.server.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@AllArgsConstructor +public class MyPageProjection { + + String memberName; + String companyName; + Integer pointValance; +} diff --git a/src/main/java/com/livable/server/member/exception/.gitkeep b/src/main/java/com/livable/server/member/exception/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/member/repository/.gitkeep b/src/main/java/com/livable/server/member/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java index 0f6d8b11..4d8e7929 100644 --- a/src/main/java/com/livable/server/member/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -1,7 +1,19 @@ package com.livable.server.member.repository; import com.livable.server.entity.Member; +import com.livable.server.member.dto.MyPageProjection; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface MemberRepository extends JpaRepository { + + @Query("SELECT new com.livable.server.member.dto.MyPageProjection(m.name, c.name, p.balance) " + + "FROM Member m " + + "INNER JOIN Company c ON c.id = m.company.id " + + "INNER JOIN Point p ON p.member.id = m.id " + + "WHERE m.id = :memberId") + Optional findMemberCompanyPointData(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/livable/server/member/service/.gitkeep b/src/main/java/com/livable/server/member/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java new file mode 100644 index 00000000..88426755 --- /dev/null +++ b/src/main/java/com/livable/server/member/service/MemberService.java @@ -0,0 +1,27 @@ +package com.livable.server.member.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.dto.MemberResponse; +import com.livable.server.member.dto.MyPageProjection; +import com.livable.server.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class MemberService { + + private final MemberRepository memberRepository; + + public MemberResponse.MyPageDTO getMyPageData(Long memberId) { + + Optional myPageProjectionOption = memberRepository.findMemberCompanyPointData(memberId); + MyPageProjection myPageProjection = myPageProjectionOption.orElseThrow(() + -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)); + + return MemberResponse.MyPageDTO.from(myPageProjection); + } +} diff --git a/src/test/java/com/livable/server/member/controller/MemberControllerTest.java b/src/test/java/com/livable/server/member/controller/MemberControllerTest.java new file mode 100644 index 00000000..5658bb56 --- /dev/null +++ b/src/test/java/com/livable/server/member/controller/MemberControllerTest.java @@ -0,0 +1,54 @@ +package com.livable.server.member.controller; + +import com.livable.server.member.dto.MemberResponse; +import com.livable.server.member.service.MemberService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@WebMvcTest(MemberController.class) +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MemberService memberService; + + @Nested + @DisplayName("๋งˆ์ดํŽ˜์ด์ง€ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/members"; + + MemberResponse.MyPageDTO mockDTO = MemberResponse.MyPageDTO.builder() + .memberName("TestName") + .companyName("TestCompany") + .pointValance(200) + .build(); + + Mockito.when(memberService.getMyPageData(ArgumentMatchers.anyLong())) + .thenReturn(mockDTO); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/member/service/MemberServiceTest.java b/src/test/java/com/livable/server/member/service/MemberServiceTest.java new file mode 100644 index 00000000..c73e7a0c --- /dev/null +++ b/src/test/java/com/livable/server/member/service/MemberServiceTest.java @@ -0,0 +1,74 @@ +package com.livable.server.member.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.member.dto.MemberResponse; +import com.livable.server.member.dto.MyPageProjection; +import com.livable.server.member.repository.MemberRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private MemberService memberService; + + @Nested + @DisplayName("๋งˆ์ดํŽ˜์ด์ง€ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + Long memberId = 1L; + String memberName = "TestName"; + String companyName = "TestCompany"; + Integer pointValance = 200; + + MyPageProjection mockResult + = new MyPageProjection(memberName, companyName, pointValance); + + Mockito.when(memberRepository.findMemberCompanyPointData(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(mockResult)); + + // When + MemberResponse.MyPageDTO actual = memberService.getMyPageData(memberId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(memberName, actual.getMemberName()), + () -> Assertions.assertEquals(companyName, actual.getCompanyName()), + () -> Assertions.assertEquals(pointValance, actual.getPointValance()) + ); + } + + @DisplayName("์‹คํŒจ - ์œ ํšจํ•˜์ง€ ์•Š์€ ํšŒ์› ์ •๋ณด") + @Test + void failure_Test_InvalidMember() { + // Given + Long memberId = 1L; + + // When + Mockito.when(memberRepository.findMemberCompanyPointData(ArgumentMatchers.anyLong())) + .thenReturn(Optional.empty()); + + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + memberService.getMyPageData(memberId)); + } + } +} From cbaff5ee9e744fb3d4c12e2a0e3e3aff8460a0e3 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:58:50 +0900 Subject: [PATCH 029/111] =?UTF-8?q?=EB=B9=8C=EB=94=A9=20=EB=82=B4=20?= =?UTF-8?q?=EC=8B=9D=EB=8B=B9=20=EB=B0=8F=20=EC=B9=B4=ED=8E=98=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20API=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#6?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: StringToRestaurantCategoryConverter ๊ตฌํ˜„(#55) * Test: CustomConverter ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#55) StringToRestaurantCategoryConverter์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ * Feat: VisitorRepository.findBuildingIdById ๊ตฌํ˜„ (#55) ์‚ฌ์šฉ๋˜๋Š” ๊ณณ์ด ๋งŽ์•„์„œ ๋”ฐ๋กœ ๋ถ„๋ฆฌ * Test: VisitorRepository.findBuildingIdById ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#55) * Feat: RestaurantRepository ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#55) findRestaurantByBuildingIdAndRestaurantCategory ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ * Modify: RestaurantRepository ์ฟผ๋ฆฌ ์ˆ˜์ • (#55) * Feat: ์ฃผ๋ณ€ ์‹๋‹น ์‘๋‹ต Dto ์ƒ์„ฑ (#55) * Feat: ๋žœ๋ค ์ƒ์„ฑ๊ธฐ ๊ตฌํ˜„ (#55) * Feat: BuildingRestaurantMap ๋„ค์ด๋ฐ ๋ฉ”์„œ๋“œ ์ž‘์„ฑ (#55) * Feat: RestaurantService ์ฃผ๋ณ€ ์Œ์‹์  ์กฐํšŒ ๋กœ์ง ์ž‘์„ฑ (#55) * Feat: RestaurantController ์ฃผ๋ณ€ ์Œ์‹์  ์กฐํšŒ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ (#55) * Feat: RestaurantRepository Test ํด๋ž˜์Šค ์ƒ์„ฑ (#55) * Modify: ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ ์ œ๊ฑฐ (#55) * Test: RestaurantController.findRestaurantByCategory ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#55) * Refactor: RandomGenerator ๋ฆฌํŒฉํ† ๋ง (#55) ๊ณตํ†ต ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ * Feat: ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ (#55) VisitationRequest ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ * Feat: ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ (#55) VisitationRequest ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ * Refactor: ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ…Œ์ŠคํŠธ ๋ฆฌํŒฉํ„ฐ๋ง (#55) * Refactor: ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ…Œ์ŠคํŠธ ๋ฆฌํŒฉํ„ฐ๋ง (#55) * Test: RestaurantService ํ…Œ์ŠคํŠธ ๊ตฌํ˜„(#55) RestaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory ์„ฑ๊ณต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ * Test: RestaurantService ํ…Œ์ŠคํŠธ ๊ตฌํ˜„(#55) RestaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory ์‹คํŒจ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ --- .../livable/server/core/config/WebConfig.java | 15 ++ .../StringToRestaurantCategoryConverter.java | 12 ++ .../server/entity/RestaurantCategory.java | 23 ++- .../controller/RestaurantController.java | 35 +++++ .../restaurant/domain/RandomGenerator.java | 17 +++ .../domain/RandomPageGenerator.java | 22 +++ .../domain/RestaurantErrorCode.java | 16 +++ .../restaurant/dto/RestaurantResponse.java | 28 ++++ .../BuildingRestaurantMapRepository.java | 13 ++ .../RestaurantCustomRepository.java | 16 +++ .../RestaurantCustomRepositoryImpl.java | 76 ++++++++++ .../repository/RestaurantRepository.java | 7 + .../restaurant/service/RestaurantService.java | 46 ++++++ .../domain/VisitationValidationMessage.java | 1 + .../visitation/dto/VisitationRequest.java | 8 ++ .../repository/VisitorCustomRepository.java | 2 + .../VisitorCustomRepositoryImpl.java | 21 +++ ...ringToRestaurantCategoryConverterTest.java | 56 ++++++++ .../controller/RestaurantControllerTest.java | 59 ++++++++ .../mock/MockNearRestaurantDto.java | 6 + .../repository/RestaurantRepositoryTest.java | 69 +++++++++ .../service/RestaurantServiceTest.java | 112 +++++++++++++++ .../visitation/dto/VisitationRequestTest.java | 134 ++++++++++++++++-- .../repository/ParkingLogRepositoryTest.java | 2 +- .../repository/VisitorRepositoryTest.java | 33 ++++- 25 files changed, 809 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/livable/server/core/config/WebConfig.java create mode 100644 src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java create mode 100644 src/main/java/com/livable/server/restaurant/controller/RestaurantController.java create mode 100644 src/main/java/com/livable/server/restaurant/domain/RandomGenerator.java create mode 100644 src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java create mode 100644 src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java create mode 100644 src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java create mode 100644 src/main/java/com/livable/server/restaurant/repository/BuildingRestaurantMapRepository.java create mode 100644 src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java create mode 100644 src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java create mode 100644 src/main/java/com/livable/server/restaurant/repository/RestaurantRepository.java create mode 100644 src/main/java/com/livable/server/restaurant/service/RestaurantService.java create mode 100644 src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java create mode 100644 src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java create mode 100644 src/test/java/com/livable/server/restaurant/mock/MockNearRestaurantDto.java create mode 100644 src/test/java/com/livable/server/restaurant/repository/RestaurantRepositoryTest.java create mode 100644 src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java diff --git a/src/main/java/com/livable/server/core/config/WebConfig.java b/src/main/java/com/livable/server/core/config/WebConfig.java new file mode 100644 index 00000000..bff3d525 --- /dev/null +++ b/src/main/java/com/livable/server/core/config/WebConfig.java @@ -0,0 +1,15 @@ +package com.livable.server.core.config; + +import com.livable.server.core.util.StringToRestaurantCategoryConverter; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToRestaurantCategoryConverter()); + } +} diff --git a/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java b/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java new file mode 100644 index 00000000..824574ec --- /dev/null +++ b/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java @@ -0,0 +1,12 @@ +package com.livable.server.core.util; + +import com.livable.server.entity.RestaurantCategory; +import org.springframework.core.convert.converter.Converter; + +public class StringToRestaurantCategoryConverter implements Converter { + + @Override + public RestaurantCategory convert(String event) { + return RestaurantCategory.of(event); + } +} diff --git a/src/main/java/com/livable/server/entity/RestaurantCategory.java b/src/main/java/com/livable/server/entity/RestaurantCategory.java index b98fca14..fb3e1144 100644 --- a/src/main/java/com/livable/server/entity/RestaurantCategory.java +++ b/src/main/java/com/livable/server/entity/RestaurantCategory.java @@ -1,7 +1,26 @@ package com.livable.server.entity; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.restaurant.domain.RestaurantErrorCode; + +import java.util.Arrays; +import java.util.List; + public enum RestaurantCategory { - RESTAURANT, - CAFE; + RESTAURANT(List.of("RESTAURANT", "restaurant", "Restaurant")), + CAFE(List.of("CAFE", "cafe", "Cafe")); + + private final List symbols; + + RestaurantCategory(List symbols) { + this.symbols = symbols; + } + + public static RestaurantCategory of(String symbol) { + return Arrays.stream(values()) + .filter(restaurantCategory -> restaurantCategory.symbols.contains(symbol)) + .findFirst() + .orElseThrow(() -> new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_CATEGORY)); + } } diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java new file mode 100644 index 00000000..1afc9c99 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -0,0 +1,35 @@ +package com.livable.server.restaurant.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.service.RestaurantService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/restaurant") +public class RestaurantController { + + private final RestaurantService restaurantService; + + @GetMapping + public ResponseEntity> findRestaurantByCategory( + @RequestParam("type") RestaurantCategory restaurantCategory + ) { + Long visitorId = 1L; + + List result = + restaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory(visitorId, restaurantCategory); + + return ApiResponse.success(result, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/restaurant/domain/RandomGenerator.java b/src/main/java/com/livable/server/restaurant/domain/RandomGenerator.java new file mode 100644 index 00000000..982dc641 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/domain/RandomGenerator.java @@ -0,0 +1,17 @@ +package com.livable.server.restaurant.domain; + +import java.util.Random; + +public interface RandomGenerator { + + default int getRandomNumber(int end) { + return getRandomNumber(0, end); + } + + default int getRandomNumber(int start, int end) { + return new Random().nextInt(end) + start; + } + + T getRandom(int end); + T getRandom(int start, int end); +} diff --git a/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java b/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java new file mode 100644 index 00000000..4009a41a --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java @@ -0,0 +1,22 @@ +package com.livable.server.restaurant.domain; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@Component +public class RandomPageGenerator implements RandomGenerator { + + private static final int DEFAULT_PAGE_SIZE = 5; + + @Override + public Pageable getRandom(int end) { + return getRandom(0, end); + } + + @Override + public Pageable getRandom(int start, int end) { + int randomNumber = getRandomNumber(start, end - DEFAULT_PAGE_SIZE); + return PageRequest.of(randomNumber, DEFAULT_PAGE_SIZE); + } +} diff --git a/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java b/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java new file mode 100644 index 00000000..4f7d0561 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java @@ -0,0 +1,16 @@ +package com.livable.server.restaurant.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum RestaurantErrorCode implements ErrorCode { + NOT_FOUND_CATEGORY(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹๋‹น ์ข…๋ฅ˜์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java new file mode 100644 index 00000000..ffedf080 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java @@ -0,0 +1,28 @@ +package com.livable.server.restaurant.dto; + +import com.livable.server.entity.RestaurantCategory; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class RestaurantResponse { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class NearRestaurantDto { + + private RestaurantCategory restaurantCategory; + private String restaurantName; + private String restaurantImageUrl; + + private Boolean inBuilding; + + private Integer takenTime; + private Integer floor; + + private String url; + } +} diff --git a/src/main/java/com/livable/server/restaurant/repository/BuildingRestaurantMapRepository.java b/src/main/java/com/livable/server/restaurant/repository/BuildingRestaurantMapRepository.java new file mode 100644 index 00000000..2bd534b7 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/repository/BuildingRestaurantMapRepository.java @@ -0,0 +1,13 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.entity.BuildingRestaurantMap; +import com.livable.server.entity.RestaurantCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BuildingRestaurantMapRepository extends JpaRepository { + + Integer countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( + Long buildingId, + RestaurantCategory restaurantCategory + ); +} diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java new file mode 100644 index 00000000..f9660e05 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java @@ -0,0 +1,16 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.dto.RestaurantResponse; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface RestaurantCustomRepository { + + List findRestaurantByBuildingIdAndRestaurantCategory( + Long buildingId, + RestaurantCategory category, + Pageable pageable + ); +} diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java new file mode 100644 index 00000000..ebbe9ae8 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java @@ -0,0 +1,76 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.entity.QBuilding; +import com.livable.server.entity.QBuildingRestaurantMap; +import com.livable.server.entity.QRestaurant; +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.dto.RestaurantResponse; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class RestaurantCustomRepositoryImpl implements RestaurantCustomRepository { + + private final JPAQueryFactory queryFactory; + private static final int DISTANCE_PER_TIME = 80; + + @Override + public List findRestaurantByBuildingIdAndRestaurantCategory( + final Long buildingId, + final RestaurantCategory category, + final Pageable pageable + ) { + + final QRestaurant restaurant = QRestaurant.restaurant; + final QBuildingRestaurantMap buildingRestaurantMap = QBuildingRestaurantMap.buildingRestaurantMap; + final QBuilding building = QBuilding.building; + + StringExpression extractFloorFromAddressTemplate = Expressions.stringTemplate( + "REPLACE(REPLACE(SUBSTRING_INDEX({0}, ' ', -1), '์ง€ํ•˜', '-'), '์ธต', '')", + restaurant.address + ); + + + JPAQuery query = queryFactory + .selectDistinct(Projections.constructor(RestaurantResponse.NearRestaurantDto.class, + restaurant.restaurantCategory, + restaurant.name, + restaurant.thumbnailImageUrl, + buildingRestaurantMap.inBuilding, + new CaseBuilder() + .when(buildingRestaurantMap.inBuilding.eq(true)) + .then(0) + .otherwise(buildingRestaurantMap.distance + .divide(DISTANCE_PER_TIME) + .round() + .castToNum(Integer.class) + ), + new CaseBuilder() + .when(buildingRestaurantMap.inBuilding.eq(false)) + .then(0) + .otherwise( + extractFloorFromAddressTemplate + .castToNum(Integer.class) + ), + restaurant.restaurantUrl + )) + .from(building) + .innerJoin(buildingRestaurantMap).on(buildingRestaurantMap.building.id.eq(building.id)) + .innerJoin(restaurant).on(buildingRestaurantMap.restaurant.id.eq(restaurant.id)) + .where(building.id.eq(buildingId).and(restaurant.restaurantCategory.eq(category))) + .offset(pageable.getPageNumber()) + .limit(pageable.getPageSize()); + + return query.fetchJoin().fetch(); + } +} diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantRepository.java new file mode 100644 index 00000000..f49e2eb9 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.entity.Restaurant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantRepository extends JpaRepository, RestaurantCustomRepository { +} diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java new file mode 100644 index 00000000..917107f6 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -0,0 +1,46 @@ +package com.livable.server.restaurant.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.domain.RandomGenerator; +import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; +import com.livable.server.restaurant.repository.RestaurantRepository; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.repository.VisitorRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class RestaurantService { + + private final RandomGenerator randomGenerator; + + private final RestaurantRepository restaurantRepository; + private final VisitorRepository visitorRepository; + private final BuildingRestaurantMapRepository buildingRestaurantMapRepository; + + public List findNearRestaurantByVisitorIdAndRestaurantCategory( + Long visitorId, RestaurantCategory category + ) { + Long buildingId = visitorRepository.findBuildingIdById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + + Integer nearRestaurantCount = + buildingRestaurantMapRepository.countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( + buildingId, + category + ); + + return restaurantRepository.findRestaurantByBuildingIdAndRestaurantCategory( + buildingId, category, randomGenerator.getRandom(nearRestaurantCount) + ); + } +} diff --git a/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java b/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java index 17570633..c9af105c 100644 --- a/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java +++ b/src/main/java/com/livable/server/visitation/domain/VisitationValidationMessage.java @@ -3,4 +3,5 @@ public interface VisitationValidationMessage { String INVALID_CAR_NUMBER = "์ฐจ๋Ÿ‰ ๋ฒˆํ˜ธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."; + String NOT_BLANK = "๊ฐ’์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"; } diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java index eb303b8e..ec0afc59 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java @@ -5,18 +5,26 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VisitationRequest { @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) public static class ValidateQrCodeDto { + + @NotBlank(message = VisitationValidationMessage.NOT_BLANK) private String qr; } @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) public static class RegisterParkingDto { + + @NotBlank(message = VisitationValidationMessage.NOT_BLANK) @Pattern(regexp = "^\\d{2,3}[๊ฐ€-ํžฃ]{1}\\d{4}$", message = VisitationValidationMessage.INVALID_CAR_NUMBER) private String carNumber; } diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java index 47f81bb3..d7742e14 100644 --- a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepository.java @@ -7,4 +7,6 @@ public interface VisitorCustomRepository { Optional findVisitationDetailInformationById(final Long visitorId); + + Optional findBuildingIdById(final Long visitorId); } diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java index 3f69adcc..732cbd28 100644 --- a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java @@ -50,4 +50,25 @@ public Optional findVisitationDetailInf return Optional.ofNullable(query.fetchJoin().fetchOne()); } + + @Override + public Optional findBuildingIdById(Long visitorId) { + + final QBuilding building = QBuilding.building; + final QVisitor visitor = QVisitor.visitor; + final QInvitation invitation = QInvitation.invitation; + final QMember member = QMember.member; + final QCompany company = QCompany.company; + + JPAQuery query = queryFactory + .select(building.id) + .from(visitor) + .innerJoin(invitation).on(visitor.invitation.id.eq(invitation.id)) + .innerJoin(member).on(invitation.member.id.eq(member.id)) + .innerJoin(company).on(member.company.id.eq(company.id)) + .innerJoin(building).on(company.building.id.eq(building.id)) + .where(visitor.id.eq(visitorId)); + + return Optional.ofNullable(query.fetchJoin().fetchOne()); + } } diff --git a/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java b/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java new file mode 100644 index 00000000..0a627c5d --- /dev/null +++ b/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java @@ -0,0 +1,56 @@ +package com.livable.server.core.util; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.domain.RestaurantErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class StringToRestaurantCategoryConverterTest { + + public static StringToRestaurantCategoryConverter converter = new StringToRestaurantCategoryConverter(); + + @DisplayName("StringToRestaurantConverter ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_1") + @CsvSource({"RESTAURANT", "restaurant", "Restaurant"}) + @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + void convertSuccessTest_RESTAURANT(String symbol) { + + // Given + // When + RestaurantCategory restaurantCategory = converter.convert(symbol); + + // Then + assertThat(restaurantCategory).isEqualTo(RestaurantCategory.RESTAURANT); + } + + @DisplayName("StringToRestaurantConverter ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_2") + @CsvSource({"CAFE", "cafe", "Cafe"}) + @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + void convertSuccessTest_CAFE(String symbol) { + + // Given + // When + RestaurantCategory restaurantCategory = converter.convert(symbol); + + // Then + assertThat(restaurantCategory).isEqualTo(RestaurantCategory.CAFE); + } + + @DisplayName("StringToRestaurantConverter ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @CsvSource({"CAFe", "caFe", "CAfe", "123", "zz", "restAuRant"}) + @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + void convertFailTest(String symbol) { + + // Given + // When + GlobalRuntimeException globalRuntimeException = + assertThrows(GlobalRuntimeException.class, () -> converter.convert(symbol)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(RestaurantErrorCode.NOT_FOUND_CATEGORY); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java new file mode 100644 index 00000000..0f652d49 --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -0,0 +1,59 @@ +package com.livable.server.restaurant.controller; + +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.mock.MockNearRestaurantDto; +import com.livable.server.restaurant.service.RestaurantService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(RestaurantController.class) +class RestaurantControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + RestaurantService restaurantService; + + @DisplayName("[GET][/api/restaurant?type={category}] - ๋นŒ๋”ฉ ๋‚ด, ๊ทผ์ฒ˜ ์‹๋‹น ์ •์ƒ ์‘๋‹ต") + @CsvSource(value = {"CAFE", "RESTAURANT", "restaurant", "Restaurant", "Cafe", "cafe"}) + @ParameterizedTest(name = "[{index}] category={0}") + void findRestaurantByCategorySuccessTest(String category) throws Exception { + // given + List result = + IntStream.range(1, 10) + .mapToObj(idx -> new MockNearRestaurantDto()) + .collect(Collectors.toList()); + + given(restaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory( + 1L, RestaurantCategory.of(category) + )).willReturn(result); + + + // when + ResultActions resultActions = mockMvc.perform( + get("/api/restaurant") + .queryParam("type", "restaurant") + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/restaurant/mock/MockNearRestaurantDto.java b/src/test/java/com/livable/server/restaurant/mock/MockNearRestaurantDto.java new file mode 100644 index 00000000..a3797e7f --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/mock/MockNearRestaurantDto.java @@ -0,0 +1,6 @@ +package com.livable.server.restaurant.mock; + +import com.livable.server.restaurant.dto.RestaurantResponse; + +public class MockNearRestaurantDto extends RestaurantResponse.NearRestaurantDto { +} diff --git a/src/test/java/com/livable/server/restaurant/repository/RestaurantRepositoryTest.java b/src/test/java/com/livable/server/restaurant/repository/RestaurantRepositoryTest.java new file mode 100644 index 00000000..5315897d --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/repository/RestaurantRepositoryTest.java @@ -0,0 +1,69 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.Building; +import com.livable.server.entity.BuildingRestaurantMap; +import com.livable.server.entity.Restaurant; +import com.livable.server.entity.RestaurantCategory; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; +import java.time.LocalTime; +import java.util.stream.IntStream; + +@DataJpaTest +@Import(QueryDslConfig.class) +class RestaurantRepositoryTest { + + @Autowired + RestaurantRepository restaurantRepository; + + @Autowired + EntityManager entityManager; + + @Autowired + BuildingRestaurantMapRepository buildingRestaurantMapRepository; + + @BeforeEach + void dataInit() { + Building building = Building.builder() + .name("63๋นŒ๋”ฉ") + .scale("์ง€ํ•˜ 3์ธต, ์ง€์ƒ 63์ธต") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10๋ถ„๋‹น 1000์›") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 61๊ธธ 7, 392") + .subwayStation("์„์ดŒ์—ญ") + .build(); + + entityManager.persist(building); + + IntStream.range(1, 10) + .forEach(idx -> { + Restaurant restaurant = Restaurant.builder() + .name("restaurant" + idx) + .address("์„œ์šธ์‹œ ๊ฐ•๋™๊ตฌ ํƒœ์œค๋นŒ๋”ฉ " + (idx % 2 == 0 ? "์ง€ํ•˜" : "") + idx + "์ธต") + .contact("contact" + idx) + .restaurantCategory(idx % 3 == 0 ? RestaurantCategory.CAFE : RestaurantCategory.RESTAURANT) + .restaurantUrl("url" + idx) + .thumbnailImageUrl("thumbnail" + idx) + .representativeCategory("ui category" + idx) + .build(); + restaurantRepository.save(restaurant); + BuildingRestaurantMap restaurantMap = BuildingRestaurantMap.builder() + .restaurant(restaurant) + .building(building) + .inBuilding(idx % 2 == 0) + .distance(idx * 80) + .build(); + buildingRestaurantMapRepository.save(restaurantMap); + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java new file mode 100644 index 00000000..203df0ff --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java @@ -0,0 +1,112 @@ +package com.livable.server.restaurant.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.domain.RandomGenerator; +import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; +import com.livable.server.restaurant.repository.RestaurantRepository; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.repository.VisitorRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class RestaurantServiceTest { + + @InjectMocks + RestaurantService restaurantService; + + @Mock + RandomGenerator randomGenerator; + @Mock + RestaurantRepository restaurantRepository; + @Mock + VisitorRepository visitorRepository; + @Mock + BuildingRestaurantMapRepository buildingRestaurantMapRepository; + + @DisplayName("RestaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findNearRestaurantByVisitorIdAndRestaurantCategorySuccessTest() { + // given + Long visitorId = 1L; + RestaurantCategory category = RestaurantCategory.RESTAURANT; + List dtos = IntStream.range(0, 5) + .mapToObj(idx -> new RestaurantResponse.NearRestaurantDto()) + .collect(Collectors.toList()); + + Pageable pageRequest = PageRequest.of(1, 5); + + given(visitorRepository.findBuildingIdById(anyLong())).willReturn(Optional.of(1L)); + given(buildingRestaurantMapRepository.countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( + anyLong(), + any(RestaurantCategory.class) + )).willReturn(10); + given(randomGenerator.getRandom(anyInt())).willReturn(pageRequest); + given(restaurantRepository.findRestaurantByBuildingIdAndRestaurantCategory( + anyLong(), any(RestaurantCategory.class), any(Pageable.class) + )).willReturn(dtos); + + // when + List result = + restaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory(visitorId, category); + + // then + then(visitorRepository).should(times(1)).findBuildingIdById(anyLong()); + then(buildingRestaurantMapRepository).should(times(1)) + .countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( + anyLong(), + any(RestaurantCategory.class) + ); + then(randomGenerator).should(times(1)).getRandom(anyInt()); + then(restaurantRepository).should(times(1)) + .findRestaurantByBuildingIdAndRestaurantCategory( + anyLong(), + any(RestaurantCategory.class), + any(Pageable.class) + ); + assertThat(dtos).usingRecursiveComparison().isEqualTo(result); + } + + @DisplayName("RestaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findNearRestaurantByVisitorIdAndRestaurantCategoryFailTest() { + // given + Long visitorId = 1L; + RestaurantCategory category = RestaurantCategory.RESTAURANT; + List dtos = IntStream.range(0, 5) + .mapToObj(idx -> new RestaurantResponse.NearRestaurantDto()) + .collect(Collectors.toList()); + + given(visitorRepository.findBuildingIdById(anyLong())).willReturn(Optional.empty()); + + // when + GlobalRuntimeException globalRuntimeException = assertThrows( + GlobalRuntimeException.class, + () -> restaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory(visitorId, category) + ); + + // then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(visitorRepository).should(times(1)).findBuildingIdById(anyLong()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java index fb8fd399..bd55e3c5 100644 --- a/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java +++ b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java @@ -2,50 +2,154 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.livable.server.visitation.domain.VisitationValidationMessage; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; - +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.*; class VisitationRequestTest { - private static ValidatorFactory factory; private static Validator validator; - - private static ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + @BeforeAll public static void init() { - factory = Validation.buildDefaultValidatorFactory(); + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } - @DisplayName("VisitationRequest.RegisterParkingDto carNumber ์ •๊ทœ์‹ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") - @CsvSource({"12๊ฐ€1234", "12ํซ3456", "123ํ•ก0000"}) + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber Pattern ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @ValueSource(strings = {"12๊ฐ€1234", "12ํซ3456", "123ํ•ก0000"}) @ParameterizedTest(name = "[{index}] ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ: {0}") void carNumberPatternSuccessTest(String carNumber) throws JsonProcessingException { - String requestDto = "{\n" + - " \"carNumber\": \"" + carNumber + "\"\n" + - " }"; + VisitationRequest.RegisterParkingDto registerParkingDto = registerParkingDtoSerialize(carNumber); + + Set> validate = validator.validate(registerParkingDto); + + assertThat(validate.size()).isEqualTo(0); + } + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber Pattern ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @ValueSource(strings = {"123", "124", "๊ฐ€๊ฐ€๊ฐ€๊ฐ๊ฐ€"}) + @ParameterizedTest(name = "[{index}] ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ: {0}") + void carNumberPatternFailTest(String carNumber) throws JsonProcessingException { + VisitationRequest.RegisterParkingDto registerParkingDto = registerParkingDtoSerialize(carNumber); + + Set> validate = validator.validate(registerParkingDto); + List errorMessages = getErrorMessages(validate); + + assertThat(validate.size()).isEqualTo(1); + assertThat(errorMessages.contains(VisitationValidationMessage.INVALID_CAR_NUMBER)).isTrue(); + } - VisitationRequest.RegisterParkingDto registerParkingDto = - objectMapper.readValue(requestDto, VisitationRequest.RegisterParkingDto.class); + @DisplayName("VisitationRequest.RegisterParkingDto carNumber NotBlank ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_1") + @ValueSource(strings = {"abcde", "124", "๊ฐ€๊ฐ€๊ฐ€๊ฐ๊ฐ€"}) + @ParameterizedTest(name = "[{index}] ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ: {0}") + void carNumberNotBlankSuccess(String carNumber) throws JsonProcessingException { + VisitationRequest.RegisterParkingDto registerParkingDto = registerParkingDtoSerialize(carNumber); Set> validate = validator.validate(registerParkingDto); + List messages = getErrorMessages(validate); + + assertThat(validate.size()).isEqualTo(1); + assertThat(messages.contains(VisitationValidationMessage.INVALID_CAR_NUMBER)).isTrue(); + } + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber NotBlank ์‹คํŒจ ํ…Œ์ŠคํŠธ_1") + @ValueSource(strings = {"", " "}) + @ParameterizedTest(name = "[{index}] ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ: {0}") + void carNumberNotBlankFailTest_1(String carNumber) throws JsonProcessingException { + VisitationRequest.RegisterParkingDto registerParkingDto = registerParkingDtoSerialize(carNumber); + + Set> validate = validator.validate(registerParkingDto); + + List errorMessages = getErrorMessages(validate); + + assertThat(validate.size()).isEqualTo(2); + assertThat(errorMessages.contains(VisitationValidationMessage.NOT_BLANK)).isTrue(); + } + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber NotBlank ์‹คํŒจ ํ…Œ์ŠคํŠธ_2") + @NullSource + @ParameterizedTest + void carNumberNotBlankFailTest_2(String carNumber) throws JsonProcessingException { + VisitationRequest.RegisterParkingDto registerParkingDto = registerParkingDtoSerialize(carNumber); + + Set> validate = validator.validate(registerParkingDto); + + List errorMessages = getErrorMessages(validate); + + assertThat(validate.size()).isEqualTo(1); + assertThat(errorMessages.contains(VisitationValidationMessage.NOT_BLANK)).isTrue(); + } + + @DisplayName("VisitationRequest.ValidateQrCodeDto qr NotBlank ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void qrNotBlankSuccess() throws JsonProcessingException { + VisitationRequest.ValidateQrCodeDto validateQrCodeDto = validateQrCodeDtoSerialize("123"); + + Set> validate = validator.validate(validateQrCodeDto); + + List errorMessages = getErrorMessages(validate); + assertThat(validate.size()).isEqualTo(0); } + + @DisplayName("VisitationRequest.ValidateQrCodeDto qr NotBlank ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @NullSource + @ParameterizedTest + void qrNotBlankSuccess(String qr) throws JsonProcessingException { + VisitationRequest.ValidateQrCodeDto validateQrCodeDto = validateQrCodeDtoSerialize(qr); + + Set> validate = validator.validate(validateQrCodeDto); + + List errorMessages = getErrorMessages(validate); + + assertThat(validate.size()).isEqualTo(1); + assertThat(errorMessages.contains(VisitationValidationMessage.NOT_BLANK)).isTrue(); + } + + private VisitationRequest.ValidateQrCodeDto validateQrCodeDtoSerialize(String qr) throws JsonProcessingException { + + if (qr == null) { + return new VisitationRequest.ValidateQrCodeDto(); + } + + String template = "{\"qr\":\"" + qr + "\"}"; + + return objectMapper.readValue(template, VisitationRequest.ValidateQrCodeDto.class); + } + + private VisitationRequest.RegisterParkingDto registerParkingDtoSerialize(String carNumber) throws JsonProcessingException { + + if (carNumber == null) { + return new VisitationRequest.RegisterParkingDto(); + } + + String template = "{\"carNumber\":\"" + carNumber + "\"}"; + + return objectMapper.readValue(template, VisitationRequest.RegisterParkingDto.class); + } + + private static > List getErrorMessages(Set validate) { + return validate.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java index c4a1c170..937023dc 100644 --- a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java +++ b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java @@ -101,7 +101,7 @@ void dataInit() { entityManager.persist(parkingLog); } - @DisplayName("ParkingLogRepository.findParkingLogByVisitorId ์ฟผ๋ฆฌ ํ™•์ธ์šฉ ํ…Œ์ŠคํŠธ") + @DisplayName("ParkingLogRepository.findParkingLogByVisitorId ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ") @Test void test() { Visitor visitor = visitorRepository.findAll().get(0); diff --git a/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java index 379c52b7..0f2e24f5 100644 --- a/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java +++ b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java @@ -13,6 +13,7 @@ import javax.persistence.EntityManager; import java.time.LocalDate; import java.time.LocalTime; +import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -92,9 +93,9 @@ void dataInit() { entityManager.persist(visitor); } - @DisplayName("test") + @DisplayName("VisitorRepository.findVisitationDetailInformationById ์ฟผ๋ฆฌ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test - void test() { + void findVisitationDetailInformationByIdSuccessTest() { Visitor visitor = visitorRepository.findAll().get(0); Invitation invitation = visitor.getInvitation(); Member member = invitation.getMember(); @@ -117,4 +118,32 @@ void test() { () -> assertThat(detailInformationDto.getBuildingRepresentativeImageUrl()).isEqualTo(building.getRepresentativeImageUrl()) ); } + + @DisplayName("VisitorRepository.findBuildingIdById ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ_1") + @Test + void findBuildingIdByIdSuccessTest_1() { + Visitor visitor = visitorRepository.findAll().get(0); + Invitation invitation = visitor.getInvitation(); + Member member = invitation.getMember(); + Company company = member.getCompany(); + Building building = company.getBuilding(); + + Long buildingIdById = visitorRepository.findBuildingIdById(visitor.getId()).get(); + + assertThat(buildingIdById).isEqualTo(building.getId()); + } + + @DisplayName("VisitorRepository.findBuildingIdById ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ_2") + @Test + void findBuildingIdByIdSuccessTest_2() { + Visitor visitor = visitorRepository.findAll().get(0); + Invitation invitation = visitor.getInvitation(); + Member member = invitation.getMember(); + Company company = member.getCompany(); + Building building = company.getBuilding(); + + Optional buildingIdById = visitorRepository.findBuildingIdById(visitor.getId() + 1); + + assertThat(buildingIdById).isEqualTo(Optional.empty()); + } } \ No newline at end of file From 559d7c978ba5bf460a29be809b29aa8c902e1bc1 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Thu, 21 Sep 2023 18:04:44 +0900 Subject: [PATCH 030/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ํšŒ์› ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๋ณ€๊ฒฝ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์‚ญ์ œ Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Feat: ์—ฐ๊ด€๋œ ์˜ˆ์•ฝ ์ •๋ณด, ๋ฐฉ๋ฌธ์ž, ์ฃผ์ฐจ ์ •๋ณด ์‚ญ์ œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Feat: ์ฐพ๋Š” ์ดˆ๋Œ€์žฅ์ด ์—†๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ์—๋Ÿฌ ์ฝ”๋“œ ์ถ”๊ฐ€ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์‚ญ์ œ Service ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Test: ์ดˆ๋Œ€์žฅ ์‚ญ์ œ Service ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • DTO ์ƒ์„ฑ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • Controller ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Feat: Invitation ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— ๋ฐ์ดํ„ฐ ์ˆ˜์ •์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ ์ฝ”๋“œ ์ถ”๊ฐ€ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ํ•„์š”ํ•œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#57) * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • Service ๋กœ์ง ๊ตฌํ˜„ (#57) * Refactor: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • ์ถ”๊ฐ€ ์ธ์› ํ™•์ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฉ”์„œ๋“œ ์ˆ˜์ • (#57) * Test: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • Service ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#57) * Fix: ์ดˆ๋Œ€์žฅ ๋ชฉ์ ์ด ๋ฉด์ ‘์ผ ๊ฒฝ์šฐ ์ถ”๊ฐ€ ์ธ์› ํ™•์ธ ๋กœ์ง ์ˆ˜์ • (#57) * Modify: ์ดˆ๋Œ€์žฅ Service ํ…Œ์ŠคํŠธ ๊ณต๋ฐฑ ์ถ”๊ฐ€, ๋ณ€์ˆ˜ ๋ถ„๋ฆฌ (#57) * Fix: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • DTO ๋ฐฉ๋ฌธ์ž Valid๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ ์ถ”๊ฐ€ (#57) * Test: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • Controller ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#57) --- .../com/livable/server/entity/Invitation.java | 12 + .../controller/InvitationController.java | 17 + .../domain/InvitationErrorCode.java | 6 +- .../invitation/dto/InvitationRequest.java | 39 ++ .../repository/InvitationQueryRepository.java | 1 + .../InvitationQueryRepositoryImpl.java | 13 + .../InvitationReservationMapRepository.java | 6 + .../invitation/service/InvitationService.java | 155 ++++++- .../repository/ParkingLogRepository.java | 6 + .../repository/VisitorRepository.java | 13 + .../controller/InvitationControllerTest.java | 139 ++++++- .../service/InvitationServiceTest.java | 391 ++++++++++++++++++ 12 files changed, 778 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java index ff8d6a33..4ba52ddc 100644 --- a/src/main/java/com/livable/server/entity/Invitation.java +++ b/src/main/java/com/livable/server/entity/Invitation.java @@ -4,6 +4,7 @@ import javax.persistence.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; @Getter @@ -41,4 +42,15 @@ public class Invitation extends BaseTimeEntity { @Column(nullable = false) private LocalTime endTime; + + public void updateDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { + this.startDate = startDateTime.toLocalDate(); + this.startTime = startDateTime.toLocalTime(); + this.endDate = endDateTime.toLocalDate(); + this.endTime = endDateTime.toLocalTime(); + } + + public void updateDescription(String description) { + this.description = description; + } } diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index 60823bad..82d3857f 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -51,4 +51,21 @@ public ResponseEntity> getInvitation(@Path return invitationService.getInvitation(invitationId, memberId); } + @DeleteMapping("/{invitationId}") + public ResponseEntity deleteInvitation(@PathVariable Long invitationId) { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.deleteInvitation(invitationId, memberId); + } + + @PatchMapping("/{invitationId}") + public ResponseEntity updateInvitation( + @PathVariable Long invitationId, @Valid @RequestBody InvitationRequest.UpdateDTO dto) { + + Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + + return invitationService.updateInvitation(invitationId, dto, memberId); + } + } diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java index 7b1c5356..d58d39e4 100644 --- a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java +++ b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java @@ -10,12 +10,16 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum InvitationErrorCode implements ErrorCode { MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + INVITATION_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ดˆ๋Œ€์žฅ ์ •๋ณด์ž…๋‹ˆ๋‹ค."), INVALID_INTERVIEW_MAXIMUM_NUMBER(HttpStatus.BAD_REQUEST, "๋ฉด์ ‘ ์ดˆ๋Œ€ ๊ฐ€๋Šฅ ์ธ์›์ˆ˜๋Š” 1๋ช…์ž…๋‹ˆ๋‹ค."), + INVALID_INVITATION_MAXIMUM_NUMBER(HttpStatus.BAD_REQUEST, "ํ•œ ์ดˆ๋Œ€์žฅ์— ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ๋ฐฉ๋ฌธ์ž๋Š” 30๋ช…์ž…๋‹ˆ๋‹ค."), INVALID_DATE(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์‹œ์ž‘ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), INVALID_TIME(HttpStatus.BAD_REQUEST, "์ข…๋ฃŒ ์‹œ๊ฐ„์ด ์‹œ์ž‘ ์‹œ๊ฐ„๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), INVALID_TIME_UNIT(HttpStatus.BAD_REQUEST, "์‹œ๊ฐ„์˜ ๋ถ„ ๋‹จ์œ„๋Š” 0๋ถ„ ๋˜๋Š” 30๋ถ„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), INVALID_RESERVATION_COUNT(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋‚ ์งœ ๋˜๋Š” ์‹œ๊ฐ„์— ์˜ˆ์•ฝ๋œ ์žฅ์†Œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."), - INVALID_INVITATION_OWNER(HttpStatus.FORBIDDEN, "์ดˆ๋Œ€์žฅ์„ ์ž‘์„ฑํ•œ ํšŒ์›๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + INVALID_INVITATION_OWNER(HttpStatus.FORBIDDEN, "์ดˆ๋Œ€์žฅ์„ ์ž‘์„ฑํ•œ ํšŒ์›๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."), + INVALID_DELETE_DATE(HttpStatus.BAD_REQUEST, "์ดˆ๋Œ€์žฅ ์‚ญ์ œ๋Š” ๋ฐฉ๋ฌธ์ผ ๊ธฐ์ค€ ์ „๋‚ ๊นŒ์ง€๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."), + CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION(HttpStatus.BAD_REQUEST, "์ดˆ๋Œ€์žฅ์˜ ์˜ˆ์•ฝ๋œ ์žฅ์†Œ๋Š” ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java index ca67b194..f43491a8 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.Valid; import javax.validation.constraints.FutureOrPresent; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -77,4 +78,42 @@ public Visitor toEntity(Invitation invitation) { } } + @Getter + @Builder + public static class UpdateDTO { + private Long commonPlaceId; + private String description; + + @NotNull(message = NOT_NULL) + @FutureOrPresent(message = REQUIRED_FUTURE_DATE) + private LocalDateTime startDate; + + @NotNull(message = NOT_NULL) + @FutureOrPresent(message = REQUIRED_FUTURE_DATE) + private LocalDateTime endDate; + + @Valid + @NotNull(message = NOT_NULL) + private List visitors; + } + + @Getter + @Builder + public static class VisitorForUpdateDTO { + @NotNull(message = NOT_NULL) + private String name; + + @NotNull(message = NOT_NULL) + private String contact; + + public Visitor toEntity(Invitation invitation) { + return Visitor.builder() + .invitation(invitation) + .name(name) + .contact(contact) + .firstVisitedTime(null) + .build(); + } + } + } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java index 79db2f9b..aed6fd79 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java @@ -7,4 +7,5 @@ public interface InvitationQueryRepository { List findInvitationsByMemberId(Long memberId); InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long invitationId); + Long getCommonPlaceIdByInvitationId(Long invitationId); } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java index d77d2da3..77954fe3 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java @@ -91,4 +91,17 @@ public InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long return invitationDetail; } + + @Override + public Long getCommonPlaceIdByInvitationId(Long invitationId) { + return queryFactory + .select(reservation.commonPlace.id) + .from(invitation) + .leftJoin(invitationReservationMap) + .on(invitationReservationMap.invitation.id.eq(invitation.id)) + .leftJoin(reservation) + .on(reservation.id.eq(invitationReservationMap.reservation.id)) + .where(invitation.id.eq(invitationId)) + .fetchFirst(); + } } diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java index 6acdb20e..45287517 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java @@ -2,6 +2,12 @@ import com.livable.server.entity.InvitationReservationMap; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface InvitationReservationMapRepository extends JpaRepository { + + @Modifying + @Query("delete from InvitationReservationMap irm where irm.invitation.id = :invitationId") + void deleteAllByInvitationId(Long invitationId); } diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 88b648c0..8a007c1b 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -5,17 +5,18 @@ import com.livable.server.core.response.ApiResponse.Success; import com.livable.server.entity.*; import com.livable.server.invitation.domain.InvitationErrorCode; -import com.livable.server.invitation.dto.InvitationDetailTimeDto; import com.livable.server.invitation.domain.InvitationPurpose; +import com.livable.server.invitation.dto.InvitationDetailTimeDto; import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.repository.InvitationRepository; import com.livable.server.invitation.repository.InvitationReservationMapRepository; -import com.livable.server.member.repository.MemberRepository; import com.livable.server.invitation.repository.OfficeRepository; +import com.livable.server.member.repository.MemberRepository; import com.livable.server.reservation.repository.ReservationRepository; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.repository.ParkingLogRepository; import com.livable.server.visitation.repository.VisitorRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -36,13 +37,15 @@ @Service public class InvitationService { private static final int INTERVIEW_MAXIMUM_COUNT = 1; + private static final int INVITATION_MAXIMUM_COUNT = 30; private final MemberRepository memberRepository; private final OfficeRepository officeRepository; - private final ReservationRepository reservationRepository; private final InvitationRepository invitationRepository; - private final VisitorRepository visitorRepository; + private final ReservationRepository reservationRepository; private final InvitationReservationMapRepository invitationReservationMapRepository; + private final VisitorRepository visitorRepository; + private final ParkingLogRepository parkingLogRepository; public VisitationResponse.InvitationTimeDto findInvitationTime(Long visitorId) { InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(visitorId) @@ -108,7 +111,7 @@ private boolean isNotCombineTarget( } private Long getCompanyIdByMemberId(Long memberId) { - Member member = findMemberById(memberId); + Member member = checkExistMemberById(memberId); return member.getCompany().getId(); } @@ -117,7 +120,7 @@ private Long getCompanyIdByMemberId(Long memberId) { public ResponseEntity createInvitation(InvitationRequest.CreateDTO dto, Long memberId) { checkInterviewVisitorCount(dto); - Member member = findMemberById(memberId); + Member member = checkExistMemberById(memberId); Invitation invitation = createInvitation(dto, member); createVisitors(dto.getVisitors(), invitation); reserveCommonPlaces(dto, invitation); @@ -133,7 +136,7 @@ private void checkInterviewVisitorCount(InvitationRequest.CreateDTO dto) { } } - private Member findMemberById(Long memberId) { + private Member checkExistMemberById(Long memberId) { Optional memberOptional = memberRepository.findById(memberId); return memberOptional.orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); @@ -223,16 +226,16 @@ private void createInvitationReservationMap(List reservations, Invi @Transactional(readOnly = true) public ResponseEntity>> getInvitations(Long memberId) { - Member member = findMemberById(memberId); - List invitationDTOs = invitationRepository.findInvitationsByMemberId(member.getId()); + checkExistMemberById(memberId); + List invitationDTOs = invitationRepository.findInvitationsByMemberId(memberId); return ApiResponse.success(invitationDTOs, HttpStatus.OK); } @Transactional(readOnly = true) public ResponseEntity> getInvitation(Long invitationId, Long memberId) { - Member member = findMemberById(memberId); - checkInvitationOwner(invitationId, member.getId()); + checkExistMemberById(memberId); + checkInvitationOwner(invitationId, memberId); InvitationResponse.DetailDTO invitationDTO = invitationRepository.findInvitationAndVisitorsByInvitationId(invitationId); @@ -245,4 +248,134 @@ private void checkInvitationOwner(Long invitationId, Long memberId) { throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER); } } + + @Transactional + public ResponseEntity deleteInvitation(Long invitationId, Long memberId) { + checkExistMemberById(memberId); + checkInvitationOwner(invitationId, memberId); + + // Step 1. ์ดˆ๋Œ€์žฅ ๊ฐ€์ ธ์˜ด + Optional invitationOptional = invitationRepository.findById(invitationId); + Invitation invitation = invitationOptional + .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST)); + + // Step 2. ์ดˆ๋Œ€์žฅ ๋ฐฉ๋ฌธ๋‚ ์งœ ํ™•์ธ + checkInvitationStartDate(invitation); + + // Step 3. ์˜ˆ์•ฝ๋œ ์žฅ์†Œ์— ๋Œ€ํ•œ ์˜ˆ์•ฝ ์ •๋ณด ์ œ๊ฑฐ + deleteReservationsByInvitation(invitation); + + // Step 4. ์ดˆ๋Œ€์žฅ์— ๋“ฑ๋ก๋œ ๋ฐฉ๋ฌธ์ž ๋ฐ์ดํ„ฐ + ์ฃผ์ฐจ ๋“ฑ๋ก ๋ฐ์ดํ„ฐ ์‚ญ์ œ + deleteVisitorsAndParkingLogByInvitation(invitation); + + // Step 5. ์ดˆ๋Œ€์žฅ ์‚ญ์ œ + invitationRepository.delete(invitation); + + return ApiResponse.success(HttpStatus.OK); + } + + private void checkInvitationStartDate(Invitation invitation) { + if (invitation.getStartDate().isBefore(LocalDate.now())) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_DELETE_DATE); + } + } + + private void deleteReservationsByInvitation(Invitation invitation) { + invitationReservationMapRepository.deleteAllByInvitationId(invitation.getId()); + } + + private void deleteVisitorsAndParkingLogByInvitation(Invitation invitation) { + List visitors = visitorRepository.findVisitorsByInvitation(invitation); + List visitorsIds = visitors.stream().map(Visitor::getId).collect(Collectors.toList()); + + parkingLogRepository.deleteByVisitorIdsIn(visitorsIds); + visitorRepository.deleteByIdsIn(visitorsIds); + } + + @Transactional + public ResponseEntity updateInvitation(Long invitationId, InvitationRequest.UpdateDTO dto, Long memberId) { + checkExistMemberById(memberId); + checkInvitationOwner(invitationId, memberId); + + Optional invitationOptional = invitationRepository.findById(invitationId); + Invitation invitation = invitationOptional + .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST)); + + checkInvitationStartDate(invitation); + checkModifiedCommonPlaceId(invitation, dto); + + boolean shouldSendToAlreadyVisitor = false; + boolean shouldSendToAddedVisitor = checkAddedVisitorsCount(invitation, dto); + + if (isModifiedInvitationDateTime(invitation, dto)) { + shouldSendToAlreadyVisitor = true; + if (isReservedCommonPlace(dto.getCommonPlaceId())) { + + invitationReservationMapRepository.deleteAllByInvitationId(invitation.getId()); + reserveNewCommonPlaces(dto, invitation); + } + } + + invitation.updateDateTime(dto.getStartDate(), dto.getEndDate()); + invitation.updateDescription(dto.getDescription()); + + if (shouldSendToAlreadyVisitor) { + List currentVisitors = visitorRepository.findVisitorsByInvitation(invitation); + + // TODO: ๊ธฐ์กด ๋“ฑ๋ก๋˜์–ด ์žˆ๋˜ ๋ฐฉ๋ฌธ์ž๋“ค์—๊ฒŒ ์•Œ๋ฆผํ†ก์„ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ๋กœ์ง ์ถ”๊ฐ€ + } + + if (shouldSendToAddedVisitor) { + List visitors = dto.getVisitors().stream() + .map(visitor -> visitor.toEntity(invitation)).collect(Collectors.toList()); + + visitorRepository.saveAll(visitors); + + // TODO: ์ƒˆ๋กœ ๋“ฑ๋ก๋œ ๋ฐฉ๋ฌธ์ž๋“ค์—๊ฒŒ ์•Œ๋ฆผํ†ก์„ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ๋กœ์ง ์ถ”๊ฐ€ + } + + return ApiResponse.success(HttpStatus.OK); + } + + private boolean checkAddedVisitorsCount(Invitation invitation, InvitationRequest.UpdateDTO dto) { + long addedCount = dto.getVisitors().size(); + + if (addedCount != 0L && invitation.getPurpose().equals(InvitationPurpose.INTERVIEW.getValue())) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER); + } + + long alreadyCount = visitorRepository.countByInvitation(invitation); + + if (alreadyCount + addedCount > INVITATION_MAXIMUM_COUNT) { + throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_MAXIMUM_NUMBER); + } + + return addedCount != 0; + } + + private boolean isModifiedInvitationDateTime(Invitation invitation, InvitationRequest.UpdateDTO dto) { + return !LocalDateTime.of(invitation.getStartDate(), invitation.getStartTime()).isEqual(dto.getStartDate()) + || !LocalDateTime.of(invitation.getEndDate(), invitation.getEndTime()).isEqual(dto.getEndDate()); + } + + private void checkModifiedCommonPlaceId(Invitation invitation, InvitationRequest.UpdateDTO dto) { + Long currentCommonPlaceId = invitationRepository.getCommonPlaceIdByInvitationId(invitation.getId()); + + if (!currentCommonPlaceId.equals(dto.getCommonPlaceId())) { + throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } + } + + private void reserveNewCommonPlaces(InvitationRequest.UpdateDTO dto, Invitation invitation) { + LocalDateTime startDateTime = dto.getStartDate(); + LocalDateTime endDateTime = dto.getEndDate(); + checkDateTimeValidate(startDateTime, endDateTime); + + int expectedReservationCount = getExpectedReservationCount(startDateTime, endDateTime); + List reservations = reservationRepository + .findReservationsByCommonPlaceIdAndStartDateAndEndDate(dto.getCommonPlaceId(), startDateTime, endDateTime); + + checkReservationCount(reservations, expectedReservationCount); + createInvitationReservationMap(reservations, invitation); + } } diff --git a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java index baecaf70..a2987afa 100644 --- a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java @@ -2,9 +2,11 @@ import com.livable.server.entity.ParkingLog; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface ParkingLogRepository extends JpaRepository { @@ -12,4 +14,8 @@ public interface ParkingLogRepository extends JpaRepository { @Query("select p from ParkingLog p" + " where p.visitor.id = :visitorId") Optional findParkingLogByVisitorId(@Param("visitorId") final Long visitorId); + + @Modifying + @Query("delete from ParkingLog p where p.visitor.id in :visitorIds") + void deleteByVisitorIdsIn(@Param("visitorIds") List visitorIds); } diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java index 041bc8d7..4cb878fa 100644 --- a/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/VisitorRepository.java @@ -1,7 +1,20 @@ package com.livable.server.visitation.repository; +import com.livable.server.entity.Invitation; import com.livable.server.entity.Visitor; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface VisitorRepository extends JpaRepository, VisitorCustomRepository { + List findVisitorsByInvitation(Invitation invitation); + + @Modifying + @Query("delete from Visitor v where v.id in :ids") + void deleteByIdsIn(@Param("ids") List ids); + + long countByInvitation(Invitation invitation); } diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index 463ea710..0cc0467e 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDateTime; import java.util.ArrayList; @@ -25,8 +26,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -117,12 +119,12 @@ void createInvitationFail_01() throws Exception { // When & Then mockMvc.perform( - post("/api/invitation") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(dto)) - ) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_FUTURE_DATE)); + post("/api/invitation") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_FUTURE_DATE)); } @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž๊ฐ€ ํ•œ ๋ช…๋„ ์—†๋Š” ๊ฒฝ์šฐ)") @@ -204,4 +206,125 @@ void getInvitationSuccess_01() throws Exception { mockMvc.perform(get("/api/invitation/{invitationId}", invitationId)) .andExpect(status().isOk()); } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ์ž‘ ๋‚ ์งœ, ์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์š”์ฒญ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_01() throws Exception { + // Given + LocalDateTime requestDate = LocalDateTime.now(); + Long invitationId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ํƒ€๊ณ  ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์˜ค๋ฉด ๋ฐ”๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.") + .startDate(requestDate.minusDays(1L)) + .endDate(requestDate.minusDays(1L)) + .visitors(List.of()) + .build(); + + // When + ResultActions resultActions = mockMvc.perform( + patch("/api/invitation/{invitationId}", invitationId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto))); + + // Then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_FUTURE_DATE)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๋ฐฉ๋ฌธ์ž ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋„์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_02() throws Exception { + // Given + LocalDateTime requestDate = LocalDateTime.now(); + Long invitationId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ํƒ€๊ณ  ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์˜ค๋ฉด ๋ฐ”๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.") + .startDate(requestDate.plusDays(1L)) + .endDate(requestDate.plusDays(1L)) + .visitors(null) + .build(); + + // When + ResultActions resultActions = mockMvc.perform( + patch("/api/invitation/{invitationId}", invitationId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto))); + + // Then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.NOT_NULL)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๋ฐฉ๋ฌธ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_03() throws Exception { + // Given + LocalDateTime requestDate = LocalDateTime.now(); + Long invitationId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ํƒ€๊ณ  ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์˜ค๋ฉด ๋ฐ”๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.") + .startDate(requestDate.plusDays(1L)) + .endDate(requestDate.plusDays(1L)) + .visitors(List.of( + InvitationRequest.VisitorForUpdateDTO.builder() + .name(null) + .contact(null) + .build() + )) + .build(); + + // When + ResultActions resultActions = mockMvc.perform( + patch("/api/invitation/{invitationId}", invitationId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto))); + + // Then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.NOT_NULL)); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ •") + @Test + void updateInvitationSuccess_01() throws Exception { + // Given + LocalDateTime requestDate = LocalDateTime.now(); + Long invitationId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ํƒ€๊ณ  ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์˜ค๋ฉด ๋ฐ”๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.") + .startDate(requestDate.plusDays(1L)) + .endDate(requestDate.plusDays(1L)) + .visitors(List.of( + InvitationRequest.VisitorForUpdateDTO.builder() + .name("ํ™ํ”„๋ง") + .contact("01012341234") + .build() + )) + .build(); + + // When + ResultActions resultActions = mockMvc.perform( + patch("/api/invitation/{invitationId}", invitationId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto))); + + // TODO: ApiResponse.success() WildCard ๋ฆฌํŽ™ํ† ๋ง ํ›„ Mocking ์ฝ”๋“œ ์ถ”๊ฐ€ -> ํ˜„์žฌ๋Š” Stubbing ์•ˆ๋ผ์„œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜๋จ. + + // Then + resultActions.andExpect(status().isOk()); + + verify(invitationService, times(1)) + .updateInvitation(anyLong(), any(InvitationRequest.UpdateDTO.class), anyLong()); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 34ce773b..636f9c15 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -14,6 +14,7 @@ import com.livable.server.invitation.service.data.InvitationBasicData; import com.livable.server.member.repository.MemberRepository; import com.livable.server.reservation.repository.ReservationRepository; +import com.livable.server.visitation.repository.ParkingLogRepository; import com.livable.server.visitation.repository.VisitorRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -58,6 +59,9 @@ class InvitationServiceTest { @Mock private InvitationReservationMapRepository invitationReservationMapRepository; + @Mock + private ParkingLogRepository parkingLogRepository; + @InjectMocks private InvitationService invitationService; @@ -396,6 +400,7 @@ void getInvitationFail_01() { // Given Long invitationId = 1L; Long memberId = 1L; + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(0L); @@ -413,6 +418,7 @@ void getInvitationFail_02() { // Given Long invitationId = 1L; Long memberId = 1L; + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); given(invitationRepository.findInvitationAndVisitorsByInvitationId(anyLong())) @@ -426,6 +432,391 @@ void getInvitationFail_02() { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); } + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์‚ญ์ œ - ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ดˆ๋Œ€์žฅ์ธ ๊ฒฝ์šฐ") + @Test + void deleteInvitationFail_01() { + // Given + Long invitationId = 1L; + Long memberId = 1L; + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())).willReturn(Optional.empty()); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.deleteInvitation(invitationId, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVITATION_NOT_EXIST); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์‚ญ์ œ - ์‚ญ์ œ ์š”์ฒญ ๋‚ ์งœ๊ฐ€ ๋ฐฉ๋ฌธ ๋‚ ์งœ ์ดํ›„์ธ ๊ฒฝ์šฐ") + @Test + void deleteInvitationFail_02() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateBeforeRequestDate = requestDate.minusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .startDate(dateBeforeRequestDate) + .build() + )); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.deleteInvitation(invitationId, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_DELETE_DATE); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์‚ญ์ œ") + @Test + void deleteInvitationFail_03() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .startDate(dateAfterRequestDate) + .build() + )); + given(visitorRepository.findVisitorsByInvitation(any(Invitation.class))) + .willReturn(List.of(Visitor.builder().build())); + + // When + ResponseEntity result = invitationService.deleteInvitation(invitationId, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์ดˆ๋Œ€์žฅ ๋ฐฉ๋ฌธ ๋‚ ์งœ๊ฐ€ ์š”์ฒญ ๋‚ ์งœ๋ณด๋‹ค ๊ณผ๊ฑฐ์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_01() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateBeforeRequestDate = requestDate.minusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder().build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .startDate(dateBeforeRequestDate) + .build() + )); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_DELETE_DATE); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๊ธฐ์กด์— ์˜ˆ์•ฝ๋œ ์žฅ์†Œ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_02() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .startDate(dateAfterRequestDate) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())) + .willReturn(dto.getCommonPlaceId() + 1L); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๋ชฉ์ ์ธ ๋ฉด์ ‘์ธ๋ฐ ์ถ”๊ฐ€ ๋ฐฉ๋ฌธ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_03() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("๋ฉด์ ‘") + .startDate(dateAfterRequestDate) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์ตœ๋Œ€ ๋ฐฉ๋ฌธ์ž ์ˆ˜๋ฅผ ๋„˜์–ด๊ฐ„ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_04() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + given(visitorRepository.countByInvitation(any(Invitation.class))).willReturn(30L); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_INVITATION_MAXIMUM_NUMBER); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ๊ฐ„ ๋ณ€๊ฒฝ X, ์ธ์› ์ถ”๊ฐ€ X") + @Test + void updateInvitationSuccess_01() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of()) + .startDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 30, 0))) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .endDate(dateAfterRequestDate) + .startTime(LocalTime.of(10, 0, 0)) + .endTime(LocalTime.of(10, 30, 0)) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + given(visitorRepository.countByInvitation(any(Invitation.class))).willReturn(29L); + + // When + ResponseEntity result = invitationService.updateInvitation(invitationId, dto, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ๊ฐ„ ๋ณ€๊ฒฝ X, ์ธ์› ์ถ”๊ฐ€ O") + @Test + void updateInvitationSuccess_02() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors( + List.of( + InvitationRequest.VisitorForUpdateDTO.builder().build(), + InvitationRequest.VisitorForUpdateDTO.builder().build(), + InvitationRequest.VisitorForUpdateDTO.builder().build() + ) + ) + .startDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 30, 0))) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .endDate(dateAfterRequestDate) + .startTime(LocalTime.of(10, 0, 0)) + .endTime(LocalTime.of(10, 30, 0)) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + given(visitorRepository.countByInvitation(any(Invitation.class))).willReturn(1L); + given(visitorRepository.saveAll(any())).willReturn(List.of(Visitor.builder().build())); + + // When + ResponseEntity result = invitationService.updateInvitation(invitationId, dto, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ๊ฐ„ ๋ณ€๊ฒฝ O, ์ธ์› ์ถ”๊ฐ€ X") + @Test + void updateInvitationSuccess_03() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of()) + .startDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 0, 0))) + .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 30, 0))) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())).willReturn( + Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .endDate(dateAfterRequestDate) + .startTime(LocalTime.of(10, 0, 0)) + .endTime(LocalTime.of(10, 30, 0)) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + given(visitorRepository.countByInvitation(any(Invitation.class))).willReturn(3L); + given(visitorRepository.findVisitorsByInvitation(any(Invitation.class))).willReturn( + List.of( + Visitor.builder().build(), + Visitor.builder().build(), + Visitor.builder().build() + )); + given(reservationRepository.findReservationsByCommonPlaceIdAndStartDateAndEndDate( + anyLong(), + any(LocalDateTime.class), + any(LocalDateTime.class) + )).willReturn(List.of(Reservation.builder().build())); + + // When + ResponseEntity result = invitationService.updateInvitation(invitationId, dto, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ๊ฐ„ ๋ณ€๊ฒฝ O, ์ธ์› ์ถ”๊ฐ€ O") + @Test + void updateInvitationSuccess_04() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors( + List.of( + InvitationRequest.VisitorForUpdateDTO.builder().build(), + InvitationRequest.VisitorForUpdateDTO.builder().build(), + InvitationRequest.VisitorForUpdateDTO.builder().build() + ) + ) + .startDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 0, 0))) + .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 30, 0))) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); + given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(invitationRepository.findById(anyLong())).willReturn( + Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .endDate(dateAfterRequestDate) + .startTime(LocalTime.of(10, 0, 0)) + .endTime(LocalTime.of(10, 30, 0)) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); + given(visitorRepository.countByInvitation(any(Invitation.class))).willReturn(3L); + given(visitorRepository.findVisitorsByInvitation(any(Invitation.class))).willReturn( + List.of( + Visitor.builder().build(), + Visitor.builder().build(), + Visitor.builder().build() + )); + given(reservationRepository.findReservationsByCommonPlaceIdAndStartDateAndEndDate( + anyLong(), + any(LocalDateTime.class), + any(LocalDateTime.class) + )).willReturn(List.of(Reservation.builder().build())); + + // When + ResponseEntity result = invitationService.updateInvitation(invitationId, dto, memberId); + + // Then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + private List createReservations() { return new ArrayList<>(List.of( new InvitationResponse.ReservationDTO( From 226cae7b5bececf62ceb772bc69af6951db33ad9 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Fri, 22 Sep 2023 15:15:17 +0900 Subject: [PATCH 031/111] =?UTF-8?q?=ED=99=88=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A0=95=EB=B3=B4=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat : ํ™ˆ ํ™”๋ฉด ์ •๋ณด ์‘๋‹ต Controller ๊ตฌํ˜„ (#42) HomeController HomeResponse MemberErrorCode ๊ตฌํ˜„ * Test : ํ™ˆ ํ™”๋ฉด ์ •๋ณด ์‘๋‹ต Controller Test ๊ตฌํ˜„ (#42) HomeController Test ๊ตฌํ˜„ * Feat : MemberRepository ๊ตฌํ˜„ (#42) ์‚ฌ์šฉ์ž Id๋ฅผ ํ†ตํ•ด ๋นŒ๋”ฉ ์ •๋ณด ์กฐํšŒ * Feat : MemberService ๊ตฌํ˜„ (#42) ์‚ฌ์šฉ์ž Id๋ฅผ ํ†ตํ•ด ๋นŒ๋”ฉ ์ •๋ณด ์กฐํšŒ ๋กœ์ง ๊ตฌํ˜„ * Test : MemberService Test ๊ตฌํ˜„ (#42) ์‚ฌ์šฉ์ž Id๋ฅผ ํ†ตํ•ด ๋นŒ๋”ฉ ์ •๋ณด ์กฐํšŒ ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ --------- Co-authored-by: jongy --- .../home/controller/HomeController.java | 29 ++++++++++ .../livable/server/home/dto/HomeResponse.java | 20 +++++++ .../server/member/domain/MemberErrorCode.java | 3 + .../member/repository/MemberRepository.java | 15 ++++- .../server/member/service/MemberService.java | 7 ++- .../home/controller/HomeControllerTest.java | 58 +++++++++++++++++++ .../member/service/MemberServiceTest.java | 51 +++++++++++++++- 7 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/livable/server/home/controller/HomeController.java create mode 100644 src/main/java/com/livable/server/home/dto/HomeResponse.java create mode 100644 src/test/java/com/livable/server/home/controller/HomeControllerTest.java diff --git a/src/main/java/com/livable/server/home/controller/HomeController.java b/src/main/java/com/livable/server/home/controller/HomeController.java new file mode 100644 index 00000000..0fb3734e --- /dev/null +++ b/src/main/java/com/livable/server/home/controller/HomeController.java @@ -0,0 +1,29 @@ +package com.livable.server.home.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; +import com.livable.server.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/home") +@RestController +public class HomeController { + + private final MemberService memberService; + + @GetMapping + public ResponseEntity> getHomeInfo() { + + Long memberId = 1L; // TODO: 2023-09-22 JWT Token์œผ๋กœ ๋Œ€์ฒด + BuildingInfoDto buildingInfoDto = memberService.getBuildingInfo(memberId); + + return ApiResponse.success(buildingInfoDto, HttpStatus.OK); + } + +} diff --git a/src/main/java/com/livable/server/home/dto/HomeResponse.java b/src/main/java/com/livable/server/home/dto/HomeResponse.java new file mode 100644 index 00000000..b9b213d0 --- /dev/null +++ b/src/main/java/com/livable/server/home/dto/HomeResponse.java @@ -0,0 +1,20 @@ +package com.livable.server.home.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class HomeResponse { + + @Getter + @AllArgsConstructor + public static class BuildingInfoDto { + + private Long buildingId; + private String buildingName; + private Boolean hasCafeteria; + } + +} diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java index 49b54e75..ae3e5690 100644 --- a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java +++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java @@ -12,6 +12,9 @@ public enum MemberErrorCode implements ErrorCode { MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ํšŒ์›์˜ ๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + private final HttpStatus httpStatus; private final String message; + } diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java index 4d8e7929..8b98b754 100644 --- a/src/main/java/com/livable/server/member/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -1,13 +1,12 @@ package com.livable.server.member.repository; import com.livable.server.entity.Member; -import com.livable.server.member.dto.MyPageProjection; +import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Optional; - public interface MemberRepository extends JpaRepository { @Query("SELECT new com.livable.server.member.dto.MyPageProjection(m.name, c.name, p.balance) " + @@ -16,4 +15,14 @@ public interface MemberRepository extends JpaRepository { "INNER JOIN Point p ON p.member.id = m.id " + "WHERE m.id = :memberId") Optional findMemberCompanyPointData(@Param("memberId") Long memberId); + + @Query("select b.id as buildingId, b.name as buildingName, b.hasCafeteria as hasCafeteria" + + " from Member m " + + " join Company c" + + " on m.company = c" + + " join fetch Building b " + + " on c.building = b" + + " where m.id = :memberId") + Optional findBuildingInfoByMemberId(@Param("memberId") Long memberId); + } diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java index 88426755..dd8d57bf 100644 --- a/src/main/java/com/livable/server/member/service/MemberService.java +++ b/src/main/java/com/livable/server/member/service/MemberService.java @@ -7,7 +7,6 @@ import com.livable.server.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; - import java.util.Optional; @RequiredArgsConstructor @@ -24,4 +23,10 @@ public MemberResponse.MyPageDTO getMyPageData(Long memberId) { return MemberResponse.MyPageDTO.from(myPageProjection); } + + public BuildingInfoDto getBuildingInfo(Long memberId) { + return memberRepository.findBuildingInfoByMemberId(memberId) + .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); + } + } diff --git a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java new file mode 100644 index 00000000..2f0d2eba --- /dev/null +++ b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java @@ -0,0 +1,58 @@ +package com.livable.server.home.controller; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.service.MemberService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(HomeController.class) +class HomeControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + private MemberService memberService; + + @DisplayName("SUCCESS : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void getHomeInfoSuccess() throws Exception { + // given + Long memberId = 1L; + given(memberService.getBuildingInfo(memberId)) + .willReturn(new BuildingInfoDto(1L, "ํ…Œ๋ผ ํƒ€์›Œ", true)); + + // when & then + mockMvc.perform(get("/api/home")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data['buildingId']").value(1)) + .andExpect(jsonPath("$.data['buildingName']").value("ํ…Œ๋ผ ํƒ€์›Œ")) + .andExpect(jsonPath("$.data['hasCafeteria']").value(true)); + } + + @DisplayName("FAILED : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ - ์กฐํšŒ ์‹คํŒจ") + @Test + void getHomeInfoFailed() throws Exception { + // given + given(memberService.getBuildingInfo(anyLong())) + .willThrow(new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); + + // when & then + mockMvc.perform(get("/api/home")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(MemberErrorCode.BUILDING_INFO_NOT_EXIST.getMessage())); + } + +} diff --git a/src/test/java/com/livable/server/member/service/MemberServiceTest.java b/src/test/java/com/livable/server/member/service/MemberServiceTest.java index c73e7a0c..d39648d5 100644 --- a/src/test/java/com/livable/server/member/service/MemberServiceTest.java +++ b/src/test/java/com/livable/server/member/service/MemberServiceTest.java @@ -1,9 +1,17 @@ package com.livable.server.member.service; +import static com.livable.server.home.dto.HomeResponse.BuildingInfoDto; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; import com.livable.server.member.repository.MemberRepository; +import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -15,8 +23,6 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; - @ExtendWith(MockitoExtension.class) class MemberServiceTest { @@ -70,5 +76,46 @@ void failure_Test_InvalidMember() { Assertions.assertThrows(GlobalRuntimeException.class, () -> memberService.getMyPageData(memberId)); } + + @DisplayName("Success - ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต") + @Test + void getHomeInfoSuccess() { + // given + Long memberId = 1L; + Long buildingId = 1L; + String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; + Boolean hasCafeteria = true; + + given(memberRepository.findBuildingInfoByMemberId(memberId)) + .willReturn(Optional.of(new BuildingInfoDto(buildingId, buildingName, hasCafeteria)) + ); + + // when + BuildingInfoDto actual = memberService.getBuildingInfo(memberId); + + // then + assertAll( + () -> assertEquals(buildingId, actual.getBuildingId()), + () -> assertEquals(buildingName, actual.getBuildingName()), + () -> assertEquals(hasCafeteria, actual.getHasCafeteria()) + ); + + } + + @DisplayName("FAILED : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต - ์œ ํšจํ•˜์ง€ ์•Š์€ ์ •๋ณด") + @Test + void getHomeInfoFailed() { + // given + Long memberId = 1L; + + // when + Mockito.when(memberRepository.findBuildingInfoByMemberId(anyLong())) + .thenReturn(Optional.empty()); + + // then + assertThrows(GlobalRuntimeException.class, () -> + memberService.getBuildingInfo(memberId)); + } } + } From fd5067c25eabc7217654521812799d6c52aa8b21 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 22 Sep 2023 16:12:02 +0900 Subject: [PATCH 032/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#70)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ์ดˆ๋Œ€ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก ์‘๋‹ต DTO ๋ฆฌํŽ™ํ† ๋ง (#58) - ๋‚ ์งœ์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ์ „๋‹ฌํ•˜์ง€ ์•Š๋„๋ก ๊ธฐํš์ด ๋ณ€๊ฒฝ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. * Refactor: ์ดˆ๋Œ€ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต Service ๋ฆฌํŽ™ํ† ๋ง (#58) - ์—ฐ์†๋œ ์‹œ๊ฐ„์„ ํ™•์ธํ•˜๊ณ , ์‹œ๊ฐ„์„ ํ•ฉ์น˜๋Š” ๊ธฐ๋Šฅ์ด ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. * Refactor: ์ดˆ๋Œ€ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต Repository ์ฟผ๋ฆฌ ์ˆ˜์ • (#58) - date, time์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์„œ ์ œ๊ฑฐ - ์ค‘๋ณต ๊ฐ’์„ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด distinct ์ถ”๊ฐ€ * Refactor: ์ดˆ๋Œ€ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต Service ํ…Œ์ŠคํŠธ ์ˆ˜์ • (#58) * Refactor: ์ดˆ๋Œ€์žฅ ์ฃผ์ธ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ ๋ฆฌํŽ™ํ† ๋ง (#58) * Refactor: ์ดˆ๋Œ€์žฅ Service ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฆฌํŽ™ํ† ๋ง (#58) --- .../invitation/dto/InvitationResponse.java | 18 +--- .../invitation/service/InvitationService.java | 42 ++------- .../ReservationQueryRepositoryImpl.java | 8 +- .../service/InvitationServiceTest.java | 94 ++++++++----------- 4 files changed, 52 insertions(+), 110 deletions(-) diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java index 5bfb5cb1..4d55184b 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java @@ -37,9 +37,6 @@ public static OfficeDTO from(Office office) { public static class CommonPlaceDTO { private Long commonPlaceId; private String commonPlaceName; - private LocalDate date; - private LocalTime startTime; - private LocalTime endTime; public static CommonPlaceDTO from(ReservationDTO reservationDTO) { return new CommonPlaceDTO( @@ -48,10 +45,7 @@ public static CommonPlaceDTO from(ReservationDTO reservationDTO) { reservationDTO.getCommonPlaceName(), reservationDTO.getCommonPlaceFloor(), reservationDTO.getCommonPlaceRoomNumber() - ), - reservationDTO.getReservationDate(), - reservationDTO.getReservationStartTime(), - reservationDTO.getReservationEndTime() + ) ); } } @@ -67,25 +61,17 @@ public static class ReservationDTO { private String commonPlaceFloor; private String commonPlaceRoomNumber; private String commonPlaceName; - private LocalDate reservationDate; - private LocalTime reservationStartTime; - private LocalTime reservationEndTime; public ReservationDTO( Long commonPlaceId, String commonPlaceFloor, String commonPlaceRoomNumber, - String commonPlaceName, - LocalDate reservationDate, - LocalTime reservationStartTime + String commonPlaceName ) { this.commonPlaceId = commonPlaceId; this.commonPlaceFloor = commonPlaceFloor; this.commonPlaceRoomNumber = commonPlaceRoomNumber; this.commonPlaceName = commonPlaceName; - this.reservationDate = reservationDate; - this.reservationStartTime = reservationStartTime; - this.reservationEndTime = reservationStartTime.plusMinutes(30); } } diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 8a007c1b..c65dfc38 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -28,7 +28,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -56,20 +55,13 @@ public VisitationResponse.InvitationTimeDto findInvitationTime(Long visitorId) { @Transactional(readOnly = true) public ResponseEntity> getAvailablePlaces(Long memberId) { - // 1. memberId๊ฐ€ ์†ํ•œ companyId๋ฅผ ๊ฐ€์ ธ์˜ด Long companyId = getCompanyIdByMemberId(memberId); - // 2. company ์— ์†ํ•ด์žˆ๋Š” ์‚ฌ๋ฌด์‹ค ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด List officeEntities = officeRepository.findAllByCompanyId(companyId); - // 3. company ๊ฐ€ ์˜ˆ์•ฝํ•œ ๊ณต์šฉ ๊ณต๊ฐ„ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด List reservations = reservationRepository .findReservationsByCompanyId(companyId); - // 4. ReservationDTO ๋ฅผ ์—ฐ์†๋œ ์‹œ๊ฐ„์€ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” ์ž‘์—…์„ ์ง„ํ–‰ - combineConsecutiveReservation(reservations); - - // 5. DTO ๋ณ€ํ™˜ ์ž‘์—… List offices = officeEntities.stream() .map(InvitationResponse.OfficeDTO::from).collect(Collectors.toList()); @@ -84,32 +76,6 @@ public ResponseEntity> getAvailab return ApiResponse.success(responseBody, HttpStatus.OK); } - private void combineConsecutiveReservation(List reservations) { - Iterator reservationsIterator = reservations.iterator(); - InvitationResponse.ReservationDTO beforeReservation = null; - while (reservationsIterator.hasNext()) { - InvitationResponse.ReservationDTO currentReservation = reservationsIterator.next(); - - if (isNotCombineTarget(beforeReservation, currentReservation)) { - beforeReservation = currentReservation; - continue; - } - beforeReservation.setReservationEndTime(currentReservation.getReservationEndTime()); - reservationsIterator.remove(); - } - } - - private boolean isNotCombineTarget( - InvitationResponse.ReservationDTO before, - InvitationResponse.ReservationDTO current - ) { - // null ์ด๊ฑฐ๋‚˜ commonPlaceId๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ๋‚ ์งœ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ์—ฐ์†๋œ ์‹œ๊ฐ„์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋Š” ์‹œ๊ฐ„์„ ํ•ฉ์น˜๋Š” ๋ชฉํ‘œ๊ฐ€ ์•„๋‹˜ - return before == null - || !before.getCommonPlaceId().equals(current.getCommonPlaceId()) - || !before.getReservationDate().equals(current.getReservationDate()) - || !before.getReservationEndTime().equals(current.getReservationStartTime()); - } - private Long getCompanyIdByMemberId(Long memberId) { Member member = checkExistMemberById(memberId); @@ -128,7 +94,6 @@ public ResponseEntity createInvitation(InvitationRequest.CreateDTO dto, Long return ApiResponse.success(HttpStatus.CREATED); } - /* ๋ฉด์ ‘์˜ ๊ฒฝ์šฐ์—๋Š” 1๋ช…๋งŒ ์ดˆ๋Œ€ ๊ฐ€๋Šฅ */ private void checkInterviewVisitorCount(InvitationRequest.CreateDTO dto) { if (dto.getPurpose().equals(InvitationPurpose.INTERVIEW.getValue()) && dto.getVisitors().size() > INTERVIEW_MAXIMUM_COUNT) { @@ -244,7 +209,12 @@ public ResponseEntity> getInvitation(Long } private void checkInvitationOwner(Long invitationId, Long memberId) { - if (invitationRepository.countByIdAndMemberId(invitationId, memberId).equals(0L)) { + + Optional invitationOptional = invitationRepository.findById(invitationId); + Invitation invitation = invitationOptional + .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST)); + + if (!invitation.getMember().getId().equals(memberId)) { throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER); } } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index baf0312b..6e330a98 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -24,13 +24,11 @@ public class ReservationQueryRepositoryImpl implements ReservationQueryRepositor @Override public List findReservationsByCompanyId(Long companyId) { return queryFactory - .select(Projections.constructor(InvitationResponse.ReservationDTO.class, + .selectDistinct(Projections.constructor(InvitationResponse.ReservationDTO.class, commonPlace.id, commonPlace.floor, commonPlace.roomNumber, - commonPlace.name, - reservation.date, - reservation.time + commonPlace.name )) .from(reservation) .innerJoin(reservation.commonPlace, commonPlace) @@ -43,7 +41,7 @@ public List findReservationsByCompanyId(Long .where(invitationReservationMap.reservation.id.eq(reservation.id)) ) ) - .orderBy(reservation.commonPlace.id.asc(), reservation.date.asc(), reservation.time.asc()) + .orderBy(reservation.commonPlace.id.asc()) .fetch(); } diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 636f9c15..87457d9a 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -7,7 +7,6 @@ import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.dto.InvitationResponse.AvailablePlacesDTO; -import com.livable.server.invitation.dto.InvitationResponse.CommonPlaceDTO; import com.livable.server.invitation.repository.InvitationRepository; import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.invitation.repository.OfficeRepository; @@ -122,11 +121,7 @@ void getAvailablePlacesSuccess_02() { // Then assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(data.getOffices().size()).isEqualTo(3); - assertThat(data.getCommonPlaces().size()).isEqualTo(3); - - CommonPlaceDTO combinedItem = data.getCommonPlaces().get(2); - assertThat(combinedItem.getStartTime()).isEqualTo(LocalTime.of(10, 30, 0)); - assertThat(combinedItem.getEndTime()).isEqualTo(LocalTime.of(11, 30, 0)); + assertThat(data.getCommonPlaces().size()).isEqualTo(2); } @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ฑ - ๋ฉด์ ‘ ์ดˆ๋Œ€ ์ธ์› 2๋ช…") @@ -400,9 +395,11 @@ void getInvitationFail_01() { // Given Long invitationId = 1L; Long memberId = 1L; + Member member = Member.builder().id(memberId + 1L).build(); + Invitation invitation = Invitation.builder().id(invitationId).member(member).build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(0L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(invitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); // When GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, @@ -418,9 +415,11 @@ void getInvitationFail_02() { // Given Long invitationId = 1L; Long memberId = 1L; + Member member = Member.builder().id(memberId).build(); + Invitation invitation = Invitation.builder().id(invitationId).member(member).build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(invitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); given(invitationRepository.findInvitationAndVisitorsByInvitationId(anyLong())) .willReturn(any(InvitationResponse.DetailDTO.class)); @@ -440,7 +439,6 @@ void deleteInvitationFail_01() { Long memberId = 1L; given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); given(invitationRepository.findById(anyLong())).willReturn(Optional.empty()); // When @@ -459,13 +457,14 @@ void deleteInvitationFail_02() { LocalDate dateBeforeRequestDate = requestDate.minusDays(1L); Long invitationId = 1L; Long memberId = 1L; + Member member = Member.builder().id(memberId).build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .startDate(dateBeforeRequestDate) + .member(member) .build() )); @@ -485,13 +484,14 @@ void deleteInvitationFail_03() { LocalDate dateAfterRequestDate = requestDate.plusDays(1L); Long invitationId = 1L; Long memberId = 1L; + Member member = Member.builder().id(memberId).build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .startDate(dateAfterRequestDate) + .member(member) .build() )); given(visitorRepository.findVisitorsByInvitation(any(Invitation.class))) @@ -512,14 +512,15 @@ void updateInvitationFail_01() { LocalDate dateBeforeRequestDate = requestDate.minusDays(1L); Long invitationId = 1L; Long memberId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder().build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .startDate(dateBeforeRequestDate) + .member(member) .build() )); @@ -540,16 +541,17 @@ void updateInvitationFail_02() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .startDate(dateAfterRequestDate) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())) @@ -572,18 +574,19 @@ void updateInvitationFail_03() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .purpose("๋ฉด์ ‘") .startDate(dateAfterRequestDate) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -605,18 +608,19 @@ void updateInvitationFail_04() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) .purpose("ํšŒ์˜") .startDate(dateAfterRequestDate) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -639,6 +643,7 @@ void updateInvitationSuccess_01() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors(List.of()) @@ -646,8 +651,7 @@ void updateInvitationSuccess_01() { .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 30, 0))) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) @@ -656,6 +660,7 @@ void updateInvitationSuccess_01() { .endDate(dateAfterRequestDate) .startTime(LocalTime.of(10, 0, 0)) .endTime(LocalTime.of(10, 30, 0)) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -677,6 +682,7 @@ void updateInvitationSuccess_02() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors( @@ -690,8 +696,7 @@ void updateInvitationSuccess_02() { .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(10, 30, 0))) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) @@ -700,6 +705,7 @@ void updateInvitationSuccess_02() { .endDate(dateAfterRequestDate) .startTime(LocalTime.of(10, 0, 0)) .endTime(LocalTime.of(10, 30, 0)) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -722,6 +728,7 @@ void updateInvitationSuccess_03() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors(List.of()) @@ -729,8 +736,7 @@ void updateInvitationSuccess_03() { .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 30, 0))) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())).willReturn( Optional.of(Invitation.builder() .id(invitationId) @@ -739,6 +745,7 @@ void updateInvitationSuccess_03() { .endDate(dateAfterRequestDate) .startTime(LocalTime.of(10, 0, 0)) .endTime(LocalTime.of(10, 30, 0)) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -771,6 +778,7 @@ void updateInvitationSuccess_04() { Long invitationId = 1L; Long memberId = 1L; Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() .commonPlaceId(commonPlaceId) .visitors( @@ -784,8 +792,7 @@ void updateInvitationSuccess_04() { .endDate(LocalDateTime.of(dateAfterRequestDate, LocalTime.of(11, 30, 0))) .build(); - given(memberRepository.findById(anyLong())).willReturn(Optional.of(Member.builder().id(memberId).build())); - given(invitationRepository.countByIdAndMemberId(anyLong(), anyLong())).willReturn(1L); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(invitationRepository.findById(anyLong())).willReturn( Optional.of(Invitation.builder() .id(invitationId) @@ -794,6 +801,7 @@ void updateInvitationSuccess_04() { .endDate(dateAfterRequestDate) .startTime(LocalTime.of(10, 0, 0)) .endTime(LocalTime.of(10, 30, 0)) + .member(member) .build() )); given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(dto.getCommonPlaceId()); @@ -823,33 +831,13 @@ private List createReservations() { 1L, "1", "101", - "๊ณต์šฉ A", - LocalDate.of(2023, 10, 29), - LocalTime.of(10, 0, 0) - ), - new InvitationResponse.ReservationDTO( - 1L, - "1", - "101", - "๊ณต์šฉ A", - LocalDate.of(2023, 10, 30), - LocalTime.of(10, 0, 0) - ), - new InvitationResponse.ReservationDTO( - 2L, - "2", - "201", - "๊ณต์šฉ B", - LocalDate.of(2023, 10, 30), - LocalTime.of(10, 30, 0) + "๊ณต์šฉ A" ), new InvitationResponse.ReservationDTO( 2L, "2", "201", - "๊ณต์šฉ B", - LocalDate.of(2023, 10, 30), - LocalTime.of(11, 0, 0) + "๊ณต์šฉ B" ) )); } From 5fb304da2bc2db9b4fc9d1e5fbdd4ce7059b14e7 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 22 Sep 2023 16:57:32 +0900 Subject: [PATCH 033/111] =?UTF-8?q?Merge=20=EA=B3=BC=EC=A0=95=EC=A4=91=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=EB=90=9C=20=EB=B6=80=EB=B6=84=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20=EC=99=84=EB=A3=8C=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Merge ์ถฉ๋Œ ํ•ด๊ฒฐ์—์„œ ๋ˆ„๋ฝ๋œ ์ฝ”๋“œ ์ถ”๊ฐ€ (#72) --- .../java/com/livable/server/member/domain/MemberErrorCode.java | 2 +- .../com/livable/server/member/repository/MemberRepository.java | 2 ++ .../java/com/livable/server/member/service/MemberService.java | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java index ae3e5690..a8ca8e11 100644 --- a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java +++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java @@ -10,7 +10,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum MemberErrorCode implements ErrorCode { - MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ํšŒ์›์˜ ๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java index 8b98b754..9a8b52fa 100644 --- a/src/main/java/com/livable/server/member/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -3,6 +3,8 @@ import com.livable.server.entity.Member; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import java.util.Optional; + +import com.livable.server.member.dto.MyPageProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java index dd8d57bf..c7b73089 100644 --- a/src/main/java/com/livable/server/member/service/MemberService.java +++ b/src/main/java/com/livable/server/member/service/MemberService.java @@ -1,6 +1,7 @@ package com.livable.server.member.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.home.dto.HomeResponse; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; @@ -24,7 +25,7 @@ public MemberResponse.MyPageDTO getMyPageData(Long memberId) { return MemberResponse.MyPageDTO.from(myPageProjection); } - public BuildingInfoDto getBuildingInfo(Long memberId) { + public HomeResponse.BuildingInfoDto getBuildingInfo(Long memberId) { return memberRepository.findBuildingInfoByMemberId(memberId) .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); } From 9153b1976dbcd5b7ea69bbe1616885196a9080b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Fri, 22 Sep 2023 16:57:52 +0900 Subject: [PATCH 034/111] =?UTF-8?q?=EC=98=A4=EC=A0=90=EC=99=84=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#62) * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์‘๋‹ต DTO ๊ตฌํ˜„ (#62) * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์‘๋‹ต ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#62) * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#62) * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#62) * Feat: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ €์žฅ DTO ๊ตฌํ˜„ (#62) * Test: ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#62) --- .../RestaurantReviewController.java | 8 ++ .../livable/server/review/dto/Projection.java | 30 +++++++ .../review/dto/RestaurantReviewResponse.java | 35 ++++++++ .../RestaurantReviewCustomRepository.java | 5 ++ .../RestaurantReviewCustomRepositoryImpl.java | 27 +++++- .../review/service/MyReviewService.java | 4 + .../service/RestaurantReviewService.java | 25 ++++++ .../RestaurantReviewControllerTest.java | 26 ++++++ .../service/RestaurantReviewServiceTest.java | 84 ++++++++++++++++++- 9 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/livable/server/review/dto/Projection.java diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java index 003db4d2..774de36e 100644 --- a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -42,4 +42,12 @@ public ResponseEntity> detail(@PathVariable Long reviewId) { + + RestaurantReviewResponse.DetailDTO detail = restaurantReviewService.getDetail(reviewId); + + return ApiResponse.success(detail, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/review/dto/Projection.java b/src/main/java/com/livable/server/review/dto/Projection.java new file mode 100644 index 00000000..84b52acf --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/Projection.java @@ -0,0 +1,30 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.Evaluation; +import lombok.*; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Projection { + + @Getter + @Builder + @AllArgsConstructor + static public class RestaurantReview { + + private String memberName; + + private Long restaurantId; + private String restaurantName; + + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private String reviewImg; + } +} diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java index 868d162b..28a58c34 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -4,6 +4,7 @@ import lombok.*; import java.time.LocalDateTime; +import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RestaurantReviewResponse { @@ -51,4 +52,38 @@ public static class ListForMenuDTO { private Long memberId; private String memberName; } + + @Getter + @Builder + public static class DetailDTO { + + private String memberName; + + private Long restaurantId; + private String restaurantName; + + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private List reviewImages; + + public static DetailDTO from(Projection.RestaurantReview restaurantReview, List reviewImages) { + return DetailDTO.builder() + .memberName(restaurantReview.getMemberName()) + .restaurantId(restaurantReview.getRestaurantId()) + .restaurantName(restaurantReview.getRestaurantName()) + .reviewCreatedAt(restaurantReview.getReviewCreatedAt()) + .reviewDescription(restaurantReview.getReviewDescription()) + .reviewTaste(restaurantReview.getReviewTaste()) + .reviewAmount(restaurantReview.getReviewAmount()) + .reviewService(restaurantReview.getReviewService()) + .reviewSpeed(restaurantReview.getReviewSpeed()) + .reviewImages(reviewImages) + .build(); + } + } } diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java index 7c45bc3a..18ff1269 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java @@ -1,12 +1,17 @@ package com.livable.server.review.repository; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.List; + public interface RestaurantReviewCustomRepository { Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable); Page findRestaurantReviewByMenuId(Long menuId, Pageable pageable); + + List findRestaurantReviewById(Long reviewId); } diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java index 5ad0f554..c3c4a27b 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java @@ -1,8 +1,8 @@ package com.livable.server.review.repository; import com.livable.server.entity.*; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; -import com.querydsl.core.QueryFactory; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; @@ -18,6 +18,7 @@ import static com.livable.server.entity.QRestaurant.restaurant; import static com.livable.server.entity.QRestaurantReview.restaurantReview; import static com.livable.server.entity.QReview.review; +import static com.livable.server.entity.QReviewImage.reviewImage; import static com.livable.server.entity.QReviewMenuMap.reviewMenuMap; @RequiredArgsConstructor @@ -108,4 +109,28 @@ public Page findRestaurantReviewByMenuI return new PageImpl<>(content, pageable, total); } + + @Override + public List findRestaurantReviewById(Long reviewId) { + return queryFactory + .select(Projections.constructor(Projection.RestaurantReview.class, + member.name, + restaurant.id, + restaurant.name, + review.createdAt, + review.description, + restaurantReview.taste, + restaurantReview.amount, + restaurantReview.service, + restaurantReview.speed, + reviewImage.url + )) + .from(review) + .innerJoin(restaurantReview).on(restaurantReview.id.eq(review.id)) + .innerJoin(member).on(member.id.eq(review.member.id)) + .innerJoin(restaurant).on(restaurant.id.eq(restaurantReview.restaurant.id)) + .leftJoin(reviewImage).on(reviewImage.review.id.eq(review.id)) + .where(review.id.eq(reviewId)) + .fetch(); + } } diff --git a/src/main/java/com/livable/server/review/service/MyReviewService.java b/src/main/java/com/livable/server/review/service/MyReviewService.java index c2e650e5..eaee08ef 100644 --- a/src/main/java/com/livable/server/review/service/MyReviewService.java +++ b/src/main/java/com/livable/server/review/service/MyReviewService.java @@ -6,6 +6,7 @@ import com.livable.server.review.repository.MyReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -15,6 +16,7 @@ public class MyReviewService { private final MyReviewRepository myReviewRepository; + @Transactional(readOnly = true) public MyReviewResponse.DetailDTO getMyRestaurantReview(Long reviewId, Long memberId) { List myReviewProjections @@ -23,6 +25,7 @@ public MyReviewResponse.DetailDTO getMyRestaurantReview(Long reviewId, Long memb return this.convertToDTO(myReviewProjections); } + @Transactional(readOnly = true) public MyReviewResponse.DetailDTO getMyCafeteriaReview(Long reviewId, Long memberId) { List myReviewProjections @@ -31,6 +34,7 @@ public MyReviewResponse.DetailDTO getMyCafeteriaReview(Long reviewId, Long membe return this.convertToDTO(myReviewProjections); } + @Transactional(readOnly = true) public MyReviewResponse.DetailDTO getMyLunchBoxReview(Long reviewId, Long memberId) { List myReviewProjections diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java index 3bad9261..4f9432c2 100644 --- a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -1,5 +1,9 @@ package com.livable.server.review.service; +import com.livable.server.core.exception.ErrorCode; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.review.domain.MyReviewErrorCode; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.repository.RestaurantReviewRepository; import lombok.RequiredArgsConstructor; @@ -8,6 +12,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Service public class RestaurantReviewService { @@ -23,4 +30,22 @@ public Page getAllList(Long buildingId, Pageab public Page getAllListForMenu(Long menuId, Pageable pageable) { return restaurantReviewRepository.findRestaurantReviewByMenuId(menuId, pageable); } + + @Transactional(readOnly = true) + public RestaurantReviewResponse.DetailDTO getDetail(Long reviewId) { + + List restaurantReviews + = restaurantReviewRepository.findRestaurantReviewById(reviewId); + + if (restaurantReviews.isEmpty()) { + throw new GlobalRuntimeException(MyReviewErrorCode.REVIEW_NOT_EXIST); + } + + Projection.RestaurantReview restaurantReview = restaurantReviews.get(0); + List reviewImages = restaurantReviews.stream() + .map(Projection.RestaurantReview::getReviewImg) + .collect(Collectors.toList()); + + return RestaurantReviewResponse.DetailDTO.from(restaurantReview, reviewImages); + } } diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index 7d18751b..898a326f 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -1,5 +1,6 @@ package com.livable.server.review.controller; +import com.livable.server.review.dto.MyReviewResponse; import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.service.RestaurantReviewService; import org.junit.jupiter.api.DisplayName; @@ -109,4 +110,29 @@ void success_Test() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); } } + + @Nested + @DisplayName("๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ์ •๋ณด ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class Detail { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/1"; + + RestaurantReviewResponse.DetailDTO detailDTO + = RestaurantReviewResponse.DetailDTO.builder().build(); + + Mockito.when(restaurantReviewService.getDetail(ArgumentMatchers.anyLong())) + .thenReturn(detailDTO); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java index e8f2b7fe..f6fcda53 100644 --- a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -1,5 +1,7 @@ package com.livable.server.review.service; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.repository.RestaurantReviewRepository; import org.junit.jupiter.api.Assertions; @@ -17,9 +19,11 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.util.ArrayList; import java.util.List; -import static com.livable.server.review.dto.RestaurantReviewResponse.*; +import static com.livable.server.review.dto.RestaurantReviewResponse.ListDTO; +import static com.livable.server.review.dto.RestaurantReviewResponse.ListForMenuDTO; @ExtendWith(MockitoExtension.class) class RestaurantReviewServiceTest { @@ -67,7 +71,7 @@ void success_Test() { // Then Assertions.assertAll( () -> Assertions.assertEquals(10, actual.getSize()), - () -> Assertions.assertEquals(1,actual.getTotalPages()) + () -> Assertions.assertEquals(1, actual.getTotalPages()) ); } } @@ -109,8 +113,82 @@ void success_Test() { // Then Assertions.assertAll( () -> Assertions.assertEquals(10, actual.getSize()), - () -> Assertions.assertEquals(1,actual.getTotalPages()) + () -> Assertions.assertEquals(1, actual.getTotalPages()) ); } } + + @Nested + @DisplayName("๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์ƒ์„ธ ์ •๋ณด ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class Detail { + + @DisplayName("์„ฑ๊ณต - ์‹ฑ๊ธ€ ์ด๋ฏธ์ง€") + @Test + void success_Test_SingleImage() { + // Given + Long reviewId = 1L; + + List mockResult = List.of( + Projection.RestaurantReview.builder() + .reviewImg("TestImages") + .reviewDescription("TestDescription") + .build() + ); + + Mockito.when(restaurantReviewRepository.findRestaurantReviewById(ArgumentMatchers.anyLong())) + .thenReturn(mockResult); + // When + RestaurantReviewResponse.DetailDTO actual = restaurantReviewService.getDetail(reviewId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals("TestDescription", actual.getReviewDescription()), + () -> Assertions.assertEquals(1, actual.getReviewImages().size()) + ); + } + + @DisplayName("์„ฑ๊ณต - ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€") + @Test + void success_Test_MultipleImage() { + // Given + Long reviewId = 1L; + + List mockResult = List.of( + Projection.RestaurantReview.builder() + .reviewImg("TestImage1") + .reviewDescription("TestDescription") + .build(), + Projection.RestaurantReview.builder() + .reviewImg("TestImage2") + .reviewDescription("TestDescription") + .build() + ); + + Mockito.when(restaurantReviewRepository.findRestaurantReviewById(ArgumentMatchers.anyLong())) + .thenReturn(mockResult); + // When + RestaurantReviewResponse.DetailDTO actual = restaurantReviewService.getDetail(reviewId); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals("TestDescription", actual.getReviewDescription()), + () -> Assertions.assertEquals(2, actual.getReviewImages().size()) + ); + } + + @DisplayName("์‹คํŒจ - DTO๋ณ€ํ™˜ ์˜ค๋ฅ˜") + @Test + void failure_Test_FailedConvertToDTO() { + // Given + Long reviewId = 1L; + + Mockito.when(restaurantReviewRepository.findRestaurantReviewById(ArgumentMatchers.anyLong())) + .thenReturn(new ArrayList<>()); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + restaurantReviewService.getDetail(reviewId)); + } + } } \ No newline at end of file From 6c1f1dc89d241861eba9a52e56411b5da6406200 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 22 Sep 2023 19:00:45 +0900 Subject: [PATCH 035/111] =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=ED=86=B5=EC=8B=A0=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Cors=20=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ ์„ ์œ„ํ•œ Cors ์„ค์ • (#51) --- .../com/livable/server/core/config/WebConfig.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/livable/server/core/config/WebConfig.java b/src/main/java/com/livable/server/core/config/WebConfig.java index bff3d525..e29b8e8d 100644 --- a/src/main/java/com/livable/server/core/config/WebConfig.java +++ b/src/main/java/com/livable/server/core/config/WebConfig.java @@ -3,6 +3,7 @@ import com.livable.server.core.util.StringToRestaurantCategoryConverter; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -12,4 +13,14 @@ public class WebConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToRestaurantCategoryConverter()); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedHeaders("*") + .allowedMethods("*") + .allowCredentials(true) + .exposedHeaders("Authorization") + .allowedOriginPatterns("*"); + } } From dc0d5aa8de7b9e886801bbc578d972bdcf6e56f2 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:24:06 +0900 Subject: [PATCH 036/111] =?UTF-8?q?=EC=98=88=EC=95=BD=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Typo: ์˜คํƒ€ ์ˆ˜์ • (#55) * Feat: StringToLocalDateConverter ๊ตฌํ˜„ (#65) * Feat: Reservation ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#65) findNotUsedReservationTime ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ * Test: Reservation.findNotUsedReservationTime ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#65) findNotUsedReservationTime ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ * Style: ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ (#65) VisitationController.validateQrCode ๋งค๊ฐœ๋ณ€์ˆ˜ ์ฝ”๋“œ๋ผ์ธ ๋„ˆ๋ฌด ๊ธธ์–ด์„œ ์ค„๋ฐ”๊ฟˆ ์ถ”๊ฐ€ํ•จ * Feat: Projection Dto ์ถ”๊ฐ€ (#65) AvailableReservationTimeProjection * Fix: PersistContext ๊ณต์œ ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ ํ•ด๊ฒฐ (#65) * Modify: ProjectionDto ์ˆ˜์ • (#65) * Feat: Projections List ๊ฐ์ฒด ๊ตฌํ˜„ (#65) * Feat: Dto ๊ฐ์ฒด ๊ตฌํ˜„ (#65) * Feat: ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#65) * Feat: ReservationController ๊ตฌํ˜„ (#65) * Feat: ReservationService ๋กœ์ง ๊ตฌํ˜„ (#65) * Test: ReservationService ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#65) * Test: ReservationController ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#65) * Chore: ๋ถˆํ•„์š”ํ•œ .gitkeep ์ œ๊ฑฐ (#65) --- .../livable/server/core/config/WebConfig.java | 2 + .../core/util/StringToLocalDateConverter.java | 15 ++ .../server/reservation/controller/.gitkeep | 0 .../controller/ReservationController.java | 30 ++++ .../AvailableReservationTimeProjection.java | 16 ++ .../AvailableReservationTimeProjections.java | 29 ++++ .../reservation/dto/ReservationResponse.java | 21 +++ .../ReservationQueryRepository.java | 6 + .../ReservationQueryRepositoryImpl.java | 24 +++ .../server/reservation/service/.gitkeep | 0 .../service/ReservationService.java | 49 ++++++ .../server/restaurant/controller/.gitkeep | 0 .../livable/server/restaurant/domain/.gitkeep | 0 .../livable/server/restaurant/dto/.gitkeep | 0 .../server/restaurant/repository/.gitkeep | 0 .../server/restaurant/service/.gitkeep | 0 .../controller/VisitationController.java | 4 +- ...ringToRestaurantCategoryConverterTest.java | 6 +- .../controller/ReservationControllerTest.java | 66 ++++++++ .../repository/ReservationRepositoryTest.java | 159 ++++++++++++++++++ .../service/ReservationServiceTest.java | 102 +++++++++++ 21 files changed, 525 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java delete mode 100644 src/main/java/com/livable/server/reservation/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/controller/ReservationController.java create mode 100644 src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjection.java create mode 100644 src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjections.java create mode 100644 src/main/java/com/livable/server/reservation/dto/ReservationResponse.java delete mode 100644 src/main/java/com/livable/server/reservation/service/.gitkeep create mode 100644 src/main/java/com/livable/server/reservation/service/ReservationService.java delete mode 100644 src/main/java/com/livable/server/restaurant/controller/.gitkeep delete mode 100644 src/main/java/com/livable/server/restaurant/domain/.gitkeep delete mode 100644 src/main/java/com/livable/server/restaurant/dto/.gitkeep delete mode 100644 src/main/java/com/livable/server/restaurant/repository/.gitkeep delete mode 100644 src/main/java/com/livable/server/restaurant/service/.gitkeep create mode 100644 src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java create mode 100644 src/test/java/com/livable/server/reservation/repository/ReservationRepositoryTest.java create mode 100644 src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java diff --git a/src/main/java/com/livable/server/core/config/WebConfig.java b/src/main/java/com/livable/server/core/config/WebConfig.java index e29b8e8d..6c4ac9b9 100644 --- a/src/main/java/com/livable/server/core/config/WebConfig.java +++ b/src/main/java/com/livable/server/core/config/WebConfig.java @@ -1,5 +1,6 @@ package com.livable.server.core.config; +import com.livable.server.core.util.StringToLocalDateConverter; import com.livable.server.core.util.StringToRestaurantCategoryConverter; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; @@ -12,6 +13,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToRestaurantCategoryConverter()); + registry.addConverter(new StringToLocalDateConverter()); } @Override diff --git a/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java new file mode 100644 index 00000000..028ebaf6 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java @@ -0,0 +1,15 @@ +package com.livable.server.core.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; + +import java.time.LocalDate; + +@Slf4j +public class StringToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(String source) { + return LocalDate.parse(source); + } +} diff --git a/src/main/java/com/livable/server/reservation/controller/.gitkeep b/src/main/java/com/livable/server/reservation/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/reservation/controller/ReservationController.java b/src/main/java/com/livable/server/reservation/controller/ReservationController.java new file mode 100644 index 00000000..ba7d6426 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/controller/ReservationController.java @@ -0,0 +1,30 @@ +package com.livable.server.reservation.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.reservation.dto.ReservationResponse; +import com.livable.server.reservation.service.ReservationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/reservation") +public class ReservationController { + + private final ReservationService reservationService; + + @GetMapping("/places/{commonPlaceId}") + public ResponseEntity> findAvailableTimes( + @PathVariable Long commonPlaceId, + @RequestParam("date") LocalDate localDate + ) { + List result = + reservationService.findAvailableReservationTimes(1L, commonPlaceId, localDate); + return ApiResponse.success(result, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjection.java b/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjection.java new file mode 100644 index 00000000..a0097259 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjection.java @@ -0,0 +1,16 @@ +package com.livable.server.reservation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDate; +import java.time.LocalTime; + +@Getter +@AllArgsConstructor +public class AvailableReservationTimeProjection { + + private LocalDate date; + private LocalTime time; +} diff --git a/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjections.java b/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjections.java new file mode 100644 index 00000000..212965a5 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/dto/AvailableReservationTimeProjections.java @@ -0,0 +1,29 @@ +package com.livable.server.reservation.dto; + +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class AvailableReservationTimeProjections { + + List projections; + + public List toDto() { + return projections.stream() + .collect(Collectors.groupingBy( + AvailableReservationTimeProjection::getDate, + Collectors.mapping(AvailableReservationTimeProjection::getTime, Collectors.toList()) + )) + .entrySet() + .stream() + .map(entry -> ReservationResponse.AvailableReservationTimePerDateDto + .builder() + .date(entry.getKey()) + .availableTimes(entry.getValue()) + .build() + ) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/livable/server/reservation/dto/ReservationResponse.java b/src/main/java/com/livable/server/reservation/dto/ReservationResponse.java new file mode 100644 index 00000000..387f275e --- /dev/null +++ b/src/main/java/com/livable/server/reservation/dto/ReservationResponse.java @@ -0,0 +1,21 @@ +package com.livable.server.reservation.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReservationResponse { + + @Getter + @Builder + public static class AvailableReservationTimePerDateDto { + private LocalDate date; + private List availableTimes; + } +} diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index 66309497..0b09aef2 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -2,7 +2,9 @@ import com.livable.server.entity.Reservation; import com.livable.server.invitation.dto.InvitationResponse; +import com.livable.server.reservation.dto.AvailableReservationTimeProjection; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -10,4 +12,8 @@ public interface ReservationQueryRepository { List findReservationsByCompanyId(Long companyId); List findReservationsByCommonPlaceIdAndStartDateAndEndDate( Long commonPlaceId, LocalDateTime startDateTime, LocalDateTime endDateTime); + + List findNotUsedReservationTime( + Long companyId, Long commonPlaceId, LocalDate date + ); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index 6e330a98..b894c0a3 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -2,12 +2,14 @@ import com.livable.server.entity.Reservation; import com.livable.server.invitation.dto.InvitationResponse; +import com.livable.server.reservation.dto.AvailableReservationTimeProjection; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -72,4 +74,26 @@ public List findReservationsByCommonPlaceIdAndStartDateAndEndDate( .orderBy(reservation.date.asc(), reservation.time.asc()) .fetch(); } + + @Override + public List findNotUsedReservationTime( + Long companyId, Long commonPlaceId, LocalDate date + ) { + return queryFactory + .select(Projections.constructor(AvailableReservationTimeProjection.class, + reservation.date, + reservation.time + ) + ) + .from(reservation) + .where(reservation.id.notIn( + JPAExpressions.select(invitationReservationMap.reservation.id) + .from(invitationReservationMap) + ), + reservation.company.id.eq(companyId), + reservation.date.eq(date), + reservation.commonPlace.id.eq(commonPlaceId) + ) + .fetch(); + } } diff --git a/src/main/java/com/livable/server/reservation/service/.gitkeep b/src/main/java/com/livable/server/reservation/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/reservation/service/ReservationService.java b/src/main/java/com/livable/server/reservation/service/ReservationService.java new file mode 100644 index 00000000..6f115b19 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/service/ReservationService.java @@ -0,0 +1,49 @@ +package com.livable.server.reservation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Member; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.dto.AvailableReservationTimeProjection; +import com.livable.server.reservation.dto.AvailableReservationTimeProjections; +import com.livable.server.reservation.dto.ReservationResponse; +import com.livable.server.reservation.repository.ReservationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class ReservationService { + + private final ReservationRepository reservationRepository; + private final MemberRepository memberRepository; + + public List findAvailableReservationTimes( + Long memberId, + Long commonPlaceId, + LocalDate date + ) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)); + + AvailableReservationTimeProjections availableReservationTimeProjections = + getAvailableReservationTimeProjections(member.getCompany().getId(), commonPlaceId, date); + + return availableReservationTimeProjections.toDto(); + } + + private AvailableReservationTimeProjections getAvailableReservationTimeProjections( + Long companyId, Long commonPlaceId, LocalDate date + ) { + List timeProjections = + reservationRepository.findNotUsedReservationTime(companyId, commonPlaceId, date); + + return new AvailableReservationTimeProjections(timeProjections); + } +} diff --git a/src/main/java/com/livable/server/restaurant/controller/.gitkeep b/src/main/java/com/livable/server/restaurant/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/restaurant/domain/.gitkeep b/src/main/java/com/livable/server/restaurant/domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/restaurant/dto/.gitkeep b/src/main/java/com/livable/server/restaurant/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/restaurant/repository/.gitkeep b/src/main/java/com/livable/server/restaurant/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/restaurant/service/.gitkeep b/src/main/java/com/livable/server/restaurant/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index beb72ff6..dc023a35 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -37,7 +37,9 @@ public ResponseEntity> createQrCode() { } @PostMapping("/qr") - public ResponseEntity> validateQrCode(@RequestBody VisitationRequest.ValidateQrCodeDto validateQrCodeDto) { + public ResponseEntity> validateQrCode( + @RequestBody @Valid VisitationRequest.ValidateQrCodeDto validateQrCodeDto + ) { visitationFacadeService.validateQrCode(validateQrCodeDto.getQr()); diff --git a/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java b/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java index 0a627c5d..93088fd9 100644 --- a/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java +++ b/src/test/java/com/livable/server/core/util/StringToRestaurantCategoryConverterTest.java @@ -16,7 +16,7 @@ class StringToRestaurantCategoryConverterTest { @DisplayName("StringToRestaurantConverter ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_1") @CsvSource({"RESTAURANT", "restaurant", "Restaurant"}) - @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + @ParameterizedTest(name = "[{index}] ์ž…๋ ฅ๋ฌธ์ž: {0}") void convertSuccessTest_RESTAURANT(String symbol) { // Given @@ -29,7 +29,7 @@ void convertSuccessTest_RESTAURANT(String symbol) { @DisplayName("StringToRestaurantConverter ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_2") @CsvSource({"CAFE", "cafe", "Cafe"}) - @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + @ParameterizedTest(name = "[{index}] ์ž…๋ ฅ๋ฌธ์ž: {0}") void convertSuccessTest_CAFE(String symbol) { // Given @@ -42,7 +42,7 @@ void convertSuccessTest_CAFE(String symbol) { @DisplayName("StringToRestaurantConverter ์‹คํŒจ ํ…Œ์ŠคํŠธ") @CsvSource({"CAFe", "caFe", "CAfe", "123", "zz", "restAuRant"}) - @ParameterizedTest(name = "[{index} ์ž…๋ ฅ๋ฌธ์ž: {0}") + @ParameterizedTest(name = "[{index}] ์ž…๋ ฅ๋ฌธ์ž: {0}") void convertFailTest(String symbol) { // Given diff --git a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java new file mode 100644 index 00000000..edbdae82 --- /dev/null +++ b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java @@ -0,0 +1,66 @@ +package com.livable.server.reservation.controller; + +import com.livable.server.reservation.dto.ReservationResponse; +import com.livable.server.reservation.service.ReservationService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ReservationController.class) +class ReservationControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + ReservationService reservationService; + + @DisplayName( + "[GET][/api/reservation/places/{commonPlaceId}?date={yyyy-MM-dd}] - ํŠน์ • ํšŒ์˜์‹ค์˜ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์‹œ๊ฐ„ ์‘๋‹ต(์˜ˆ์•ฝํ•ด๋‘” ์‹œ๊ฐ„)" + ) + @Test + void findAvailableTimesSuccessTest() throws Exception { + // given + List result = IntStream.range(1, 10) + .mapToObj(idx -> ReservationResponse.AvailableReservationTimePerDateDto.builder() + .date(LocalDate.now()) + .availableTimes(new ArrayList<>(List.of(LocalTime.now()))) + .build() + ) + .collect(Collectors.toList()); + + given(reservationService.findAvailableReservationTimes(anyLong(), anyLong(), any(LocalDate.class))) + .willReturn(result); + + // when + ResultActions resultActions = mockMvc.perform( + get("/api/reservation/places/1") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("date", "2023-09-22") + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data[0].date").isString()) + .andExpect(jsonPath("$.data[0].availableTimes").isArray()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/reservation/repository/ReservationRepositoryTest.java b/src/test/java/com/livable/server/reservation/repository/ReservationRepositoryTest.java new file mode 100644 index 00000000..56a51248 --- /dev/null +++ b/src/test/java/com/livable/server/reservation/repository/ReservationRepositoryTest.java @@ -0,0 +1,159 @@ +package com.livable.server.reservation.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.*; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.dto.AvailableReservationTimeProjection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DataJpaTest +@Import(QueryDslConfig.class) +class ReservationRepositoryTest { + + public static final LocalDate START_DATE = LocalDate.now(); + public static final LocalTime START_TIME = LocalTime.of(1, 10); + public static final LocalTime END_TIME = LocalTime.of(1, 20); + public static final LocalDate END_DATE = LocalDate.now(); + + @Autowired + ReservationRepository reservationRepository; + + @Autowired + EntityManager entityManager; + + @Autowired + MemberRepository memberRepository; + + @BeforeEach + void dateInit() { + Building building = Building.builder() + .name("63๋นŒ๋”ฉ") + .scale("์ง€ํ•˜ 3์ธต, ์ง€์ƒ 63์ธต") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10๋ถ„๋‹น 1000์›") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 61๊ธธ 7, 392") + .subwayStation("์„์ดŒ์—ญ") + .build(); + + entityManager.persist(building); + + Company company = Company.builder() + .name("ํŒจ์ŠคํŠธ์บ ํผ์Šค") + .building(building) + .build(); + + entityManager.persist(company); + + Member member = Member.builder() + .company(company) + .contact("01012345678") + .name("๊น€ํ›ˆ์„ญ") + .email("test@naver.com") + .employeeNumber("9q0mavfdmmpoaskp") + .profileImageUrl("./profileImageUrl") + .password("1234") + .role(Role.USER) + .build(); + + entityManager.persist(member); + + Invitation invitation = Invitation.builder() + .member(member) + .endDate(END_DATE) + .startDate(START_DATE) + .startTime(START_TIME) + .endTime(END_TIME) + .description("์•Œ์•„์„œ ์™€") + .purpose("INTERVIEW") + .officeName("ํŒจ์ŠคํŠธ์บ ํผ์Šค ์‚ฌ๋ฌด์‹ค") + .build(); + + entityManager.persist(invitation); + + List commonPlaceList = IntStream.range(0, 2) + .mapToObj( + idx -> CommonPlace.builder() + .name("commonPlace" + idx) + .floor("floor" + idx) + .roomNumber("roomNumber" + idx) + .building(building) + .build() + ) + .collect(Collectors.toList()); + commonPlaceList.forEach(entityManager::persist); + + List reservationList = IntStream.range(1, 10) + .mapToObj( + idx -> Reservation.builder() + .date(LocalDate.now()) + .time(LocalTime.of(10, 0, 0).plusMinutes(idx * 30L)) + .commonPlace(commonPlaceList.get(idx % 2)) + .company(company) + .build() + ) + .collect(Collectors.toList()); + + reservationList.forEach(entityManager::persist); + + List invitationReservationMapList = IntStream.range(0, 3) + .mapToObj(idx -> { + return InvitationReservationMap.builder() + .reservation(reservationList.get(idx)) + .invitation(invitation) + .build(); + }) + .collect(Collectors.toList()); + + invitationReservationMapList.forEach(entityManager::persist); + } + + @DisplayName("ReservationRepository.findNotUsedReservationTime ์ฟผ๋ฆฌ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void test() { + + List expectedResult = IntStream.range(4, 10) + .filter(idx -> idx % 2 == 0) + .mapToObj(idx -> + new AvailableReservationTimeProjection( + LocalDate.now(), LocalTime.of(10, 0, 0).plusMinutes(30L * idx) + ) + ) + .collect(Collectors.toList()); + + + Member member = entityManager.createQuery("select m from Member m", Member.class) + .getResultList() + .get(0); + + CommonPlace commonPlace = entityManager.createQuery("select cp from CommonPlace cp", CommonPlace.class) + .getResultList() + .get(0); + List notUsedReservationTime = + reservationRepository.findNotUsedReservationTime( + member.getCompany().getId(), commonPlace.getId(), LocalDate.now() + ); + + assertThat(expectedResult).usingRecursiveComparison().isEqualTo(notUsedReservationTime); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java new file mode 100644 index 00000000..1b3e7118 --- /dev/null +++ b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java @@ -0,0 +1,102 @@ +package com.livable.server.reservation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Company; +import com.livable.server.entity.Member; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.dto.AvailableReservationTimeProjection; +import com.livable.server.reservation.dto.AvailableReservationTimeProjections; +import com.livable.server.reservation.dto.ReservationResponse; +import com.livable.server.reservation.repository.ReservationRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class ReservationServiceTest { + + @InjectMocks + ReservationService reservationService; + + @Mock + ReservationRepository reservationRepository; + + @Mock + MemberRepository memberRepository; + + @DisplayName("ReservationService.findAvailableReservationTimes ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findAvailableReservationTimesSuccessTest() { + + // given + Company company = Company.builder() + .id(1L) + .build(); + + Member member = Member.builder() + .id(1L) + .company(company) + .build(); + + List queryResult = IntStream.range(1, 5) + .mapToObj(idx -> new AvailableReservationTimeProjection( + LocalDate.now(), LocalTime.of(10, 0, 0).plusMinutes(idx * 30) + ) + ) + .collect(Collectors.toList()); + + AvailableReservationTimeProjections projections = new AvailableReservationTimeProjections(queryResult); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(reservationRepository.findNotUsedReservationTime(anyLong(), anyLong(), any(LocalDate.class))) + .willReturn(queryResult); + + // when + List result = + reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now()); + + // then + then(memberRepository).should(times(1)).findById(anyLong()); + then(reservationRepository).should(times(1)) + .findNotUsedReservationTime(anyLong(), anyLong(), any(LocalDate.class)); + + assertThat(result).usingRecursiveComparison().isEqualTo(projections.toDto()); + } + + @DisplayName("ReservationService.findAvailableReservationTimes ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findAvailableReservationTimesFailTest() { + // given + + given(memberRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> + reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now()) + ); + + // then + then(memberRepository).should(times(1)).findById(anyLong()); + + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_EXIST); + } +} \ No newline at end of file From 2bb9f19e175c2a213c10fca4938f72835a248321 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sat, 23 Sep 2023 15:36:29 +0900 Subject: [PATCH 037/111] =?UTF-8?q?JWT=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: jwt ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ (#76) * Feat: Jwt ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ, Enum ์ƒ์„ฑ (#76) * Refactor: ํ† ํฐ ์‹œํฌ๋ฆฟ ํ‚ค final ๋ณ€๊ฒฝ ํ›„ ์ƒ์„ฑ์ž ์ฃผ์ž… ๋ณ€๊ฒฝ (#76) * Refactor: ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ๋กœ์ง ์ˆ˜์ • (#76) * Test: ํ† ํฐ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#76) --- build.gradle | 1 + .../livable/server/core/util/ActorType.java | 6 ++ .../server/core/util/JwtTokenProvider.java | 56 +++++++++++++++++++ .../core/util/JwtTokenProviderTest.java | 48 ++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/main/java/com/livable/server/core/util/ActorType.java create mode 100644 src/main/java/com/livable/server/core/util/JwtTokenProvider.java create mode 100644 src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java diff --git a/build.gradle b/build.gradle index a35c6a2d..3a379762 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation 'com.google.zxing:core:3.5.2' implementation 'com.google.zxing:javase:3.5.2' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'io.jsonwebtoken:jjwt:0.9.1' } tasks.named('test') { diff --git a/src/main/java/com/livable/server/core/util/ActorType.java b/src/main/java/com/livable/server/core/util/ActorType.java new file mode 100644 index 00000000..bab77533 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/ActorType.java @@ -0,0 +1,6 @@ +package com.livable.server.core.util; + +public enum ActorType { + MEMBER, VISITOR; +} + diff --git a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java new file mode 100644 index 00000000..d4a5858a --- /dev/null +++ b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java @@ -0,0 +1,56 @@ +package com.livable.server.core.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final String secretKey; + + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { + this.secretKey = secretKey; + } + + /** + * Actor ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * @param actorType: ์—‘ํ„ฐ์˜ ์ข…๋ฅ˜ + * @param actorId: ์—‘ํ„ฐ์˜ ์‹๋ณ„๊ฐ’ + * @param expireDate: ํ† ํฐ์˜ ๋งŒ๋ฃŒ์ผ + * @return ์ž…๋ ฅ๋œ ์ •๋ณด๋กœ ๋งŒ๋“  ํ† ํฐ์„ ๋ฐ˜ํ™˜ + */ + public String createActorToken(ActorType actorType, Long actorId, Date expireDate) { + + Claims claims = Jwts.claims(); + claims.put("actorId", actorId); + claims.put("actorType", actorType); + + return Jwts.builder() + .setClaims(claims) + .setExpiration(expireDate) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + /** + * ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ + * @param token: JWT ํ† ํฐ ์ž…๋ ฅ + * @return ์ž…๋ ฅํ•œ ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด true, ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด false + */ + public boolean isValidateToken(String token) { + try { + Jws claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + + return claimsJws.getBody().getExpiration().after(new Date()); + } catch (Exception e) { + return false; + } + } + +} diff --git a/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java b/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java new file mode 100644 index 00000000..6fa0f8e0 --- /dev/null +++ b/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java @@ -0,0 +1,48 @@ +package com.livable.server.core.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +class JwtTokenProviderTest { + + private JwtTokenProvider tokenProvider; + + @BeforeEach + void init() { + String testSecretKey = "secretKey"; + tokenProvider = new JwtTokenProvider(testSecretKey); + } + + @DisplayName("[์„ฑ๊ณต] Member ํ† ํฐ ๊ฒ€์ฆ") + @Test + void validateMemberToken() { + // Given + Date expireDate = new Date(new Date().getTime() + 1000000); + String memberToken = tokenProvider.createActorToken(ActorType.MEMBER, 1L, expireDate); + + // When + boolean isValidToken = tokenProvider.isValidateToken(memberToken); + + // Then + assertThat(isValidToken).isTrue(); + } + + @DisplayName("[์„ฑ๊ณต] Visitor ํ† ํฐ ๊ฒ€์ฆ") + @Test + void validateVisitorToken() { + // Given + Date expireDate = new Date(new Date().getTime() + 1000000); + String visitorToken = tokenProvider.createActorToken(ActorType.VISITOR, 1L, expireDate); + + // When + boolean isValidToken = tokenProvider.isValidateToken(visitorToken); + + // Then + assertThat(isValidToken).isTrue(); + } +} \ No newline at end of file From 70e2b8ac2a8a8ae6f7a389bf5deba6011bda542a Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sat, 23 Sep 2023 16:00:56 +0900 Subject: [PATCH 038/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก ์‘๋‹ต ํ˜•ํƒœ Controller ๋ณ€๊ฒฝ (#79) * Refactor: ์ดˆ๋Œ€์žฅ ๋ชฉ๋ก ์‘๋‹ต ํ˜•ํƒœ ๋ณ€๊ฒฝ Service ๋กœ์ง ์ถ”๊ฐ€ (#79) --- .../controller/InvitationController.java | 4 ++- .../invitation/service/InvitationService.java | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index 82d3857f..d52ac586 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -9,7 +9,9 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.time.LocalDate; import java.util.List; +import java.util.Map; @RequiredArgsConstructor @RequestMapping("/api/invitation") @@ -36,7 +38,7 @@ public ResponseEntity createInvitation( } @GetMapping - public ResponseEntity>> getInvitations() { + public ResponseEntity>>> getInvitations() { Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index c65dfc38..f1941e89 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -28,8 +28,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @RequiredArgsConstructor @@ -190,11 +189,30 @@ private void createInvitationReservationMap(List reservations, Invi } @Transactional(readOnly = true) - public ResponseEntity>> getInvitations(Long memberId) { + public ResponseEntity>>> getInvitations(Long memberId) { checkExistMemberById(memberId); List invitationDTOs = invitationRepository.findInvitationsByMemberId(memberId); + Map> responseBody = getInvitationsGroupByLocalDate(invitationDTOs); - return ApiResponse.success(invitationDTOs, HttpStatus.OK); + return ApiResponse.success(responseBody, HttpStatus.OK); + } + + private Map> getInvitationsGroupByLocalDate( + List invitationDTOS) { + + Map> invitationsGroupByLocalDate + = new TreeMap<>(LocalDate::compareTo); + + for (InvitationResponse.ListDTO invitation : invitationDTOS) { + LocalDate date = invitation.getStartDate(); + + if (!invitationsGroupByLocalDate.containsKey(date)) { + invitationsGroupByLocalDate.put(date, new ArrayList<>()); + } + invitationsGroupByLocalDate.get(date).add(invitation); + } + + return invitationsGroupByLocalDate; } @Transactional(readOnly = true) From bbb9b2326bd974e3985c543a28f399cc0d4eee35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Sun, 24 Sep 2023 22:57:44 +0900 Subject: [PATCH 039/111] =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=AA=A9?= =?UTF-8?q?=ED=91=9C=20=EB=8B=AC=EC=84=B1=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Style: ๋”๋ฏธํŒŒ์ผ ์‚ญ์ œ (#75) * Feat: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#75) * Feat: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต DTO ๊ตฌํ˜„ (#75) * Feat: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#75) * Feat: ๋‚ ์งœ ๋ฐ์ดํ„ฐ์˜ ๋ฒ”์œ„๋ฅผ ๋‹ด๋Š” ํด๋ž˜์Šค ๊ตฌํ˜„ (#75) * Feat: ๋‚ ์งœ ๋ฐ์ดํ„ฐ์˜ ๋ฒ”์œ„๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํด๋ž˜์Šค ๊ตฌํ˜„ (#75) * Feat: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต ๋กœ์ง์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ์ฝ”๋“œ ๊ตฌํ˜„ (#75) * Feat: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#75) * Test: ํฌ์ธํŠธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#75) * Test: ๋‚ ์งœ ๋ฐ์ดํ„ฐ ๊ด€๋ จ ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#75) --- .../livable/server/point/controller/.gitkeep | 0 .../point/controller/PointController.java | 29 +++++++ .../server/point/domain/DateFactory.java | 23 +++++ .../server/point/domain/DateRange.java | 24 ++++++ .../server/point/domain/PointErrorCode.java | 17 ++++ .../com/livable/server/point/dto/.gitkeep | 0 .../server/point/dto/PointResponse.java | 14 +++ .../livable/server/point/exception/.gitkeep | 0 .../livable/server/point/repository/.gitkeep | 0 .../point/repository/PointRepository.java | 28 ++++++ .../com/livable/server/point/service/.gitkeep | 0 .../server/point/service/PointService.java | 41 +++++++++ .../point/controller/PointControllerTest.java | 56 ++++++++++++ .../server/point/domain/DateFactoryTest.java | 36 ++++++++ .../server/point/domain/DateRangeTest.java | 30 +++++++ .../point/service/PointServiceTest.java | 85 +++++++++++++++++++ 16 files changed, 383 insertions(+) delete mode 100644 src/main/java/com/livable/server/point/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/point/controller/PointController.java create mode 100644 src/main/java/com/livable/server/point/domain/DateFactory.java create mode 100644 src/main/java/com/livable/server/point/domain/DateRange.java create mode 100644 src/main/java/com/livable/server/point/domain/PointErrorCode.java delete mode 100644 src/main/java/com/livable/server/point/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/point/dto/PointResponse.java delete mode 100644 src/main/java/com/livable/server/point/exception/.gitkeep delete mode 100644 src/main/java/com/livable/server/point/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/point/repository/PointRepository.java delete mode 100644 src/main/java/com/livable/server/point/service/.gitkeep create mode 100644 src/main/java/com/livable/server/point/service/PointService.java create mode 100644 src/test/java/com/livable/server/point/controller/PointControllerTest.java create mode 100644 src/test/java/com/livable/server/point/domain/DateFactoryTest.java create mode 100644 src/test/java/com/livable/server/point/domain/DateRangeTest.java create mode 100644 src/test/java/com/livable/server/point/service/PointServiceTest.java diff --git a/src/main/java/com/livable/server/point/controller/.gitkeep b/src/main/java/com/livable/server/point/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/point/controller/PointController.java b/src/main/java/com/livable/server/point/controller/PointController.java new file mode 100644 index 00000000..1d06de7a --- /dev/null +++ b/src/main/java/com/livable/server/point/controller/PointController.java @@ -0,0 +1,29 @@ +package com.livable.server.point.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.service.PointService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; + +@RequiredArgsConstructor +@RestController +public class PointController { + + private final PointService pointService; + + @GetMapping("/api/points/logs/members") + public ResponseEntity> getMyReviewCount() { + + Long memberId = 1L; // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ + LocalDateTime currentDate = LocalDateTime.now(); + + PointResponse.ReviewCountDTO myReviewCount = pointService.getMyReviewCount(memberId, currentDate); + return ApiResponse.success(myReviewCount, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/point/domain/DateFactory.java b/src/main/java/com/livable/server/point/domain/DateFactory.java new file mode 100644 index 00000000..cbdde715 --- /dev/null +++ b/src/main/java/com/livable/server/point/domain/DateFactory.java @@ -0,0 +1,23 @@ +package com.livable.server.point.domain; + +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; + +@Component +public class DateFactory { + + /** + * ๊ธฐ์ค€์ด ๋˜๋Š” ๋‚ ์งœ ์ •๋ณด๋ฅผ ๋ฐ›์•„ ํ•ด๋‹น month์˜ ์‹œ์ž‘๊ณผ ๋ ๋ฒ”์œ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * @param localDateTime + * @return ํ•œ๋‹ฌ ๋ฒ”์œ„์˜ ์‹œ์ž‘๊ณผ ๋ ๋‚ ์งœ ๋ฐ์ดํ„ฐ + */ + public DateRange getMonthRangeOf(LocalDateTime localDateTime) { + + LocalDateTime startDate = localDateTime.with(TemporalAdjusters.firstDayOfMonth()); + LocalDateTime endDate = localDateTime.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth()); + + return new DateRange(startDate, endDate); + } +} diff --git a/src/main/java/com/livable/server/point/domain/DateRange.java b/src/main/java/com/livable/server/point/domain/DateRange.java new file mode 100644 index 00000000..8bcd7bc1 --- /dev/null +++ b/src/main/java/com/livable/server/point/domain/DateRange.java @@ -0,0 +1,24 @@ +package com.livable.server.point.domain; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class DateRange { + + private final LocalDateTime startDate; + private final LocalDateTime endDate; + + public DateRange(LocalDateTime startDate, LocalDateTime endDate) { + this.startDate = getStartTimeOfMonth(startDate); + this.endDate = getStartTimeOfMonth(endDate); + } + + private LocalDateTime getStartTimeOfMonth(LocalDateTime localDateTime) { + return localDateTime.withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); + } +} diff --git a/src/main/java/com/livable/server/point/domain/PointErrorCode.java b/src/main/java/com/livable/server/point/domain/PointErrorCode.java new file mode 100644 index 00000000..01cb8f02 --- /dev/null +++ b/src/main/java/com/livable/server/point/domain/PointErrorCode.java @@ -0,0 +1,17 @@ +package com.livable.server.point.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum PointErrorCode implements ErrorCode { + + POINT_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํฌ์ธํŠธ ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/livable/server/point/dto/.gitkeep b/src/main/java/com/livable/server/point/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/point/dto/PointResponse.java b/src/main/java/com/livable/server/point/dto/PointResponse.java new file mode 100644 index 00000000..27eddd8d --- /dev/null +++ b/src/main/java/com/livable/server/point/dto/PointResponse.java @@ -0,0 +1,14 @@ +package com.livable.server.point.dto; + +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PointResponse { + + @Getter + @AllArgsConstructor + public static class ReviewCountDTO { + + private Long count; + } +} diff --git a/src/main/java/com/livable/server/point/exception/.gitkeep b/src/main/java/com/livable/server/point/exception/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/point/repository/.gitkeep b/src/main/java/com/livable/server/point/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/point/repository/PointRepository.java b/src/main/java/com/livable/server/point/repository/PointRepository.java new file mode 100644 index 00000000..47580a9b --- /dev/null +++ b/src/main/java/com/livable/server/point/repository/PointRepository.java @@ -0,0 +1,28 @@ +package com.livable.server.point.repository; + +import com.livable.server.entity.Point; +import com.livable.server.entity.PointCode; +import com.livable.server.point.dto.PointResponse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface PointRepository extends JpaRepository { + + Optional findByMember_Id(Long memberId); + + @Query("SELECT new com.livable.server.point.dto.PointResponse$ReviewCountDTO(COUNT(pl.id)) " + + "FROM PointLog pl " + + "WHERE pl.point.id = :pointId " + + "AND pl.createdAt BETWEEN :startDate AND :endDate " + + "AND pl.code IN (:codes)") + PointResponse.ReviewCountDTO findPointCountById( + @Param("pointId") Long pointId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("codes") List codes); +} diff --git a/src/main/java/com/livable/server/point/service/.gitkeep b/src/main/java/com/livable/server/point/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/point/service/PointService.java b/src/main/java/com/livable/server/point/service/PointService.java new file mode 100644 index 00000000..ef755ec4 --- /dev/null +++ b/src/main/java/com/livable/server/point/service/PointService.java @@ -0,0 +1,41 @@ +package com.livable.server.point.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Point; +import com.livable.server.entity.PointCode; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.point.domain.DateFactory; +import com.livable.server.point.domain.DateRange; +import com.livable.server.point.domain.PointErrorCode; +import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.repository.PointRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class PointService { + + private final PointRepository pointRepository; + private final DateFactory dateFactory; + + @Transactional(readOnly = true) + public PointResponse.ReviewCountDTO getMyReviewCount(Long memberId, LocalDateTime currentDate) { + + Point point = pointRepository.findByMember_Id(memberId).orElseThrow(() -> + new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); + + DateRange dateRange = dateFactory.getMonthRangeOf(currentDate); + + return pointRepository.findPointCountById( + point.getId(), + dateRange.getStartDate(), + dateRange.getEndDate(), + List.of(PointCode.PA00, PointCode.PA01, PointCode.PA02) + ); + } +} diff --git a/src/test/java/com/livable/server/point/controller/PointControllerTest.java b/src/test/java/com/livable/server/point/controller/PointControllerTest.java new file mode 100644 index 00000000..8088e1cd --- /dev/null +++ b/src/test/java/com/livable/server/point/controller/PointControllerTest.java @@ -0,0 +1,56 @@ +package com.livable.server.point.controller; + +import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.service.PointService; +import com.livable.server.review.dto.MyReviewResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.time.LocalDateTime; + +@WebMvcTest(PointController.class) +class PointControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PointService pointService; + + @Nested + @DisplayName("๋‚˜์˜ ๋ฆฌ๋ทฐ ์นด์šดํŠธ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/points/logs/members"; + + PointResponse.ReviewCountDTO mockResponse + = new PointResponse.ReviewCountDTO(5L); + + Mockito.when(pointService.getMyReviewCount( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(mockResponse); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/point/domain/DateFactoryTest.java b/src/test/java/com/livable/server/point/domain/DateFactoryTest.java new file mode 100644 index 00000000..0acfe2ea --- /dev/null +++ b/src/test/java/com/livable/server/point/domain/DateFactoryTest.java @@ -0,0 +1,36 @@ +package com.livable.server.point.domain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class DateFactoryTest { + + private DateFactory dateFactory; + + @BeforeEach + void setUp() { + dateFactory = new DateFactory(); + } + + @DisplayName("ํ•œ๋‹ฌ์˜ ์‹œ์ž‘๊ณผ ๋์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ - ์„ฑ๊ณต") + @Test + void getMonthRange_Success_Test() { + // Given + LocalDateTime startDate = LocalDateTime.of(2023, 1, 1, 0, 0); + LocalDateTime endDate = LocalDateTime.of(2023, 2, 1, 0, 0); + LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 1, 1, 1); + + // When + DateRange actual = dateFactory.getMonthRangeOf(localDateTime); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(startDate, actual.getStartDate()), + () -> Assertions.assertEquals(endDate, actual.getEndDate()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/point/domain/DateRangeTest.java b/src/test/java/com/livable/server/point/domain/DateRangeTest.java new file mode 100644 index 00000000..79fe54d1 --- /dev/null +++ b/src/test/java/com/livable/server/point/domain/DateRangeTest.java @@ -0,0 +1,30 @@ +package com.livable.server.point.domain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class DateRangeTest { + + @DisplayName("์ƒ์„ฑ์ž ํ…Œ์ŠคํŠธ - ์„ฑ๊ณต") + @Test + void success_Test() { + // Given + LocalDateTime startDate = LocalDateTime.of(2023, 1, 23, 4, 12); + LocalDateTime endDate = LocalDateTime.of(2023, 2, 23, 4, 12); + + LocalDateTime expectedStartDate = LocalDateTime.of(2023, 1, 23, 0, 0); + LocalDateTime expectedEndDate = LocalDateTime.of(2023, 2, 23, 0, 0); + + // When + DateRange actual = new DateRange(startDate, endDate); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(expectedStartDate, actual.getStartDate()), + () -> Assertions.assertEquals(expectedEndDate, actual.getEndDate()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/point/service/PointServiceTest.java b/src/test/java/com/livable/server/point/service/PointServiceTest.java new file mode 100644 index 00000000..94b202d0 --- /dev/null +++ b/src/test/java/com/livable/server/point/service/PointServiceTest.java @@ -0,0 +1,85 @@ +package com.livable.server.point.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Point; +import com.livable.server.point.domain.DateFactory; +import com.livable.server.point.domain.DateRange; +import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.repository.PointRepository; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class PointServiceTest { + + @Mock + private PointRepository pointRepository; + + @Mock + private DateFactory dateFactory; + + @InjectMocks + private PointService pointService; + + @Nested + @DisplayName("๋‚˜์˜ ๋ฆฌ๋ทฐ ์นด์šดํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class MyRestaurantReview { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + Long memberId = 1L; + LocalDateTime currentDate = LocalDateTime.now(); + PointResponse.ReviewCountDTO countDTO = new PointResponse.ReviewCountDTO(5L); + Point point = Point.builder().id(1L).build(); + + LocalDateTime startDate = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime endDte = LocalDateTime.of(2023, 2, 1, 0, 0, 0); + DateRange dateRange = new DateRange(startDate, endDte); + + Mockito.when(dateFactory.getMonthRangeOf(ArgumentMatchers.any(LocalDateTime.class))) + .thenReturn(dateRange); + + Mockito.when(pointRepository.findByMember_Id(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointRepository.findPointCountById( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.anyList() + )).thenReturn(countDTO); + + // When + PointResponse.ReviewCountDTO actual = pointService.getMyReviewCount(memberId, currentDate); + + // Then + Assertions.assertEquals(5L, actual.getCount()); + } + + @DisplayName("์‹คํŒจ - ์œ ํšจํ•˜์ง€ ์•Š์€ ํฌ์ธํŠธ ์ •๋ณด") + @Test + void failure_Test_existPointData() { + // Given + Long memberId = 1L; + LocalDateTime currentDate = LocalDateTime.now(); + + Mockito.when(pointRepository.findByMember_Id(ArgumentMatchers.anyLong())) + .thenReturn(Optional.empty()); + + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + pointService.getMyReviewCount(memberId, currentDate)); + } + } +} \ No newline at end of file From 538934f35deac86c59241f4802582890381cd36c Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Mon, 25 Sep 2023 21:41:02 +0900 Subject: [PATCH 040/111] =?UTF-8?q?JWT=20=ED=86=A0=ED=81=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=99=84=EB=A3=8C=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: jjwt ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ˆ˜์ • (#91) * Feat: Actor CustomResolver ๊ตฌํ˜„ ๋ฐ ๊ด€๋ จ ๋„๋ฉ”์ธ ๊ตฌ์„ฑ (#91) * Feat: Claims ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๊ตฌํ˜„ (#91) * Feat: MemberErrorCode์— JWT ๊ด€๋ จ ์—๋Ÿฌ์ฝ”๋“œ ์ถ”๊ฐ€ (#91) * Refactor: Controller์— Member, Visitor ํ† ํฐ ์ ์šฉ (#91) * Refactor: Controller Test์— Member, Visitor ํ† ํฐ ์ ์šฉ (#91) --- build.gradle | 5 +- .../livable/server/core/config/WebConfig.java | 15 +++++ .../com/livable/server/core/util/Actor.java | 12 ++++ .../livable/server/core/util/ActorType.java | 12 ++++ .../server/core/util/JwtTokenProvider.java | 45 +++++++++++--- .../livable/server/core/util/LoginActor.java | 11 ++++ .../core/util/LoginActorArgumentResolver.java | 43 +++++++++++++ .../home/controller/HomeController.java | 9 ++- .../controller/InvitationController.java | 43 +++++++++---- .../member/controller/MemberController.java | 9 ++- .../server/member/domain/MemberErrorCode.java | 4 +- .../point/controller/PointController.java | 9 ++- .../controller/ReservationController.java | 13 +++- .../controller/RestaurantController.java | 12 +++- .../review/controller/MyReviewController.java | 24 +++++--- .../controller/VisitationController.java | 23 +++++-- .../core/util/JwtTokenProviderTest.java | 2 +- .../livable/server/core/util/TestConfig.java | 17 ++++++ .../home/controller/HomeControllerTest.java | 22 ++++++- .../controller/InvitationControllerTest.java | 60 +++++++++++++++++-- .../controller/MemberControllerTest.java | 12 ++++ .../point/controller/PointControllerTest.java | 12 +++- .../controller/ReservationControllerTest.java | 12 ++++ .../controller/RestaurantControllerTest.java | 12 ++++ .../controller/MyReviewControllerTest.java | 16 +++++ .../RestaurantReviewControllerTest.java | 3 + .../controller/VisitationControllerTest.java | 21 +++++++ 27 files changed, 419 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/livable/server/core/util/Actor.java create mode 100644 src/main/java/com/livable/server/core/util/LoginActor.java create mode 100644 src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java create mode 100644 src/test/java/com/livable/server/core/util/TestConfig.java diff --git a/build.gradle b/build.gradle index 3a379762..9bdfc282 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,10 @@ dependencies { implementation 'com.google.zxing:core:3.5.2' implementation 'com.google.zxing:javase:3.5.2' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - implementation 'io.jsonwebtoken:jjwt:0.9.1' +// implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' } tasks.named('test') { diff --git a/src/main/java/com/livable/server/core/config/WebConfig.java b/src/main/java/com/livable/server/core/config/WebConfig.java index 6c4ac9b9..1c84f0ee 100644 --- a/src/main/java/com/livable/server/core/config/WebConfig.java +++ b/src/main/java/com/livable/server/core/config/WebConfig.java @@ -1,15 +1,25 @@ package com.livable.server.core.config; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActorArgumentResolver; import com.livable.server.core.util.StringToLocalDateConverter; import com.livable.server.core.util.StringToRestaurantCategoryConverter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + +@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { + private final JwtTokenProvider tokenProvider; + + @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToRestaurantCategoryConverter()); @@ -25,4 +35,9 @@ public void addCorsMappings(CorsRegistry registry) { .exposedHeaders("Authorization") .allowedOriginPatterns("*"); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new LoginActorArgumentResolver(tokenProvider)); + } } diff --git a/src/main/java/com/livable/server/core/util/Actor.java b/src/main/java/com/livable/server/core/util/Actor.java new file mode 100644 index 00000000..0978c3c0 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/Actor.java @@ -0,0 +1,12 @@ +package com.livable.server.core.util; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Actor { + + private Long id; + private ActorType actorType; +} diff --git a/src/main/java/com/livable/server/core/util/ActorType.java b/src/main/java/com/livable/server/core/util/ActorType.java index bab77533..0d0fa7ab 100644 --- a/src/main/java/com/livable/server/core/util/ActorType.java +++ b/src/main/java/com/livable/server/core/util/ActorType.java @@ -1,6 +1,18 @@ package com.livable.server.core.util; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.member.domain.MemberErrorCode; + +import java.util.Arrays; + public enum ActorType { MEMBER, VISITOR; + + public static ActorType of(String type) { + return Arrays.stream(values()) + .filter(actorType -> actorType.name().equals(type)) + .findFirst() + .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.INVALID_ACTOR_TYPE)); + } } diff --git a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java index d4a5858a..b3ab044a 100644 --- a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java +++ b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java @@ -1,21 +1,22 @@ package com.livable.server.core.util; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.member.domain.MemberErrorCode; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.security.Key; import java.util.Date; @Component public class JwtTokenProvider { - private final String secretKey; + private final Key secretKey; public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { - this.secretKey = secretKey; + this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes()); } /** @@ -34,7 +35,7 @@ public String createActorToken(ActorType actorType, Long actorId, Date expireDat return Jwts.builder() .setClaims(claims) .setExpiration(expireDate) - .signWith(SignatureAlgorithm.HS256, secretKey) + .signWith(secretKey) .compact(); } @@ -45,7 +46,10 @@ public String createActorToken(ActorType actorType, Long actorId, Date expireDat */ public boolean isValidateToken(String token) { try { - Jws claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); return claimsJws.getBody().getExpiration().after(new Date()); } catch (Exception e) { @@ -53,4 +57,29 @@ public boolean isValidateToken(String token) { } } + public Claims parseClaims(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (JwtException e) { + e.printStackTrace(); + throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN); + } + } + + public static void checkMemberToken(Actor actor) { + if (actor.getActorType() != ActorType.MEMBER) { + throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN); + } + } + + public static void checkVisitorToken(Actor actor) { + if (actor.getActorType() != ActorType.VISITOR) { + throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN); + } + } + } diff --git a/src/main/java/com/livable/server/core/util/LoginActor.java b/src/main/java/com/livable/server/core/util/LoginActor.java new file mode 100644 index 00000000..8147e17c --- /dev/null +++ b/src/main/java/com/livable/server/core/util/LoginActor.java @@ -0,0 +1,11 @@ +package com.livable.server.core.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface LoginActor { +} diff --git a/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java b/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java new file mode 100644 index 00000000..e5dadddb --- /dev/null +++ b/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java @@ -0,0 +1,43 @@ +package com.livable.server.core.util; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Slf4j +public class LoginActorArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtTokenProvider tokenProvider; + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginActor.class) && parameter.getParameterType().equals(Actor.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) throws Exception { + String authorization = webRequest.getHeader("Authorization"); + String token = authorization.split("Bearer ")[1]; + + Claims claims = tokenProvider.parseClaims(token); + Long actorId = claims.get("actorId", Long.class); + String actorType = claims.get("actorType", String.class); + + return Actor.builder() + .id(actorId) + .actorType(ActorType.of(actorType)) + .build(); + } +} diff --git a/src/main/java/com/livable/server/home/controller/HomeController.java b/src/main/java/com/livable/server/home/controller/HomeController.java index 0fb3734e..f3d6005e 100644 --- a/src/main/java/com/livable/server/home/controller/HomeController.java +++ b/src/main/java/com/livable/server/home/controller/HomeController.java @@ -1,6 +1,9 @@ package com.livable.server.home.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.service.MemberService; import lombok.RequiredArgsConstructor; @@ -18,9 +21,11 @@ public class HomeController { private final MemberService memberService; @GetMapping - public ResponseEntity> getHomeInfo() { + public ResponseEntity> getHomeInfo(@LoginActor Actor actor) { - Long memberId = 1L; // TODO: 2023-09-22 JWT Token์œผ๋กœ ๋Œ€์ฒด + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); BuildingInfoDto buildingInfoDto = memberService.getBuildingInfo(memberId); return ApiResponse.success(buildingInfoDto, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java index d52ac586..ec43d010 100644 --- a/src/main/java/com/livable/server/invitation/controller/InvitationController.java +++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java @@ -1,9 +1,15 @@ package com.livable.server.invitation.controller; +import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.invitation.dto.InvitationRequest; import com.livable.server.invitation.dto.InvitationResponse; import com.livable.server.invitation.service.InvitationService; +import com.livable.server.member.domain.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,51 +27,64 @@ public class InvitationController { private final InvitationService invitationService; @GetMapping("/places/available") - public ResponseEntity> getAvailablePlaces() { + public ResponseEntity> getAvailablePlaces(@LoginActor Actor actor) { - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); return invitationService.getAvailablePlaces(memberId); } @PostMapping public ResponseEntity createInvitation( - @Valid @RequestBody InvitationRequest.CreateDTO dto) { + @Valid @RequestBody InvitationRequest.CreateDTO dto, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + Long memberId = actor.getId(); return invitationService.createInvitation(dto, memberId); } @GetMapping - public ResponseEntity>>> getInvitations() { + public ResponseEntity>>> getInvitations(@LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + Long memberId = actor.getId(); return invitationService.getInvitations(memberId); } @GetMapping("/{invitationId}") - public ResponseEntity> getInvitation(@PathVariable Long invitationId) { + public ResponseEntity> getInvitation( + @PathVariable Long invitationId, @LoginActor Actor actor) { - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); return invitationService.getInvitation(invitationId, memberId); } @DeleteMapping("/{invitationId}") - public ResponseEntity deleteInvitation(@PathVariable Long invitationId) { + public ResponseEntity deleteInvitation(@PathVariable Long invitationId, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + Long memberId = actor.getId(); return invitationService.deleteInvitation(invitationId, memberId); } @PatchMapping("/{invitationId}") public ResponseEntity updateInvitation( - @PathVariable Long invitationId, @Valid @RequestBody InvitationRequest.UpdateDTO dto) { + @PathVariable Long invitationId, @Valid @RequestBody InvitationRequest.UpdateDTO dto, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - Long memberId = 1L; // TODO: JWT ํ† ํฐ์—์„œ ์ถ”์ถœํ•œ ๊ฐ’์œผ๋กœ ์ˆ˜์ • + Long memberId = actor.getId(); return invitationService.updateInvitation(invitationId, dto, memberId); } diff --git a/src/main/java/com/livable/server/member/controller/MemberController.java b/src/main/java/com/livable/server/member/controller/MemberController.java index dca66fc1..74769bd3 100644 --- a/src/main/java/com/livable/server/member/controller/MemberController.java +++ b/src/main/java/com/livable/server/member/controller/MemberController.java @@ -1,6 +1,9 @@ package com.livable.server.member.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.service.MemberService; import lombok.RequiredArgsConstructor; @@ -16,9 +19,11 @@ public class MemberController { private final MemberService memberService; @GetMapping("/api/members") - public ResponseEntity> myPage() { + public ResponseEntity> myPage(@LoginActor Actor actor) { - Long memberId = 1L; // TODO: JWT ๊ตฌํ˜„ ํ›„ ํ† ํฐ์—์„œ ๊ฐ’ ์ถ”์ถœ ์˜ˆ์ • + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); MemberResponse.MyPageDTO myPageDTO = memberService.getMyPageData(memberId); return ApiResponse.success(myPageDTO, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java index a8ca8e11..f8b84298 100644 --- a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java +++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java @@ -12,7 +12,9 @@ public enum MemberErrorCode implements ErrorCode { MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), - BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ํšŒ์›์˜ ๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ํšŒ์›์˜ ๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "์ž˜๋ชป๋œ ํ† ํฐ ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + INVALID_ACTOR_TYPE(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ํ˜•์˜ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/point/controller/PointController.java b/src/main/java/com/livable/server/point/controller/PointController.java index 1d06de7a..efa042f3 100644 --- a/src/main/java/com/livable/server/point/controller/PointController.java +++ b/src/main/java/com/livable/server/point/controller/PointController.java @@ -1,6 +1,9 @@ package com.livable.server.point.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.point.dto.PointResponse; import com.livable.server.point.service.PointService; import lombok.RequiredArgsConstructor; @@ -18,9 +21,11 @@ public class PointController { private final PointService pointService; @GetMapping("/api/points/logs/members") - public ResponseEntity> getMyReviewCount() { + public ResponseEntity> getMyReviewCount(@LoginActor Actor actor) { - Long memberId = 1L; // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ LocalDateTime currentDate = LocalDateTime.now(); PointResponse.ReviewCountDTO myReviewCount = pointService.getMyReviewCount(memberId, currentDate); diff --git a/src/main/java/com/livable/server/reservation/controller/ReservationController.java b/src/main/java/com/livable/server/reservation/controller/ReservationController.java index ba7d6426..30299814 100644 --- a/src/main/java/com/livable/server/reservation/controller/ReservationController.java +++ b/src/main/java/com/livable/server/reservation/controller/ReservationController.java @@ -1,6 +1,9 @@ package com.livable.server.reservation.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.reservation.dto.ReservationResponse; import com.livable.server.reservation.service.ReservationService; import lombok.RequiredArgsConstructor; @@ -21,10 +24,14 @@ public class ReservationController { @GetMapping("/places/{commonPlaceId}") public ResponseEntity> findAvailableTimes( @PathVariable Long commonPlaceId, - @RequestParam("date") LocalDate localDate - ) { + @RequestParam("date") LocalDate localDate, + @LoginActor Actor actor + ) { + + JwtTokenProvider.checkMemberToken(actor); + List result = - reservationService.findAvailableReservationTimes(1L, commonPlaceId, localDate); + reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, localDate); return ApiResponse.success(result, HttpStatus.OK); } } diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 1afc9c99..a91fd956 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -1,6 +1,9 @@ package com.livable.server.restaurant.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.dto.RestaurantResponse; import com.livable.server.restaurant.service.RestaurantService; @@ -23,9 +26,12 @@ public class RestaurantController { @GetMapping public ResponseEntity> findRestaurantByCategory( - @RequestParam("type") RestaurantCategory restaurantCategory - ) { - Long visitorId = 1L; + @RequestParam("type") RestaurantCategory restaurantCategory, @LoginActor Actor actor + ) { + + JwtTokenProvider.checkVisitorToken(actor); + + Long visitorId = actor.getId(); List result = restaurantService.findNearRestaurantByVisitorIdAndRestaurantCategory(visitorId, restaurantCategory); diff --git a/src/main/java/com/livable/server/review/controller/MyReviewController.java b/src/main/java/com/livable/server/review/controller/MyReviewController.java index d2cb8c03..98932752 100644 --- a/src/main/java/com/livable/server/review/controller/MyReviewController.java +++ b/src/main/java/com/livable/server/review/controller/MyReviewController.java @@ -1,6 +1,9 @@ package com.livable.server.review.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.review.dto.MyReviewResponse; import com.livable.server.review.service.MyReviewService; import lombok.RequiredArgsConstructor; @@ -18,10 +21,11 @@ public class MyReviewController { @GetMapping("/api/reviews/restaurant/{reviewId}/members") public ResponseEntity> getMyRestaurantReview( - @PathVariable Long reviewId) { + @PathVariable Long reviewId, @LoginActor Actor actor) { - // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ - Long memberId = 1L; + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); MyReviewResponse.DetailDTO myRestaurantReview = myReviewService.getMyRestaurantReview(reviewId, memberId); return ApiResponse.success(myRestaurantReview, HttpStatus.OK); @@ -29,10 +33,11 @@ public ResponseEntity> getMyRest @GetMapping("/api/reviews/cafeteria/{reviewId}/members") public ResponseEntity> getMyCafeteriaReviewDetail( - @PathVariable Long reviewId) { + @PathVariable Long reviewId, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ - Long memberId = 1L; + Long memberId = actor.getId(); MyReviewResponse.DetailDTO myCafeteriaReview = myReviewService.getMyCafeteriaReview(reviewId, memberId); return ApiResponse.success(myCafeteriaReview, HttpStatus.OK); @@ -40,10 +45,11 @@ public ResponseEntity> getMyCafe @GetMapping("/api/reviews/lunchbox/{reviewId}/members") public ResponseEntity> getMyLunchboxReviewDetail( - @PathVariable Long reviewId) { + @PathVariable Long reviewId, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); - // TODO: JWT ๊ตฌํ˜„์™„๋ฃŒ ์‹œ ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊บผ๋‚ด์˜ฌ ๊ฒƒ - Long memberId = 1L; + Long memberId = actor.getId(); MyReviewResponse.DetailDTO myLunchBoxReview = myReviewService.getMyLunchBoxReview(reviewId, memberId); return ApiResponse.success(myLunchBoxReview, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index dc023a35..2093d2a4 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -1,6 +1,9 @@ package com.livable.server.visitation.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; import com.livable.server.visitation.dto.VisitationResponse; import com.livable.server.visitation.service.VisitationFacadeService; import com.livable.server.visitation.dto.VisitationRequest; @@ -19,8 +22,11 @@ public class VisitationController { private final VisitationFacadeService visitationFacadeService; @GetMapping - public ResponseEntity> findVisitationDetailInformation() { - Long visitorId = 1L; + public ResponseEntity> findVisitationDetailInformation(@LoginActor Actor actor) { + + JwtTokenProvider.checkVisitorToken(actor); + + Long visitorId = actor.getId(); VisitationResponse.DetailInformationDto detailInformationDto = visitationFacadeService.findVisitationDetailInformation(visitorId); @@ -28,9 +34,11 @@ public ResponseEntity> findVisitationDetailInformati } @GetMapping("/qr") - public ResponseEntity> createQrCode() { + public ResponseEntity> createQrCode(@LoginActor Actor actor) { + + JwtTokenProvider.checkVisitorToken(actor); - Long visitorId = 1L; + Long visitorId = actor.getId(); String base64QrCode = visitationFacadeService.createQrCode(visitorId); return ApiResponse.success(base64QrCode, HttpStatus.OK); @@ -48,9 +56,12 @@ public ResponseEntity> validateQrCode( @PostMapping("/parking") public ResponseEntity> registerParking( - @RequestBody @Valid VisitationRequest.RegisterParkingDto registerParkingDto + @RequestBody @Valid VisitationRequest.RegisterParkingDto registerParkingDto, @LoginActor Actor actor ) { - Long visitorId = 1L; + + JwtTokenProvider.checkVisitorToken(actor); + + Long visitorId = actor.getId(); visitationFacadeService.registerParking(visitorId, registerParkingDto.getCarNumber()); return ApiResponse.success(HttpStatus.CREATED); diff --git a/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java b/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java index 6fa0f8e0..ffcaed8c 100644 --- a/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java +++ b/src/test/java/com/livable/server/core/util/JwtTokenProviderTest.java @@ -14,7 +14,7 @@ class JwtTokenProviderTest { @BeforeEach void init() { - String testSecretKey = "secretKey"; + String testSecretKey = "di0xNUNaQDR1MWksaXM4MH5rdSZYLEM2I3dbR0ZQcWJUOVl5UFhmOV52cEROLmE0bCZheHdWLztCZHJoVjwz"; tokenProvider = new JwtTokenProvider(testSecretKey); } diff --git a/src/test/java/com/livable/server/core/util/TestConfig.java b/src/test/java/com/livable/server/core/util/TestConfig.java new file mode 100644 index 00000000..3c157473 --- /dev/null +++ b/src/test/java/com/livable/server/core/util/TestConfig.java @@ -0,0 +1,17 @@ +package com.livable.server.core.util; + + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestConfig { + + @Bean + public JwtTokenProvider jwtTokenProvider() { + String secretKey = "di0xNUNaQDR1MWksaXM4MH5rdSZYLEM2I3dbR0ZQcWJUOVl5UFhmOV52cEROLmE0bCZheHdWLztCZHJoVjwz"; + + return new JwtTokenProvider(secretKey); + } + +} diff --git a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java index 2f0d2eba..f4f9482d 100644 --- a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java +++ b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java @@ -7,6 +7,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.service.MemberService; @@ -15,14 +18,21 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +import java.util.Date; + +@Import(TestConfig.class) @WebMvcTest(HomeController.class) class HomeControllerTest { @Autowired MockMvc mockMvc; + @Autowired + JwtTokenProvider tokenProvider; + @MockBean private MemberService memberService; @@ -31,11 +41,15 @@ class HomeControllerTest { void getHomeInfoSuccess() throws Exception { // given Long memberId = 1L; + String token = tokenProvider.createActorToken(ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); given(memberService.getBuildingInfo(memberId)) .willReturn(new BuildingInfoDto(1L, "ํ…Œ๋ผ ํƒ€์›Œ", true)); // when & then - mockMvc.perform(get("/api/home")) + mockMvc.perform( + get("/api/home") + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isOk()) .andExpect(jsonPath("$.data['buildingId']").value(1)) .andExpect(jsonPath("$.data['buildingName']").value("ํ…Œ๋ผ ํƒ€์›Œ")) @@ -46,11 +60,15 @@ void getHomeInfoSuccess() throws Exception { @Test void getHomeInfoFailed() throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); given(memberService.getBuildingInfo(anyLong())) .willThrow(new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); // when & then - mockMvc.perform(get("/api/home")) + mockMvc.perform( + get("/api/home") + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(MemberErrorCode.BUILDING_INFO_NOT_EXIST.getMessage())); } diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index 0cc0467e..afad199d 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.domain.InvitationValidationMessage; import com.livable.server.invitation.dto.InvitationRequest; @@ -13,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -21,10 +25,10 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -32,12 +36,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Import(TestConfig.class) @WebMvcTest(InvitationController.class) class InvitationControllerTest { @Autowired MockMvc mockMvc; + @Autowired + private JwtTokenProvider tokenProvider; + @Autowired ObjectMapper mapper; @@ -60,8 +68,13 @@ void getAvailablePlacesSuccess_01() throws Exception { HttpStatus.OK )); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then - mockMvc.perform(get("/api/invitation/places/available")) + mockMvc.perform( + get("/api/invitation/places/available") + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isOk()) .andExpect(jsonPath("$.data['offices']").isArray()) .andExpect(jsonPath("$.data['offices'][0]['officeName']").value("์‚ฌ๋ฌด์‹ค A")) @@ -92,8 +105,12 @@ void getAvailablePlacesFail_01() throws Exception { given(invitationService.getAvailablePlaces(anyLong())) .willThrow(new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST)); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then - mockMvc.perform(get("/api/invitation/places/available")) + mockMvc.perform(get("/api/invitation/places/available") + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(InvitationErrorCode.MEMBER_NOT_EXIST.getMessage())); } @@ -117,9 +134,12 @@ void createInvitationFail_01() throws Exception { )) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then mockMvc.perform( post("/api/invitation") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto)) ) @@ -141,9 +161,12 @@ void createInvitationFail_02() throws Exception { .visitors(List.of()) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then mockMvc.perform( post("/api/invitation") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto)) ) @@ -170,9 +193,12 @@ void createInvitationFail_03() throws Exception { .visitors(visitors) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then mockMvc.perform( post("/api/invitation") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto)) ) @@ -188,8 +214,13 @@ void getInvitationFail_01() throws Exception { given(invitationService.getInvitation(anyLong(), anyLong())) .willThrow(new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER)); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then - mockMvc.perform(get("/api/invitation/{invitationId}", invitationId)) + mockMvc.perform( + get("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isForbidden()) .andExpect(jsonPath("$.message").value(InvitationErrorCode.INVALID_INVITATION_OWNER.getMessage())); } @@ -202,8 +233,13 @@ void getInvitationSuccess_01() throws Exception { given(invitationService.getInvitation(anyLong(), anyLong())) .willReturn(ApiResponse.success(InvitationResponse.DetailDTO.builder().build(), HttpStatus.OK)); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When & Then - mockMvc.perform(get("/api/invitation/{invitationId}", invitationId)) + mockMvc.perform( + get("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) + ) .andExpect(status().isOk()); } @@ -222,9 +258,12 @@ void updateInvitationFail_01() throws Exception { .visitors(List.of()) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When ResultActions resultActions = mockMvc.perform( patch("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto))); @@ -249,9 +288,12 @@ void updateInvitationFail_02() throws Exception { .visitors(null) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When ResultActions resultActions = mockMvc.perform( patch("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto))); @@ -281,9 +323,12 @@ void updateInvitationFail_03() throws Exception { )) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When ResultActions resultActions = mockMvc.perform( patch("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto))); @@ -313,9 +358,12 @@ void updateInvitationSuccess_01() throws Exception { )) .build(); + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + // When ResultActions resultActions = mockMvc.perform( patch("/api/invitation/{invitationId}", invitationId) + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(dto))); diff --git a/src/test/java/com/livable/server/member/controller/MemberControllerTest.java b/src/test/java/com/livable/server/member/controller/MemberControllerTest.java index 5658bb56..56eaab27 100644 --- a/src/test/java/com/livable/server/member/controller/MemberControllerTest.java +++ b/src/test/java/com/livable/server/member/controller/MemberControllerTest.java @@ -1,5 +1,8 @@ package com.livable.server.member.controller; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.service.MemberService; import org.junit.jupiter.api.DisplayName; @@ -10,17 +13,24 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.Date; + +@Import(TestConfig.class) @WebMvcTest(MemberController.class) class MemberControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private JwtTokenProvider tokenProvider; + @MockBean private MemberService memberService; @@ -33,6 +43,7 @@ class MyRestaurantReview { void success_Test() throws Exception { // Given String uri = "/api/members"; + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); MemberResponse.MyPageDTO mockDTO = MemberResponse.MyPageDTO.builder() .memberName("TestName") @@ -46,6 +57,7 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) + .header("Authorization", "Bearer " + token) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); diff --git a/src/test/java/com/livable/server/point/controller/PointControllerTest.java b/src/test/java/com/livable/server/point/controller/PointControllerTest.java index 8088e1cd..59a8f4f5 100644 --- a/src/test/java/com/livable/server/point/controller/PointControllerTest.java +++ b/src/test/java/com/livable/server/point/controller/PointControllerTest.java @@ -1,8 +1,10 @@ package com.livable.server.point.controller; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.point.dto.PointResponse; import com.livable.server.point.service.PointService; -import com.livable.server.review.dto.MyReviewResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -11,19 +13,25 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.time.LocalDateTime; +import java.util.Date; +@Import(TestConfig.class) @WebMvcTest(PointController.class) class PointControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private JwtTokenProvider tokenProvider; + @MockBean private PointService pointService; @@ -36,6 +44,7 @@ class MyRestaurantReview { void success_Test() throws Exception { // Given String uri = "/api/points/logs/members"; + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); PointResponse.ReviewCountDTO mockResponse = new PointResponse.ReviewCountDTO(5L); @@ -48,6 +57,7 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) + .header("Authorization", "Bearer " + token) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); diff --git a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java index edbdae82..efbd4769 100644 --- a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java @@ -1,5 +1,8 @@ package com.livable.server.reservation.controller; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.reservation.dto.ReservationResponse; import com.livable.server.reservation.service.ReservationService; import org.junit.jupiter.api.DisplayName; @@ -7,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -14,6 +18,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -24,12 +29,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@Import(TestConfig.class) @WebMvcTest(ReservationController.class) class ReservationControllerTest { @Autowired MockMvc mockMvc; + @Autowired + JwtTokenProvider tokenProvider; + @MockBean ReservationService reservationService; @@ -39,6 +48,8 @@ class ReservationControllerTest { @Test void findAvailableTimesSuccessTest() throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + List result = IntStream.range(1, 10) .mapToObj(idx -> ReservationResponse.AvailableReservationTimePerDateDto.builder() .date(LocalDate.now()) @@ -53,6 +64,7 @@ void findAvailableTimesSuccessTest() throws Exception { // when ResultActions resultActions = mockMvc.perform( get("/api/reservation/places/1") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .queryParam("date", "2023-09-22") ); diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java index 0f652d49..dca41875 100644 --- a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -1,5 +1,8 @@ package com.livable.server.restaurant.controller; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.dto.RestaurantResponse; import com.livable.server.restaurant.mock.MockNearRestaurantDto; @@ -10,9 +13,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -22,12 +27,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Import(TestConfig.class) @WebMvcTest(RestaurantController.class) class RestaurantControllerTest { @Autowired MockMvc mockMvc; + @Autowired + JwtTokenProvider tokenProvider; + @MockBean RestaurantService restaurantService; @@ -36,6 +45,8 @@ class RestaurantControllerTest { @ParameterizedTest(name = "[{index}] category={0}") void findRestaurantByCategorySuccessTest(String category) throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); + List result = IntStream.range(1, 10) .mapToObj(idx -> new MockNearRestaurantDto()) @@ -49,6 +60,7 @@ void findRestaurantByCategorySuccessTest(String category) throws Exception { // when ResultActions resultActions = mockMvc.perform( get("/api/restaurant") + .header("Authorization", "Bearer " + token) .queryParam("type", "restaurant") ); diff --git a/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java index 753153f5..21f20d62 100644 --- a/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/MyReviewControllerTest.java @@ -1,5 +1,8 @@ package com.livable.server.review.controller; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.review.dto.MyReviewResponse; import com.livable.server.review.service.MyReviewService; import org.junit.jupiter.api.DisplayName; @@ -10,17 +13,24 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.Date; + +@Import(TestConfig.class) @WebMvcTest(MyReviewController.class) class MyReviewControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private JwtTokenProvider tokenProvider; + @MockBean private MyReviewService myReviewService; @@ -33,6 +43,7 @@ class MyRestaurantReview { void success_Test() throws Exception { // Given String uri = "/api/reviews/restaurant/1/members"; + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); MyReviewResponse.DetailDTO mockResponse = MyReviewResponse.DetailDTO.builder().build(); @@ -45,6 +56,7 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) + .header("Authorization", "Bearer " + token) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); @@ -60,6 +72,7 @@ class MyCafeteriaReview { void success_Test() throws Exception { // Given String uri = "/api/reviews/cafeteria/1/members"; + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); MyReviewResponse.DetailDTO mockResponse = MyReviewResponse.DetailDTO.builder().build(); @@ -72,6 +85,7 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) + .header("Authorization", "Bearer " + token) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); @@ -86,6 +100,7 @@ class MyLunchBoxReview { @Test void success_Test() throws Exception { // Given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); String uri = "/api/reviews/lunchbox/1/members"; MyReviewResponse.DetailDTO mockResponse @@ -99,6 +114,7 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) + .header("Authorization", "Bearer " + token) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index 898a326f..49098365 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -1,5 +1,6 @@ package com.livable.server.review.controller; +import com.livable.server.core.util.TestConfig; import com.livable.server.review.dto.MyReviewResponse; import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.service.RestaurantReviewService; @@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -22,6 +24,7 @@ import java.util.List; +@Import(TestConfig.class) @WebMvcTest(controllers = RestaurantReviewController.class) class RestaurantReviewControllerTest { diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 5d8d36df..07bdf03b 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.livable.server.core.exception.ErrorCode; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationRequest; import com.livable.server.visitation.mock.MockDetailInformationDto; @@ -14,15 +17,19 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import java.util.Date; + import static org.mockito.BDDMockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@Import(TestConfig.class) @WebMvcTest(VisitationController.class) class VisitationControllerTest { @@ -32,6 +39,9 @@ class VisitationControllerTest { @Autowired ObjectMapper objectMapper; + @Autowired + JwtTokenProvider tokenProvider; + @MockBean private VisitationFacadeService visitationFacadeService; @@ -40,12 +50,15 @@ class VisitationControllerTest { void findVisitationDetailInformationSuccessTest() throws Exception { // Given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); MockDetailInformationDto mockDetailInformationDto = new MockDetailInformationDto(); given(visitationFacadeService.findVisitationDetailInformation(anyLong())).willReturn(mockDetailInformationDto); + // When ResultActions resultActions = mockMvc.perform( get("/api/visitation") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) ); @@ -60,6 +73,7 @@ void findVisitationDetailInformationSuccessTest() throws Exception { @DisplayName("[GET][/api/visitation/qr] - QR์„ ์ƒ์„ฑ ์ •์ƒ ์‘๋‹ต") @Test void createQrCodeSuccessTest() throws Exception { + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); String base64QrCode = "base64QrCode์ž„ ใ…‹ใ…‹"; // given @@ -68,6 +82,7 @@ void createQrCodeSuccessTest() throws Exception { // when ResultActions resultActions = mockMvc.perform( get("/api/visitation/qr") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) ); @@ -83,6 +98,7 @@ void createQrCodeSuccessTest() throws Exception { @Test void validateQrCodeSuccess() throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); String qr = "qr"; VisitationRequest.ValidateQrCodeDto validateQrCodeSuccessMockRequest = new MockValidateQrCodeDto(qr); @@ -91,6 +107,7 @@ void validateQrCodeSuccess() throws Exception { // when ResultActions resultActions = mockMvc.perform( post("/api/visitation/qr") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(validateQrCodeSuccessMockRequest)) ); @@ -105,6 +122,7 @@ void validateQrCodeSuccess() throws Exception { @Test void validateQrCodeFail() throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); String qr = "qr"; ErrorCode errorCode = VisitationErrorCode.INVALID_QR_PERIOD; String errorMessage = errorCode.getMessage(); @@ -116,6 +134,7 @@ void validateQrCodeFail() throws Exception { // when ResultActions resultActions = mockMvc.perform( post("/api/visitation/qr") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(validateQrCodeSuccessMockRequest)) ); @@ -132,6 +151,7 @@ void validateQrCodeFail() throws Exception { @Test void registerParking() throws Exception { // given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); String carNumber = "12๊ฐ€1234"; MockRegisterParkingDto mockRegisterParkingDto = new MockRegisterParkingDto(carNumber); Long visitorId = 1L; @@ -142,6 +162,7 @@ void registerParking() throws Exception { // when ResultActions resultActions = mockMvc.perform( post("/api/visitation/parking") + .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(mockRegisterParkingDto)) ); From 5fe494fa78421b5e05acc5c8458bdbf4f94c4262 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Tue, 26 Sep 2023 10:13:42 +0900 Subject: [PATCH 041/111] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=9C=EB=B2=84=EC=99=80=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=94=EA=B8=B0=20=EC=99=84=EB=A3=8C=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์‹œ๊ฐ„ ์„ค์ • (#94) * Chore: ๋กœ๊ทธ ํƒ€์ž„์กด ์„ค์ • (#94) --- .../com/livable/server/LivableServerApplication.java | 12 ++++++++++++ src/main/resources/application.yml | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/LivableServerApplication.java b/src/main/java/com/livable/server/LivableServerApplication.java index fb598eb6..487c7721 100644 --- a/src/main/java/com/livable/server/LivableServerApplication.java +++ b/src/main/java/com/livable/server/LivableServerApplication.java @@ -1,8 +1,14 @@ package com.livable.server; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import javax.annotation.PostConstruct; +import java.time.LocalDateTime; +import java.util.TimeZone; + +@Slf4j @SpringBootApplication public class LivableServerApplication { @@ -10,4 +16,10 @@ public static void main(String[] args) { SpringApplication.run(LivableServerApplication.class, args); } + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + log.info("ํ”„๋กœ์ ํŠธ ์„ธํŒ… ์‹œ๊ฐ„: {}", LocalDateTime.now()); // TODO: ์„œ๋ฒ„ ๋กœ๊ทธ ํ…Œ์ŠคํŠธ ํ›„ ์ง€์šธ์˜ˆ์ • + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 35663b09..27a4dea6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,4 +4,8 @@ spring: jpa: properties: hibernate: - default_batch_fetch_size: 100 \ No newline at end of file + default_batch_fetch_size: 100 + +logging: + pattern: + dateformat: yyyy-MM-dd HH:mm:ss.SSSz,Asia/Seoul \ No newline at end of file From d826b40cae7a9bb69cc5b1f09da73daaa8718529 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:45:19 +0900 Subject: [PATCH 042/111] =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=B6=9C=EC=9E=85?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Controller ๊ตฌํ˜„ (#69) ์ถœ์ž… ์นด๋“œ ํ™•์ธ ๊ด€๋ จ ์ฝ”๋“œ ๊ตฌํ˜„ * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ ์šฉ ํด๋ž˜์Šค ์ถ”๊ฐ€ (#69) * Test : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Controller ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ (#69) * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Repository ์ฝ”๋“œ ์ž‘์„ฑ (#69) * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Service ์ฝ”๋“œ ์ž‘์„ฑ (#69) * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Controller ์ฝ”๋“œ ์ž‘์„ฑ (#69) ๋ˆ„๋ฝ ์ฝ”๋“œ ์ถ”๊ฐ€ * Test : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Service ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ (#69) * Docs : ์ฝ”๋“œ ์Šคํƒ€์ผ ์ˆ˜์ • (#69) * Typo : ์˜คํƒ€ ์ˆ˜์ • (#69) * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Repository ์ฝ”๋“œ ์ˆ˜์ • (#69) Select ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์ผ ๊ฒฝ์šฐ ๊ฐ€์žฅ ์ฒซ๋ฒˆ์งธ ๊ฒฐ๊ณผ๊ฐ’์„ ๋งคํ•‘ํ•˜๋„๋ก ์ˆ˜์ • * Feat : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Service ์ฝ”๋“œ ์ˆ˜์ • (#69) ์ฟผ๋ฆฌ ์ˆ˜์ •์œผ๋กœ ์ธํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜ * TEST : ์ถœ์ž… ์นด๋“œ ํ™•์ธ Service ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • (#69) ์ฟผ๋ฆฌ ์ˆ˜์ •์œผ๋กœ ์ธํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜ * Refactor : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Controller ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ (#90) JWT ํ† ํฐ ์ถ”๊ฐ€๋กœ ์ธํ•œ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ * Refactor : JWT ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ (#69) JWT ํ† ํฐ ์ถ”๊ฐ€๋กœ ์ธํ•œ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ --- .../home/controller/HomeController.java | 17 ++- .../livable/server/home/dto/HomeResponse.java | 26 ++++ .../server/member/domain/MemberErrorCode.java | 6 +- .../member/dto/AccessCardProjection.java | 17 +++ .../member/repository/MemberRepository.java | 29 ++-- .../server/member/service/MemberService.java | 24 +++- .../home/controller/HomeControllerTest.java | 58 +++++++- .../member/service/MemberServiceTest.java | 126 ++++++++++++------ 8 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/livable/server/member/dto/AccessCardProjection.java diff --git a/src/main/java/com/livable/server/home/controller/HomeController.java b/src/main/java/com/livable/server/home/controller/HomeController.java index f3d6005e..91f58c0e 100644 --- a/src/main/java/com/livable/server/home/controller/HomeController.java +++ b/src/main/java/com/livable/server/home/controller/HomeController.java @@ -4,23 +4,22 @@ import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; +import com.livable.server.home.dto.HomeResponse.AccessCardDto; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor -@RequestMapping("/api/home") @RestController public class HomeController { private final MemberService memberService; - @GetMapping + @GetMapping("api/home") public ResponseEntity> getHomeInfo(@LoginActor Actor actor) { JwtTokenProvider.checkMemberToken(actor); @@ -31,4 +30,16 @@ public ResponseEntity> getHomeInfo(@LoginAc return ApiResponse.success(buildingInfoDto, HttpStatus.OK); } + @GetMapping("/api/access-card") + public ResponseEntity> getAccessCard(@LoginActor Actor actor){ + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + AccessCardDto accessCardDto = memberService.getAccessCardData(memberId); + + return ApiResponse.success(accessCardDto, HttpStatus.OK); + } + } diff --git a/src/main/java/com/livable/server/home/dto/HomeResponse.java b/src/main/java/com/livable/server/home/dto/HomeResponse.java index b9b213d0..e78a63b8 100644 --- a/src/main/java/com/livable/server/home/dto/HomeResponse.java +++ b/src/main/java/com/livable/server/home/dto/HomeResponse.java @@ -1,7 +1,9 @@ package com.livable.server.home.dto; +import com.livable.server.member.dto.AccessCardProjection; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,4 +19,28 @@ public static class BuildingInfoDto { private Boolean hasCafeteria; } + @Getter + @Builder + public static class AccessCardDto { + + private String buildingName; + private String employeeNumber; + private String companyName; + private String floor; + private String roomNumber; + private String employeeName; + + public static AccessCardDto from(AccessCardProjection accessCardProjection) { + return AccessCardDto + .builder() + .buildingName(accessCardProjection.getBuildingName()) + .employeeNumber(accessCardProjection.getEmployeeNumber()) + .companyName(accessCardProjection.getCompanyName()) + .floor(accessCardProjection.getFloor()) + .roomNumber(accessCardProjection.getRoomNumber()) + .employeeName(accessCardProjection.getEmployeeName()) + .build(); + } + } + } diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java index f8b84298..27b7d380 100644 --- a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java +++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java @@ -13,9 +13,13 @@ public enum MemberErrorCode implements ErrorCode { MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ํšŒ์›์˜ ๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "์ž˜๋ชป๋œ ํ† ํฐ ์ •๋ณด์ž…๋‹ˆ๋‹ค."), - INVALID_ACTOR_TYPE(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ํ˜•์˜ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."); + + INVALID_ACTOR_TYPE(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ํ˜•์˜ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."), + RETRIEVE_ACCESSCARD_FAILED(HttpStatus.BAD_REQUEST, "์ถœ์ž… ์นด๋“œ ์ •๋ณด๋ฅผ ์กฐํšŒ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/member/dto/AccessCardProjection.java b/src/main/java/com/livable/server/member/dto/AccessCardProjection.java new file mode 100644 index 00000000..fcd86984 --- /dev/null +++ b/src/main/java/com/livable/server/member/dto/AccessCardProjection.java @@ -0,0 +1,17 @@ +package com.livable.server.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AccessCardProjection { + + String buildingName; + String employeeNumber; + String companyName; + String floor; + String roomNumber; + String employeeName; + +} diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java index 9a8b52fa..db1c50b8 100644 --- a/src/main/java/com/livable/server/member/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -2,9 +2,10 @@ import com.livable.server.entity.Member; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; -import java.util.Optional; - +import com.livable.server.member.dto.AccessCardProjection; import com.livable.server.member.dto.MyPageProjection; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -18,13 +19,21 @@ public interface MemberRepository extends JpaRepository { "WHERE m.id = :memberId") Optional findMemberCompanyPointData(@Param("memberId") Long memberId); - @Query("select b.id as buildingId, b.name as buildingName, b.hasCafeteria as hasCafeteria" + - " from Member m " + - " join Company c" + - " on m.company = c" + - " join fetch Building b " + - " on c.building = b" + - " where m.id = :memberId") + @Query("SELECT distinct new com.livable.server.member.dto.AccessCardProjection(b.name, m.employeeNumber, c.name, o.floor, o.roomNumber, m.name) " + + "FROM Member m " + + "INNER JOIN Company c ON c.id = m.company.id " + + "INNER JOIN Office o ON c.id = o.company.id " + + "INNER JOIN Building b ON b.id = c.building.id " + + "WHERE m.id = :memberId") + List findAccessCardData(@Param("memberId") Long memberId); + + @Query("SELECT b.id AS buildingId, b.name AS buildingName, b.hasCafeteria AS hasCafeteria " + + "FROM Member m " + + "JOIN Company c " + + "ON m.company = c " + + "JOIN FETCH Building b " + + "ON c.building = b " + + "WHERE m.id = :memberId") Optional findBuildingInfoByMemberId(@Param("memberId") Long memberId); - + } diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java index c7b73089..24ba0524 100644 --- a/src/main/java/com/livable/server/member/service/MemberService.java +++ b/src/main/java/com/livable/server/member/service/MemberService.java @@ -2,13 +2,16 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.home.dto.HomeResponse; +import com.livable.server.home.dto.HomeResponse.AccessCardDto; import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.dto.AccessCardProjection; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; import com.livable.server.member.repository.MemberRepository; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Optional; @RequiredArgsConstructor @Service @@ -24,10 +27,21 @@ public MemberResponse.MyPageDTO getMyPageData(Long memberId) { return MemberResponse.MyPageDTO.from(myPageProjection); } - + + public HomeResponse.AccessCardDto getAccessCardData(Long memberId) { + + List accessCardProjectionOptional = memberRepository.findAccessCardData(memberId); + + if (accessCardProjectionOptional.isEmpty()) { + throw new GlobalRuntimeException(MemberErrorCode.RETRIEVE_ACCESSCARD_FAILED); + } + + return AccessCardDto.from(accessCardProjectionOptional.get(0)); + } + public HomeResponse.BuildingInfoDto getBuildingInfo(Long memberId) { - return memberRepository.findBuildingInfoByMemberId(memberId) - .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); + return memberRepository.findBuildingInfoByMemberId(memberId).orElseThrow(() + -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); } - + } diff --git a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java index f4f9482d..38224ca1 100644 --- a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java +++ b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java @@ -10,9 +10,12 @@ import com.livable.server.core.util.ActorType; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.TestConfig; +import com.livable.server.home.dto.HomeResponse.AccessCardDto; import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.dto.AccessCardProjection; import com.livable.server.member.service.MemberService; +import java.util.Date; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,8 +24,6 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; -import java.util.Date; - @Import(TestConfig.class) @WebMvcTest(HomeController.class) class HomeControllerTest { @@ -39,6 +40,7 @@ class HomeControllerTest { @DisplayName("SUCCESS : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") @Test void getHomeInfoSuccess() throws Exception { + // given Long memberId = 1L; String token = tokenProvider.createActorToken(ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); @@ -59,6 +61,7 @@ void getHomeInfoSuccess() throws Exception { @DisplayName("FAILED : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ - ์กฐํšŒ ์‹คํŒจ") @Test void getHomeInfoFailed() throws Exception { + // given String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); given(memberService.getBuildingInfo(anyLong())) @@ -73,4 +76,55 @@ void getHomeInfoFailed() throws Exception { .andExpect(jsonPath("$.message").value(MemberErrorCode.BUILDING_INFO_NOT_EXIST.getMessage())); } + @DisplayName("SUCCESS - ์ถœ์ž… ์นด๋“œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void getAccessCardSuccess() throws Exception { + + // given + String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; + String employeeNumber = "123456"; + String companyName = "OFFICE 01"; + String floor = "1์ธต"; + String roomNumber = "101ํ˜ธ" ; + String employeeName = "TestUser"; + + Long memberId = 1L; + String token = tokenProvider.createActorToken(ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); + + AccessCardProjection accessCardProjection = new AccessCardProjection(buildingName, employeeNumber, companyName, floor, roomNumber, employeeName); + + given(memberService.getAccessCardData(anyLong())) + .willReturn(AccessCardDto.from(accessCardProjection)); + + // when & then + mockMvc.perform(get("/api/access-card") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data['buildingName']").value(buildingName)) + .andExpect(jsonPath("$.data['employeeNumber']").value(employeeNumber)) + .andExpect(jsonPath("$.data['companyName']").value(companyName)) + .andExpect(jsonPath("$.data['floor']").value(floor)) + .andExpect(jsonPath("$.data['roomNumber']").value(roomNumber)) + .andExpect(jsonPath("$.data['employeeName']").value(employeeName)); + } + + @DisplayName("FAILED - ์ถœ์ž… ์นด๋“œ ์ •๋ณด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ - ์กฐํšŒ ์‹คํŒจ") + @Test + void getAccessCardFail() throws Exception { + + // given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + given(memberService.getAccessCardData(anyLong())) + .willThrow(new GlobalRuntimeException(MemberErrorCode.RETRIEVE_ACCESSCARD_FAILED)); + + // when & then + mockMvc.perform(get("/api/access-card") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(MemberErrorCode.RETRIEVE_ACCESSCARD_FAILED.getMessage())); + } + } diff --git a/src/test/java/com/livable/server/member/service/MemberServiceTest.java b/src/test/java/com/livable/server/member/service/MemberServiceTest.java index d39648d5..de2d03aa 100644 --- a/src/test/java/com/livable/server/member/service/MemberServiceTest.java +++ b/src/test/java/com/livable/server/member/service/MemberServiceTest.java @@ -2,15 +2,18 @@ import static com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.home.dto.HomeResponse.AccessCardDto; +import com.livable.server.member.dto.AccessCardProjection; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; import com.livable.server.member.repository.MemberRepository; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -76,46 +79,93 @@ void failure_Test_InvalidMember() { Assertions.assertThrows(GlobalRuntimeException.class, () -> memberService.getMyPageData(memberId)); } - - @DisplayName("Success - ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต") - @Test - void getHomeInfoSuccess() { - // given - Long memberId = 1L; - Long buildingId = 1L; - String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; - Boolean hasCafeteria = true; - - given(memberRepository.findBuildingInfoByMemberId(memberId)) - .willReturn(Optional.of(new BuildingInfoDto(buildingId, buildingName, hasCafeteria)) - ); - - // when - BuildingInfoDto actual = memberService.getBuildingInfo(memberId); - - // then - assertAll( - () -> assertEquals(buildingId, actual.getBuildingId()), - () -> assertEquals(buildingName, actual.getBuildingName()), - () -> assertEquals(hasCafeteria, actual.getHasCafeteria()) - ); + } - } + @DisplayName("Success - ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต") + @Test + void getHomeInfoSuccess() { + // given + Long memberId = 1L; + Long buildingId = 1L; + String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; + Boolean hasCafeteria = true; + + given(memberRepository.findBuildingInfoByMemberId(memberId)) + .willReturn(Optional.of(new BuildingInfoDto(buildingId, buildingName, hasCafeteria)) + ); - @DisplayName("FAILED : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต - ์œ ํšจํ•˜์ง€ ์•Š์€ ์ •๋ณด") - @Test - void getHomeInfoFailed() { - // given - Long memberId = 1L; + // when + BuildingInfoDto actual = memberService.getBuildingInfo(memberId); - // when - Mockito.when(memberRepository.findBuildingInfoByMemberId(anyLong())) - .thenReturn(Optional.empty()); + // then + assertAll( + () -> Assertions.assertEquals(buildingId, actual.getBuildingId()), + () -> Assertions.assertEquals(buildingName, actual.getBuildingName()), + () -> Assertions.assertEquals(hasCafeteria, actual.getHasCafeteria()) + ); - // then - assertThrows(GlobalRuntimeException.class, () -> - memberService.getBuildingInfo(memberId)); - } } - + + @DisplayName("FAILED : ํ™ˆ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ •๋ณด ์‘๋‹ต - ์œ ํšจ ํ•˜์ง€ ์•Š์€ ์ •๋ณด") + @Test + void getHomeInfoFailed() { + // given + Long memberId = 1L; + + // when + Mockito.when(memberRepository.findBuildingInfoByMemberId(anyLong())) + .thenReturn(Optional.empty()); + + // then + assertThrows(GlobalRuntimeException.class, () -> + memberService.getBuildingInfo(memberId)); + } + + @DisplayName("SUCCESS - ์ถœ์ž… ์นด๋“œ ์ •๋ณด ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getAccessCardSuccess() { + // given + String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; + String employeeNumber = "123456"; + String companyName = "OFFICE 01"; + String floor = "1์ธต"; + String roomNumber = "101ํ˜ธ" ; + String employeeName = "TestUser"; + + AccessCardProjection accessCardProjection = new AccessCardProjection(buildingName, employeeNumber, companyName, floor, roomNumber, employeeName); + List accessCardProjectionList = new ArrayList<>(); + accessCardProjectionList.add(accessCardProjection); + + given(memberRepository.findAccessCardData(anyLong())) + .willReturn(accessCardProjectionList); + + // when + AccessCardDto actual = memberService.getAccessCardData(anyLong()); + + // then + assertAll( + () -> Assertions.assertEquals(buildingName, actual.getBuildingName()), + () -> Assertions.assertEquals(employeeNumber, actual.getEmployeeNumber()), + () -> Assertions.assertEquals(companyName, actual.getCompanyName()), + () -> Assertions.assertEquals(floor, actual.getFloor()), + () -> Assertions.assertEquals(roomNumber, actual.getRoomNumber()), + () -> Assertions.assertEquals(employeeName, actual.getEmployeeName()) + ); + } + + @DisplayName("FAILED - ์ถœ์ž… ์นด๋“œ ์ •๋ณด ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ - ์กฐํšŒ ์‹คํŒจ") + @Test + void getAccessCardFail() { + // given + Long memberId = 1L; + + // when + Mockito.when(memberRepository.findAccessCardData(anyLong())) + .thenReturn(new ArrayList<>()); + + // then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + memberService.getAccessCardData(memberId)); + } + } From 3335ebf0f074fb4f8d78ee960abc3cdd240a6aec Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:03:41 +0900 Subject: [PATCH 043/111] =?UTF-8?q?=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix : ํ™ˆ ํ™”๋ฉด ์ •๋ณด ์‘๋‹ต ๋งคํ•‘ class ์ถ”๊ฐ€ (#42) * Fix : ํ™ˆ ํ™”๋ฉด ์ •๋ณด ์‘๋‹ต DTO ์ˆ˜์ • (#42) builder ์ถ”๊ฐ€ * Fix : ํ™ˆ ํ™”๋ฉด ์ •๋ณด ์ฟผ๋ฆฌ ์ˆ˜์ • (#42) ๋งคํ•‘ class ์ถ”๊ฐ€๋กœ ์ธํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ Convention ํ†ต์ผ * Fix : Service ์ˆ˜์ • ์‚ฌํ•ญ ๋ฐ˜์˜ (#42) * Fix : ์ฝ”๋“œ ์ˆ˜์ •์œผ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • (#42) * Refactor : JWT ์ ์šฉ (#88) JWT ํ† ํฐ ์ถ”๊ฐ€๋กœ ์ธํ•œ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ * Style : ํฌ๋งคํŒ… ์ˆ˜์ • (#88) style change * Modify : ๋จธ์ง€ ์ค‘ ๋ˆ„๋ฝ๋œ ์ฝ”๋“œ ์ถ”๊ฐ€ (#88) --- .../server/home/controller/HomeController.java | 1 + .../com/livable/server/home/dto/HomeResponse.java | 13 +++++++++++-- .../server/member/dto/BuildingInfoProjection.java | 14 ++++++++++++++ .../server/member/repository/MemberRepository.java | 8 ++++---- .../server/member/service/MemberService.java | 10 +++++++--- .../server/home/controller/HomeControllerTest.java | 6 +++++- .../server/member/service/MemberServiceTest.java | 5 ++++- 7 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java diff --git a/src/main/java/com/livable/server/home/controller/HomeController.java b/src/main/java/com/livable/server/home/controller/HomeController.java index 91f58c0e..df6c965c 100644 --- a/src/main/java/com/livable/server/home/controller/HomeController.java +++ b/src/main/java/com/livable/server/home/controller/HomeController.java @@ -25,6 +25,7 @@ public ResponseEntity> getHomeInfo(@LoginAc JwtTokenProvider.checkMemberToken(actor); Long memberId = actor.getId(); + BuildingInfoDto buildingInfoDto = memberService.getBuildingInfo(memberId); return ApiResponse.success(buildingInfoDto, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/home/dto/HomeResponse.java b/src/main/java/com/livable/server/home/dto/HomeResponse.java index e78a63b8..e1d47aa9 100644 --- a/src/main/java/com/livable/server/home/dto/HomeResponse.java +++ b/src/main/java/com/livable/server/home/dto/HomeResponse.java @@ -1,8 +1,8 @@ package com.livable.server.home.dto; import com.livable.server.member.dto.AccessCardProjection; +import com.livable.server.member.dto.BuildingInfoProjection; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,12 +11,21 @@ public class HomeResponse { @Getter - @AllArgsConstructor + @Builder public static class BuildingInfoDto { private Long buildingId; private String buildingName; private Boolean hasCafeteria; + + public static BuildingInfoDto from(BuildingInfoProjection buildingInfoProjection) { + return BuildingInfoDto.builder() + .buildingId(buildingInfoProjection.getBuildingId()) + .buildingName(buildingInfoProjection.getBuildingName()) + .hasCafeteria(buildingInfoProjection.getHasCafeteria()) + .build(); + } + } @Getter diff --git a/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java b/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java new file mode 100644 index 00000000..517abeb2 --- /dev/null +++ b/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java @@ -0,0 +1,14 @@ +package com.livable.server.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BuildingInfoProjection { + + Long buildingId; + String buildingName; + Boolean hasCafeteria; + +} diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java index db1c50b8..1a2bedf0 100644 --- a/src/main/java/com/livable/server/member/repository/MemberRepository.java +++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java @@ -1,8 +1,8 @@ package com.livable.server.member.repository; import com.livable.server.entity.Member; -import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.dto.AccessCardProjection; +import com.livable.server.member.dto.BuildingInfoProjection; import com.livable.server.member.dto.MyPageProjection; import java.util.List; import java.util.Optional; @@ -27,13 +27,13 @@ public interface MemberRepository extends JpaRepository { "WHERE m.id = :memberId") List findAccessCardData(@Param("memberId") Long memberId); - @Query("SELECT b.id AS buildingId, b.name AS buildingName, b.hasCafeteria AS hasCafeteria " + + @Query("SELECT new com.livable.server.member.dto.BuildingInfoProjection (b.id, b.name, b.hasCafeteria) " + "FROM Member m " + "JOIN Company c " + "ON m.company = c " + - "JOIN FETCH Building b " + + "JOIN Building b " + "ON c.building = b " + "WHERE m.id = :memberId") - Optional findBuildingInfoByMemberId(@Param("memberId") Long memberId); + Optional findBuildingInfoByMemberId(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java index 24ba0524..28f97b5f 100644 --- a/src/main/java/com/livable/server/member/service/MemberService.java +++ b/src/main/java/com/livable/server/member/service/MemberService.java @@ -5,6 +5,7 @@ import com.livable.server.home.dto.HomeResponse.AccessCardDto; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.dto.AccessCardProjection; +import com.livable.server.member.dto.BuildingInfoProjection; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; import com.livable.server.member.repository.MemberRepository; @@ -40,8 +41,11 @@ public HomeResponse.AccessCardDto getAccessCardData(Long memberId) { } public HomeResponse.BuildingInfoDto getBuildingInfo(Long memberId) { - return memberRepository.findBuildingInfoByMemberId(memberId).orElseThrow(() - -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); - } + Optional buildingInfoProjectionOptional = memberRepository.findBuildingInfoByMemberId(memberId); + BuildingInfoProjection buildingInfoProjection = buildingInfoProjectionOptional.orElseThrow(() + -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST)); + return HomeResponse.BuildingInfoDto.from(buildingInfoProjection); + } + } diff --git a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java index 38224ca1..3e6260b5 100644 --- a/src/test/java/com/livable/server/home/controller/HomeControllerTest.java +++ b/src/test/java/com/livable/server/home/controller/HomeControllerTest.java @@ -14,6 +14,7 @@ import com.livable.server.home.dto.HomeResponse.BuildingInfoDto; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.dto.AccessCardProjection; +import com.livable.server.member.dto.BuildingInfoProjection; import com.livable.server.member.service.MemberService; import java.util.Date; import org.junit.jupiter.api.DisplayName; @@ -44,8 +45,11 @@ void getHomeInfoSuccess() throws Exception { // given Long memberId = 1L; String token = tokenProvider.createActorToken(ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); + + BuildingInfoProjection buildingInfoProjection = new BuildingInfoProjection(1L, "ํ…Œ๋ผ ํƒ€์›Œ", true); + given(memberService.getBuildingInfo(memberId)) - .willReturn(new BuildingInfoDto(1L, "ํ…Œ๋ผ ํƒ€์›Œ", true)); + .willReturn(BuildingInfoDto.from(buildingInfoProjection)); // when & then mockMvc.perform( diff --git a/src/test/java/com/livable/server/member/service/MemberServiceTest.java b/src/test/java/com/livable/server/member/service/MemberServiceTest.java index de2d03aa..51e5d9aa 100644 --- a/src/test/java/com/livable/server/member/service/MemberServiceTest.java +++ b/src/test/java/com/livable/server/member/service/MemberServiceTest.java @@ -9,6 +9,7 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.home.dto.HomeResponse.AccessCardDto; import com.livable.server.member.dto.AccessCardProjection; +import com.livable.server.member.dto.BuildingInfoProjection; import com.livable.server.member.dto.MemberResponse; import com.livable.server.member.dto.MyPageProjection; import com.livable.server.member.repository.MemberRepository; @@ -90,8 +91,10 @@ void getHomeInfoSuccess() { String buildingName = "ํ…Œ๋ผ ํƒ€์›Œ"; Boolean hasCafeteria = true; + BuildingInfoProjection buildingInfoProjection = new BuildingInfoProjection(buildingId, buildingName, hasCafeteria); + given(memberRepository.findBuildingInfoByMemberId(memberId)) - .willReturn(Optional.of(new BuildingInfoDto(buildingId, buildingName, hasCafeteria)) + .willReturn(Optional.of(buildingInfoProjection) ); // when From 62f7fb87578fbfc52e5032b049f0d6a37ab416c1 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:05:11 +0900 Subject: [PATCH 044/111] =?UTF-8?q?=EB=A3=B0=EB=A0=9B=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC,=20=EB=A9=94=EB=89=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore : gitKeep ์‚ญ์ œ (#90) * Feat : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ DTO ๊ตฌํ˜„ (#90) * Feat : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Controller ๊ตฌํ˜„ (#90) * Feat : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Repository ๊ตฌํ˜„ (#90) * Feat : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Service ๊ตฌํ˜„ (#90) * Test : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Service Test ์ž‘์„ฑ (#90) * Test : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Controller Test ์ž‘์„ฑ (#90) * Typo : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ Controller ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ (#90) ์ž˜๋ชป๋œ ๋ณ€์ˆ˜๋ช… ์ˆ˜์ • * Refactor : ๋ฃฐ๋ › ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต์šฉ JWT ์ ์šฉ (#90) JWT ํ† ํฐ ์ถ”๊ฐ€๋กœ ์ธํ•œ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ * Fix : ์ฟผ๋ฆฌ ์ˆ˜์ • (#90) join ์ฟผ๋ฆฌ ์ˆ˜์ • --- .../livable/server/menu/controller/.gitkeep | 0 .../menu/controller/MenuController.java | 35 ++++++++ .../server/menu/domain/MenuErrorCode.java | 18 +++++ .../java/com/livable/server/menu/dto/.gitkeep | 0 .../livable/server/menu/dto/MenuResponse.java | 21 +++++ .../livable/server/menu/dto/RouletteMenu.java | 20 +++++ .../menu/dto/RouletteMenuProjection.java | 14 ++++ .../livable/server/menu/repository/.gitkeep | 0 .../menu/repository/MenuRepository.java | 30 +++++++ .../com/livable/server/menu/service/.gitkeep | 0 .../server/menu/service/MenuService.java | 71 ++++++++++++++++ .../menu/controller/MenuControllerTest.java | 80 ++++++++++++++++++ .../server/menu/service/MenuServiceTest.java | 81 +++++++++++++++++++ 13 files changed, 370 insertions(+) delete mode 100644 src/main/java/com/livable/server/menu/controller/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/controller/MenuController.java create mode 100644 src/main/java/com/livable/server/menu/domain/MenuErrorCode.java delete mode 100644 src/main/java/com/livable/server/menu/dto/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/dto/MenuResponse.java create mode 100644 src/main/java/com/livable/server/menu/dto/RouletteMenu.java create mode 100644 src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java delete mode 100644 src/main/java/com/livable/server/menu/repository/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/repository/MenuRepository.java delete mode 100644 src/main/java/com/livable/server/menu/service/.gitkeep create mode 100644 src/main/java/com/livable/server/menu/service/MenuService.java create mode 100644 src/test/java/com/livable/server/menu/controller/MenuControllerTest.java create mode 100644 src/test/java/com/livable/server/menu/service/MenuServiceTest.java diff --git a/src/main/java/com/livable/server/menu/controller/.gitkeep b/src/main/java/com/livable/server/menu/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java new file mode 100644 index 00000000..4e3c3990 --- /dev/null +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -0,0 +1,35 @@ +package com.livable.server.menu.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.response.ApiResponse.Success; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; +import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.service.MenuService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class MenuController { + + private final MenuService menuService; + + @GetMapping("/api/menus") + public ResponseEntity>> getRouletteMenus(@LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + List rouletteMenuDTOs = menuService.getRouletteMenus(memberId); + + return ApiResponse.success(rouletteMenuDTOs, HttpStatus.OK); + } + +} diff --git a/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java new file mode 100644 index 00000000..0788e875 --- /dev/null +++ b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java @@ -0,0 +1,18 @@ +package com.livable.server.menu.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum MenuErrorCode implements ErrorCode { + + RETRIEVE_ROULETTE_MENU_FAILED(HttpStatus.BAD_REQUEST, "๋ฃฐ๋ › ๋ฉ”๋‰ด ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; + +} diff --git a/src/main/java/com/livable/server/menu/dto/.gitkeep b/src/main/java/com/livable/server/menu/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/menu/dto/MenuResponse.java b/src/main/java/com/livable/server/menu/dto/MenuResponse.java new file mode 100644 index 00000000..1ba5ff92 --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/MenuResponse.java @@ -0,0 +1,21 @@ +package com.livable.server.menu.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MenuResponse { + + @Getter + @AllArgsConstructor + public static class RouletteMenuDTO { + + private String categoryName; + private List menus; + + } + +} diff --git a/src/main/java/com/livable/server/menu/dto/RouletteMenu.java b/src/main/java/com/livable/server/menu/dto/RouletteMenu.java new file mode 100644 index 00000000..ec8ab912 --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/RouletteMenu.java @@ -0,0 +1,20 @@ +package com.livable.server.menu.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RouletteMenu { + + private Long menuId; + private String name; + + public static RouletteMenu from(RouletteMenuProjection rouletteMenuProjection) { + return RouletteMenu.builder() + .menuId(rouletteMenuProjection.getMenuId()) + .name(rouletteMenuProjection.getName()) + .build(); + } + +} diff --git a/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java b/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java new file mode 100644 index 00000000..e1fa262c --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java @@ -0,0 +1,14 @@ +package com.livable.server.menu.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RouletteMenuProjection { + + Long menuId; + String name; + String menuCategoryName; + +} diff --git a/src/main/java/com/livable/server/menu/repository/.gitkeep b/src/main/java/com/livable/server/menu/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java new file mode 100644 index 00000000..6576fe13 --- /dev/null +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -0,0 +1,30 @@ +package com.livable.server.menu.repository; + +import com.livable.server.entity.Menu; +import com.livable.server.menu.dto.RouletteMenuProjection; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface MenuRepository extends JpaRepository { + + @Query("SELECT distinct new com.livable.server.menu.dto.RouletteMenuProjection(menu.id, menu.name, mc.name) " + + "FROM Member m " + + "JOIN Company c " + + "ON c.id = m.company.id " + + "JOIN BuildingRestaurantMap brm " + + "ON brm.building.id = c.building.id " + + "JOIN RestaurantMenuMap rmm " + + "ON brm.restaurant.id = rmm.restaurant.id " + + "JOIN Menu menu " + + "ON rmm.menu.id = menu.id " + + "JOIN MenuCategory mc " + + "ON menu.menuCategory.id = mc.id " + + "WHERE m.id = :memberId" + ) + List findRouletteMenus(@Param("memberId") Long memberId); + +} diff --git a/src/main/java/com/livable/server/menu/service/.gitkeep b/src/main/java/com/livable/server/menu/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java new file mode 100644 index 00000000..2203f385 --- /dev/null +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -0,0 +1,71 @@ +package com.livable.server.menu.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.dto.RouletteMenu; +import com.livable.server.menu.dto.RouletteMenuProjection; +import com.livable.server.menu.repository.MenuRepository; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Slf4j +public class MenuService { + + private final MenuRepository menuRepository; + + public List getRouletteMenus(Long memberId) { + + List rouletteMenuProjections = menuRepository.findRouletteMenus(memberId); + + isValidateRouletteMenus(rouletteMenuProjections); + + Map> rouletteMenuMap = getMenusGroupByMenuCategory(rouletteMenuProjections); + + return convertToDTO(rouletteMenuMap); + } + + private void isValidateRouletteMenus(List rouletteMenuProjections) { + if (rouletteMenuProjections.isEmpty()) { + throw new GlobalRuntimeException((MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED)); + } + } + + private Map> getMenusGroupByMenuCategory(List rouletteMenuProjections){ + + Map> menuGroupByMenuCategoryMap = new LinkedHashMap<>(); + + for (RouletteMenuProjection rouletteMenuProjection : rouletteMenuProjections) { + + String menuCategoryName = rouletteMenuProjection.getMenuCategoryName(); + + RouletteMenu rouletteMenus = RouletteMenu.from(rouletteMenuProjection); + + menuGroupByMenuCategoryMap.computeIfAbsent( + menuCategoryName, k -> new ArrayList<>()) + .add(rouletteMenus); + } + + return menuGroupByMenuCategoryMap; + + } + + private List convertToDTO(Map> menuGroupByMenuCategoryMap) { + + List rouletteMenuDTOS = new ArrayList<>(); + + menuGroupByMenuCategoryMap.forEach((key, value) -> + rouletteMenuDTOS.add(new RouletteMenuDTO(key, value)) + ); + + return rouletteMenuDTOS; + } + +} diff --git a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java new file mode 100644 index 00000000..5f03302c --- /dev/null +++ b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java @@ -0,0 +1,80 @@ +package com.livable.server.menu.controller; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; +import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.service.MenuService; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@Import(TestConfig.class) +@WebMvcTest(MenuController.class) +class MenuControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + JwtTokenProvider tokenProvider; + + @MockBean + MenuService menuService; + + @DisplayName("SUCCESS - ๋ฃฐ๋ ›์— ์‚ฌ์šฉ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void getRouletteMenusSuccess() throws Exception { + //given + String token = tokenProvider.createActorToken( + ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + List mockResponse = new ArrayList<>(); + + given(menuService.getRouletteMenus(anyLong())) + .willReturn(mockResponse); + + //when & then + mockMvc.perform( + get("/api/menus") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @DisplayName("FAIELD - ๋ฃฐ๋ ›์— ์‚ฌ์šฉ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void getRouletteMenusFailed() throws Exception { + //given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + List mockResponse = new ArrayList<>(); + + given(menuService.getRouletteMenus(anyLong())) + .willThrow(new GlobalRuntimeException(MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED)); + + //when & then + mockMvc.perform(get("/api/menus") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED.getMessage())); + } + +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java new file mode 100644 index 00000000..9e217b15 --- /dev/null +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -0,0 +1,81 @@ +package com.livable.server.menu.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.dto.RouletteMenu; +import com.livable.server.menu.dto.RouletteMenuProjection; +import com.livable.server.menu.repository.MenuRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MenuServiceTest { + + @InjectMocks + MenuService menuService; + + @Mock + MenuRepository menuRepository; + + @DisplayName("SUCCESS - ๋ฃฐ๋ ›์— ์‚ฌ์šฉ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getRouletteMenusSuccess() { + + //given + String categoryName = "์นดํ…Œ๊ณ ๋ฆฌ1"; + String menuName = "๋ฉ”๋‰ด1"; + + RouletteMenuProjection rouletteMenuProjection = new RouletteMenuProjection(1L, menuName, categoryName); + + List rouletteMenuProjections = new ArrayList<>(); + rouletteMenuProjections.add(rouletteMenuProjection); + + RouletteMenu rouletteMenu = RouletteMenu.from(rouletteMenuProjection); + + List result = new ArrayList<>(); + result.add(rouletteMenu); + + RouletteMenuDTO expected = new RouletteMenuDTO(categoryName, result); + + given(menuRepository.findRouletteMenus(anyLong())) + .willReturn(rouletteMenuProjections); + + //when + List actual = + menuService.getRouletteMenus(1L); + + //then + then(menuRepository).should(times(1)).findRouletteMenus(anyLong()); + assertAll( + () -> assertEquals(expected.getCategoryName(), actual.get(0).getCategoryName()), + () -> assertEquals(expected.getMenus().size(), actual.get(0).getMenus().size()) + ); + } + + @DisplayName("FAIELD - ๋ฃฐ๋ ›์— ์‚ฌ์šฉ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getRouletteMenusFailed() { + + //given + given(menuRepository.findRouletteMenus(anyLong())) + .willReturn(new ArrayList<>()); + + //when + assertThrows(GlobalRuntimeException.class, () -> + menuService.getRouletteMenus(1l)); + } +} \ No newline at end of file From 8f1db8f7e721ccebad44586bbafe9e3f08b05ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Tue, 26 Sep 2023 14:28:43 +0900 Subject: [PATCH 045/111] =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: QueryDSL ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ถ”์ƒํ™” ๋ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ณ€๊ฒฝ (#86) * Refactor: JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ QueryDSL ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์†๋ฐ›๋„๋ก ๋ณ€๊ฒฝ (#86) * Feat: ๋ณ€๊ฒฝ๋œ ์ตœ์‹  ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋งคํ•‘์‹œํ‚ฌ ํด๋ž˜์Šค ๊ตฌํ˜„ (#86) * Feat: ๋ณ€๊ฒฝ๋œ ์ตœ์‹  ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋งคํ•‘ ์„ค์ • ์ถ”๊ฐ€ (#86) * Refactor: ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ์ด๋ฏธ์ง€ ์ด๋ฆ„์—์„œ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ๊ตฌ๋ถ„์ž๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฌธ์ž๋ฅผ ์‚ญ์ œํ•˜๋„๋ก ๋ณ€๊ฒฝ (#86) * Feat: ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ URL์„ ํ•ฉ์นœ ๋ฌธ์ž์—ด์„ ๋ฆฌ์ŠคํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#86) * Feat: ๊ฐœ์„ ๋œ ๋ฆฌ๋ทฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#86) * Modify: ๋ณ€๊ฒฝ๋œ ํด๋ž˜์Šค์— ๋”ฐ๋ฅธ ์ฝ”๋“œ ๋ณ€๊ฒฝ (#86) * Test: ์ด๋ฏธ์ง€๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#86) * Test: ๊ธฐ๋Šฅ ๊ฐœ์„ ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • (#86) * Modify: reviewDescription์ด ์‘๋‹ต์— ๋ฐ˜์˜ ์•ˆ๋˜๋Š” ํ˜„์ƒ ์ˆ˜์ • (#86) --- .../server/core/util/ImageSeparator.java | 26 ++++++++ .../livable/server/core/util/S3Uploader.java | 5 +- .../livable/server/entity/ReviewImage.java | 21 +++++++ .../RestaurantReviewController.java | 10 +-- .../livable/server/review/dto/Projection.java | 25 +++++++- .../dto/RestaurantReviewProjection.java | 54 ++++++++++++++++ .../review/dto/RestaurantReviewResponse.java | 43 ++++++++----- .../review/repository/MyReviewRepository.java | 15 ++--- .../RestaurantReviewProjectionRepository.java | 59 ++++++++++++++++++ .../RestaurantReviewRepository.java | 7 ++- .../querydsl/MyReviewQueryDslRepository.java | 16 +++++ .../MyReviewQueryDslRepositoryImpl.java} | 4 +- .../RestaurantReviewQueryDslRepository.java} | 6 +- ...staurantReviewQueryDslRepositoryImpl.java} | 50 +-------------- .../service/RestaurantReviewService.java | 19 ++++-- .../server/core/util/ImageSeparatorTest.java | 62 +++++++++++++++++++ .../RestaurantReviewControllerTest.java | 34 +++++----- .../review/service/MyReviewServiceTest.java | 2 - .../service/RestaurantReviewServiceTest.java | 62 +++++++++++-------- 19 files changed, 383 insertions(+), 137 deletions(-) create mode 100644 src/main/java/com/livable/server/core/util/ImageSeparator.java create mode 100644 src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java create mode 100644 src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java create mode 100644 src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepository.java rename src/main/java/com/livable/server/review/repository/{MyReviewRepositoryImpl.java => querydsl/MyReviewQueryDslRepositoryImpl.java} (95%) rename src/main/java/com/livable/server/review/repository/{RestaurantReviewCustomRepository.java => querydsl/RestaurantReviewQueryDslRepository.java} (66%) rename src/main/java/com/livable/server/review/repository/{RestaurantReviewCustomRepositoryImpl.java => querydsl/RestaurantReviewQueryDslRepositoryImpl.java} (62%) create mode 100644 src/test/java/com/livable/server/core/util/ImageSeparatorTest.java diff --git a/src/main/java/com/livable/server/core/util/ImageSeparator.java b/src/main/java/com/livable/server/core/util/ImageSeparator.java new file mode 100644 index 00000000..0e6b92c9 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/ImageSeparator.java @@ -0,0 +1,26 @@ +package com.livable.server.core.util; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +@Component +public class ImageSeparator { + + public static final String IMAGE_SEPARATOR = ","; + + /** + * IMAGE_SEPARATOR๋กœ ์ด์–ด์ง„ ์ด๋ฏธ์ง€ url ๋ฌธ์ž์—ด์„ ๋ฆฌ์ŠคํŠธ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * @param concatenatedImageUrl + * @return ๋ถ„๋ฆฌ๋œ url ๋ฆฌ์ŠคํŠธ + */ + public List separateConcatenatedImages(String concatenatedImageUrl) { + if (Objects.isNull(concatenatedImageUrl)) { + return new ArrayList<>(); + } + return Arrays.asList(concatenatedImageUrl.split(IMAGE_SEPARATOR)); + } +} diff --git a/src/main/java/com/livable/server/core/util/S3Uploader.java b/src/main/java/com/livable/server/core/util/S3Uploader.java index 75d4a427..e9711606 100644 --- a/src/main/java/com/livable/server/core/util/S3Uploader.java +++ b/src/main/java/com/livable/server/core/util/S3Uploader.java @@ -37,8 +37,9 @@ public List saveFile(List files) throws IOException { public String saveFile(MultipartFile file) throws IOException { - final String originalFileName = file.getOriginalFilename(); - assert originalFileName != null; + String filename = file.getOriginalFilename(); + final String originalFileName = filename.replaceAll(ImageSeparator.IMAGE_SEPARATOR, ""); + final String fileExtension = getFileExtension(originalFileName); validationAllowedFileExtension(fileExtension); diff --git a/src/main/java/com/livable/server/entity/ReviewImage.java b/src/main/java/com/livable/server/entity/ReviewImage.java index 4af82a9a..a7791fa3 100644 --- a/src/main/java/com/livable/server/entity/ReviewImage.java +++ b/src/main/java/com/livable/server/entity/ReviewImage.java @@ -1,9 +1,30 @@ package com.livable.server.entity; +import com.livable.server.review.dto.RestaurantReviewProjection; import lombok.*; import javax.persistence.*; +import java.time.LocalDateTime; +@SqlResultSetMapping( + name = "RestaurantReviewListMapping", + classes = @ConstructorResult( + targetClass = RestaurantReviewProjection.class, + columns = { + @ColumnResult(name = "memberName", type = String.class), + @ColumnResult(name = "restaurantId", type = Long.class), + @ColumnResult(name = "restaurantName", type = String.class), + @ColumnResult(name = "reviewId", type = Long.class), + @ColumnResult(name = "reviewCreatedAt", type = LocalDateTime.class), + @ColumnResult(name = "reviewDescription", type = String.class), + @ColumnResult(name = "reviewTaste", type = String.class), + @ColumnResult(name = "reviewAmount", type = String.class), + @ColumnResult(name = "reviewService", type = String.class), + @ColumnResult(name = "reviewSpeed", type = String.class), + @ColumnResult(name = "images", type = String.class), + } + ) +) @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java index 774de36e..54c53121 100644 --- a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RequiredArgsConstructor @RestController @RequestMapping("/api/reviews") @@ -22,14 +24,14 @@ public class RestaurantReviewController { private final RestaurantReviewService restaurantReviewService; @GetMapping("/buildings/{buildingId}") - public ResponseEntity>> list( + public ResponseEntity>> list( @PathVariable Long buildingId, @PageableDefault Pageable pageable) { - Page list = - restaurantReviewService.getAllList(buildingId, pageable); + List allListForBuilding = + restaurantReviewService.getAllListForBuilding(buildingId, pageable); - return ApiResponse.success(list, HttpStatus.OK); + return ApiResponse.success(allListForBuilding, HttpStatus.OK); } @GetMapping("/menus/{menuId}") diff --git a/src/main/java/com/livable/server/review/dto/Projection.java b/src/main/java/com/livable/server/review/dto/Projection.java index 84b52acf..ad39ad33 100644 --- a/src/main/java/com/livable/server/review/dto/Projection.java +++ b/src/main/java/com/livable/server/review/dto/Projection.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; -@NoArgsConstructor(access = AccessLevel.PRIVATE) public class Projection { @Getter @@ -25,6 +24,28 @@ static public class RestaurantReview { private Evaluation reviewService; private Evaluation reviewSpeed; - private String reviewImg; + private String images; + } + + @Getter + @Builder + @AllArgsConstructor + public static class RestaurantReviewList { + + private Long memberId; + private String memberName; + + private Long restaurantId; + private String restaurantName; + + private Long reviewId; + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private String images; } } diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java new file mode 100644 index 00000000..525e65f9 --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java @@ -0,0 +1,54 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.Evaluation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class RestaurantReviewProjection { + + private String memberName; + + private Long restaurantId; + private String restaurantName; + + private Long reviewId; + private LocalDateTime reviewCreatedAt; + private String reviewDescription; + private Evaluation reviewTaste; + private Evaluation reviewAmount; + private Evaluation reviewService; + private Evaluation reviewSpeed; + + private String images; + + public RestaurantReviewProjection(String memberName, + Long restaurantId, + String restaurantName, + Long reviewId, + LocalDateTime reviewCreatedAt, + String reviewDescription, + String reviewTaste, + String reviewAmount, + String reviewService, + String reviewSpeed, + String images) { + + this.memberName = memberName; + this.restaurantId = restaurantId; + this.restaurantName = restaurantName; + this.reviewId = reviewId; + this.reviewCreatedAt = reviewCreatedAt; + this.reviewDescription = reviewDescription; + this.reviewTaste = Evaluation.valueOf(reviewTaste); + this.reviewAmount = Evaluation.valueOf(reviewAmount); + this.reviewService = Evaluation.valueOf(reviewService); + this.reviewSpeed = Evaluation.valueOf(reviewSpeed); + this.images = images; + } +} diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java index 28a58c34..f5161b83 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -1,5 +1,6 @@ package com.livable.server.review.dto; +import com.livable.server.core.util.ImageSeparator; import com.livable.server.entity.Evaluation; import lombok.*; @@ -11,24 +12,38 @@ public class RestaurantReviewResponse { @Getter @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class ListDTO { + public static class ListForBuildingDTO { - private Long reviewId; - private LocalDateTime reviewCreatedAt; - private String reviewDescription; + private final String memberName; - private Evaluation reviewTaste; - private Evaluation reviewAmount; - private Evaluation reviewService; - private Evaluation reviewSpeed; + private final Long restaurantId; + private final String restaurantName; - private Long restaurantId; - private String restaurantName; + private final Long reviewId; + private final LocalDateTime reviewCreatedAt; + private final String reviewDescription; + private final Evaluation reviewTaste; + private final Evaluation reviewAmount; + private final Evaluation reviewService; + private final Evaluation reviewSpeed; - private Long memberId; - private String memberName; + private List reviewImages; + + public static ListForBuildingDTO valueOf(RestaurantReviewProjection restaurantReviewList, ImageSeparator imageSeparator) { + return ListForBuildingDTO.builder() + .memberName(restaurantReviewList.getMemberName()) + .restaurantId(restaurantReviewList.getRestaurantId()) + .restaurantName(restaurantReviewList.getRestaurantName()) + .reviewId(restaurantReviewList.getReviewId()) + .reviewCreatedAt(restaurantReviewList.getReviewCreatedAt()) + .reviewDescription(restaurantReviewList.getReviewDescription()) + .reviewTaste(restaurantReviewList.getReviewTaste()) + .reviewAmount(restaurantReviewList.getReviewAmount()) + .reviewService(restaurantReviewList.getReviewService()) + .reviewSpeed(restaurantReviewList.getReviewSpeed()) + .reviewImages(imageSeparator.separateConcatenatedImages(restaurantReviewList.getImages())) + .build(); + } } @Getter diff --git a/src/main/java/com/livable/server/review/repository/MyReviewRepository.java b/src/main/java/com/livable/server/review/repository/MyReviewRepository.java index 42482a8e..f4e1dc15 100644 --- a/src/main/java/com/livable/server/review/repository/MyReviewRepository.java +++ b/src/main/java/com/livable/server/review/repository/MyReviewRepository.java @@ -1,16 +1,11 @@ package com.livable.server.review.repository; -import com.livable.server.review.dto.MyReviewProjection; +import com.livable.server.entity.Review; +import com.livable.server.review.repository.querydsl.MyReviewQueryDslRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository -public interface MyReviewRepository { - - List findRestaurantReviewByReviewId(Long reviewId, Long memberId); - - List findLunchBoxReviewByReviewId(Long reviewId, Long memberId); - - List findCafeteriaReviewByReviewId(Long reviewId, Long memberId); +public interface MyReviewRepository + extends JpaRepository, MyReviewQueryDslRepository { } diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java new file mode 100644 index 00000000..f0a495e4 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java @@ -0,0 +1,59 @@ +package com.livable.server.review.repository; + +import com.livable.server.core.util.ImageSeparator; +import com.livable.server.review.dto.RestaurantReviewProjection; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.List; + +@Repository +public class RestaurantReviewProjectionRepository { + + private static final String FIND_RESTAURANT_REVIEW_QUERY; + + static { + FIND_RESTAURANT_REVIEW_QUERY = "SELECT " + + "m.name AS memberName, " + + "res.id AS restaurantId, " + + "res.name AS restaurantName, " + + "r.id AS reviewId, " + + "r.created_at AS reviewCreatedAt, " + + "r.description AS reviewDescription, " + + "rr.taste AS reviewTaste, " + + "rr.amount AS reviewAmount, " + + "rr.service AS reviewService, " + + "rr.speed AS reviewSpeed, " + + "GROUP_CONCAT(ri.url SEPARATOR :separator) AS images " + + "FROM review r " + + "INNER JOIN restaurant_review rr ON r.id = rr.id " + + "INNER JOIN member m ON r.member_id = m.id " + + "INNER JOIN restaurant res ON rr.restaurant_id = res.id " + + "LEFT JOIN review_image ri ON ri.review_id = r.id " + + "WHERE rr.restaurant_id IN " + + "(SELECT restaurant_id " + + "FROM building_restaurant_map " + + "WHERE building_id = :buildingId) " + + "GROUP BY r.id, rr.id, m.id, res.id " + + "ORDER BY r.created_at DESC " + + "LIMIT :limit " + + "OFFSET :offset"; + } + + @PersistenceContext + private EntityManager entityManager; + + public List findRestaurantReviewProjectionByBuildingId(Long buildingId, Pageable pageable) { + + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_REVIEW_QUERY, "RestaurantReviewListMapping") + .setParameter("separator", ImageSeparator.IMAGE_SEPARATOR) + .setParameter("buildingId", buildingId) + .setParameter("limit", pageable.getPageSize()) + .setParameter("offset", pageable.getOffset()); + + return (List) query.getResultList(); + } +} diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java index 79425fca..60664d0e 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewRepository.java @@ -1,8 +1,11 @@ package com.livable.server.review.repository; import com.livable.server.entity.RestaurantReview; +import com.livable.server.review.repository.querydsl.RestaurantReviewQueryDslRepository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; -public interface RestaurantReviewRepository extends JpaRepository, RestaurantReviewCustomRepository { - +@Repository +public interface RestaurantReviewRepository + extends JpaRepository, RestaurantReviewQueryDslRepository { } diff --git a/src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepository.java b/src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepository.java new file mode 100644 index 00000000..52f48ebd --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepository.java @@ -0,0 +1,16 @@ +package com.livable.server.review.repository.querydsl; + +import com.livable.server.review.dto.MyReviewProjection; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MyReviewQueryDslRepository { + + List findRestaurantReviewByReviewId(Long reviewId, Long memberId); + + List findLunchBoxReviewByReviewId(Long reviewId, Long memberId); + + List findCafeteriaReviewByReviewId(Long reviewId, Long memberId); +} diff --git a/src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepositoryImpl.java similarity index 95% rename from src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java rename to src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepositoryImpl.java index 4f7eba34..66368333 100644 --- a/src/main/java/com/livable/server/review/repository/MyReviewRepositoryImpl.java +++ b/src/main/java/com/livable/server/review/repository/querydsl/MyReviewQueryDslRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.livable.server.review.repository; +package com.livable.server.review.repository.querydsl; import com.livable.server.review.dto.MyReviewProjection; import com.querydsl.core.types.Projections; @@ -17,7 +17,7 @@ @Component @RequiredArgsConstructor -public class MyReviewRepositoryImpl implements MyReviewRepository { +public class MyReviewQueryDslRepositoryImpl implements MyReviewQueryDslRepository { private final JPAQueryFactory jpaQueryFactory; diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java b/src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepository.java similarity index 66% rename from src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java rename to src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepository.java index 18ff1269..babf7e0c 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepository.java +++ b/src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepository.java @@ -1,4 +1,4 @@ -package com.livable.server.review.repository; +package com.livable.server.review.repository.querydsl; import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; @@ -7,9 +7,7 @@ import java.util.List; -public interface RestaurantReviewCustomRepository { - - Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable); +public interface RestaurantReviewQueryDslRepository { Page findRestaurantReviewByMenuId(Long menuId, Pageable pageable); diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java b/src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepositoryImpl.java similarity index 62% rename from src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java rename to src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepositoryImpl.java index c3c4a27b..2a83a015 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/review/repository/querydsl/RestaurantReviewQueryDslRepositoryImpl.java @@ -1,6 +1,5 @@ -package com.livable.server.review.repository; +package com.livable.server.review.repository.querydsl; -import com.livable.server.entity.*; import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.RestaurantReviewResponse; import com.querydsl.core.types.Projections; @@ -22,55 +21,10 @@ import static com.livable.server.entity.QReviewMenuMap.reviewMenuMap; @RequiredArgsConstructor -public class RestaurantReviewCustomRepositoryImpl implements RestaurantReviewCustomRepository { +public class RestaurantReviewQueryDslRepositoryImpl implements RestaurantReviewQueryDslRepository { private final JPAQueryFactory queryFactory; - @Override - public Page findRestaurantReviewByBuildingId(Long buildingId, Pageable pageable) { - - final QReview review = QReview.review; - final QRestaurantReview restaurantReview = QRestaurantReview.restaurantReview; - final QMember member = QMember.member; - final QRestaurant restaurant = QRestaurant.restaurant; - final QBuildingRestaurantMap buildingRestaurantMap = QBuildingRestaurantMap.buildingRestaurantMap; - - JPAQuery query = queryFactory - .select(Projections.constructor(RestaurantReviewResponse.ListDTO.class, - review.id, - review.createdAt, - review.description, - restaurantReview.taste, - restaurantReview.amount, - restaurantReview.service, - restaurantReview.speed, - restaurantReview.restaurant.id, - restaurant.name, - review.member.id, - member.name - )) - .from(review) - .innerJoin(restaurantReview).on(review.id.eq(restaurantReview.id)) - .innerJoin(member).on(review.member.id.eq(member.id)) - .innerJoin(restaurant).on(restaurantReview.restaurant.id.eq(restaurant.id)) - .where(restaurantReview.restaurant.id.in( - JPAExpressions - .select(buildingRestaurantMap.restaurant.id) - .from(buildingRestaurantMap) - .where(buildingRestaurantMap.building.id.eq(buildingId)) - )) - .orderBy(review.createdAt.desc()); - - List content = query - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetchJoin().fetch(); - - long total = query.fetchCount(); - - return new PageImpl<>(content, pageable, total); - } - @Override public Page findRestaurantReviewByMenuId(Long menuId, Pageable pageable) { diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java index 4f9432c2..bbd2ab94 100644 --- a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -1,10 +1,12 @@ package com.livable.server.review.service; -import com.livable.server.core.exception.ErrorCode; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ImageSeparator; import com.livable.server.review.domain.MyReviewErrorCode; import com.livable.server.review.dto.Projection; +import com.livable.server.review.dto.RestaurantReviewProjection; import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.repository.RestaurantReviewProjectionRepository; import com.livable.server.review.repository.RestaurantReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -20,10 +22,19 @@ public class RestaurantReviewService { private final RestaurantReviewRepository restaurantReviewRepository; + private final RestaurantReviewProjectionRepository restaurantProjectionRepository; + private final ImageSeparator imageSeparator; @Transactional(readOnly = true) - public Page getAllList(Long buildingId, Pageable pageable) { - return restaurantReviewRepository.findRestaurantReviewByBuildingId(buildingId, pageable); + public List getAllListForBuilding(Long buildingId, Pageable pageable) { + + List restaurantReviewLists = + restaurantProjectionRepository.findRestaurantReviewProjectionByBuildingId(buildingId, pageable); + + return restaurantReviewLists.stream() + .map(restaurantReviewList -> + RestaurantReviewResponse.ListForBuildingDTO.valueOf(restaurantReviewList, imageSeparator)) + .collect(Collectors.toList()); } @Transactional(readOnly = true) @@ -43,7 +54,7 @@ public RestaurantReviewResponse.DetailDTO getDetail(Long reviewId) { Projection.RestaurantReview restaurantReview = restaurantReviews.get(0); List reviewImages = restaurantReviews.stream() - .map(Projection.RestaurantReview::getReviewImg) + .map(Projection.RestaurantReview::getImages) .collect(Collectors.toList()); return RestaurantReviewResponse.DetailDTO.from(restaurantReview, reviewImages); diff --git a/src/test/java/com/livable/server/core/util/ImageSeparatorTest.java b/src/test/java/com/livable/server/core/util/ImageSeparatorTest.java new file mode 100644 index 00000000..01559be4 --- /dev/null +++ b/src/test/java/com/livable/server/core/util/ImageSeparatorTest.java @@ -0,0 +1,62 @@ +package com.livable.server.core.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ImageSeparatorTest { + + private ImageSeparator imageSeparator; + + @BeforeEach + void setUp() { + imageSeparator = new ImageSeparator(); + } + + @Test + void success_Test_GivenMultipleUrls() { + // Given + String images = "https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95%ED%83%95.jpg,https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95.jpg"; + + // When + List actual = imageSeparator.separateConcatenatedImages(images); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(2, actual.size()), + () -> Assertions.assertEquals("https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95%ED%83%95.jpg", actual.get(0)), + () -> Assertions.assertEquals("https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95.jpg", actual.get(1)) + ); + } + + @Test + void success_Test_GivenSingleUrls() { + // Given + String images = "https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95%ED%83%95.jpg"; + + // When + List actual = imageSeparator.separateConcatenatedImages(images); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(1, actual.size()), + () -> Assertions.assertEquals("https://livable-final.s3.ap-northeast-2.amazonaws.com/%EB%A7%88%EB%9D%BC%ED%83%95%ED%83%95.jpg", actual.get(0)) + ); + } + + @Test + void success_Test_GivenNull() { + // Given + String images = null; + + // When + List actual = imageSeparator.separateConcatenatedImages(images); + + // Then + Assertions.assertAll( + () -> Assertions.assertTrue(actual.isEmpty()) + ); + } +} diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index 49098365..035c26a1 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -44,24 +44,24 @@ void success_Test() throws Exception { // Given String uri = "/api/reviews/buildings/1"; - List mockList = List.of( - RestaurantReviewResponse.ListDTO.builder().reviewId(1L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(2L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(3L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(4L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(5L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(6L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(7L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(8L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(9L).build(), - RestaurantReviewResponse.ListDTO.builder().reviewId(10L).build() + List mockList = List.of( + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(1L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(2L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(3L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(4L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(5L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(6L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(7L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(8L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(9L).build(), + RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(10L).build() ); Pageable pageable = PageRequest.of(0, 10); - Page mockPage = new PageImpl<>(mockList, pageable, 1); - Mockito.when(restaurantReviewService - .getAllList(ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class))) - .thenReturn(mockPage); + Mockito.when(restaurantReviewService.getAllListForBuilding( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(Pageable.class)) + ).thenReturn(mockList); // When // Then @@ -69,8 +69,8 @@ void success_Test() throws Exception { .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) - .andExpect(MockMvcResultMatchers.jsonPath("$.data.content").isArray()) - .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); + .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.length()").value(10)); } } diff --git a/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java b/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java index ea702c26..de7846e9 100644 --- a/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/MyReviewServiceTest.java @@ -18,8 +18,6 @@ import java.util.ArrayList; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; - @ExtendWith(MockitoExtension.class) class MyReviewServiceTest { diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java index f6fcda53..3e68a55b 100644 --- a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -1,8 +1,11 @@ package com.livable.server.review.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ImageSeparator; import com.livable.server.review.dto.Projection; +import com.livable.server.review.dto.RestaurantReviewProjection; import com.livable.server.review.dto.RestaurantReviewResponse; +import com.livable.server.review.repository.RestaurantReviewProjectionRepository; import com.livable.server.review.repository.RestaurantReviewRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -22,7 +25,7 @@ import java.util.ArrayList; import java.util.List; -import static com.livable.server.review.dto.RestaurantReviewResponse.ListDTO; +import static com.livable.server.review.dto.RestaurantReviewResponse.*; import static com.livable.server.review.dto.RestaurantReviewResponse.ListForMenuDTO; @ExtendWith(MockitoExtension.class) @@ -31,12 +34,18 @@ class RestaurantReviewServiceTest { @Mock private RestaurantReviewRepository restaurantReviewRepository; + @Mock + private RestaurantReviewProjectionRepository restaurantReviewProjectionRepository; + + @Mock + private ImageSeparator imageSeparator; + @InjectMocks private RestaurantReviewService restaurantReviewService; @Nested @DisplayName("๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") - class list { + class listForBuilding { @DisplayName("์„ฑ๊ณต") @Test @@ -44,34 +53,35 @@ void success_Test() { // Given Long buildingId = 1L; - List mockList = List.of( - ListDTO.builder().reviewId(1L).build(), - ListDTO.builder().reviewId(2L).build(), - ListDTO.builder().reviewId(3L).build(), - ListDTO.builder().reviewId(4L).build(), - ListDTO.builder().reviewId(5L).build(), - ListDTO.builder().reviewId(6L).build(), - ListDTO.builder().reviewId(7L).build(), - ListDTO.builder().reviewId(8L).build(), - ListDTO.builder().reviewId(9L).build(), - ListDTO.builder().reviewId(10L).build() + List mockList = List.of( + RestaurantReviewProjection.builder().reviewId(1L).build(), + RestaurantReviewProjection.builder().reviewId(2L).build(), + RestaurantReviewProjection.builder().reviewId(3L).build(), + RestaurantReviewProjection.builder().reviewId(4L).build(), + RestaurantReviewProjection.builder().reviewId(5L).build(), + RestaurantReviewProjection.builder().reviewId(6L).build(), + RestaurantReviewProjection.builder().reviewId(7L).build(), + RestaurantReviewProjection.builder().reviewId(8L).build(), + RestaurantReviewProjection.builder().reviewId(9L).build(), + RestaurantReviewProjection.builder().reviewId(10L).build() ); Pageable pageable = PageRequest.of(0, 10); - Page mockPage = new PageImpl<>(mockList, pageable, 1); - Mockito.when(restaurantReviewRepository.findRestaurantReviewByBuildingId( + Mockito.when(imageSeparator.separateConcatenatedImages(null)) + .thenReturn(new ArrayList<>()); + + Mockito.when(restaurantReviewProjectionRepository.findRestaurantReviewProjectionByBuildingId( ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class) - )).thenReturn(mockPage); + )).thenReturn(mockList); // When - Page actual = - restaurantReviewService.getAllList(buildingId, pageable); + List actual = + restaurantReviewService.getAllListForBuilding(buildingId, pageable); // Then Assertions.assertAll( - () -> Assertions.assertEquals(10, actual.getSize()), - () -> Assertions.assertEquals(1, actual.getTotalPages()) + () -> Assertions.assertEquals(10, actual.size()) ); } } @@ -130,7 +140,7 @@ void success_Test_SingleImage() { List mockResult = List.of( Projection.RestaurantReview.builder() - .reviewImg("TestImages") + .images("TestImages") .reviewDescription("TestDescription") .build() ); @@ -138,7 +148,7 @@ void success_Test_SingleImage() { Mockito.when(restaurantReviewRepository.findRestaurantReviewById(ArgumentMatchers.anyLong())) .thenReturn(mockResult); // When - RestaurantReviewResponse.DetailDTO actual = restaurantReviewService.getDetail(reviewId); + DetailDTO actual = restaurantReviewService.getDetail(reviewId); // Then Assertions.assertAll( @@ -155,11 +165,11 @@ void success_Test_MultipleImage() { List mockResult = List.of( Projection.RestaurantReview.builder() - .reviewImg("TestImage1") + .images("TestImage1") .reviewDescription("TestDescription") .build(), Projection.RestaurantReview.builder() - .reviewImg("TestImage2") + .images("TestImage2") .reviewDescription("TestDescription") .build() ); @@ -167,7 +177,7 @@ void success_Test_MultipleImage() { Mockito.when(restaurantReviewRepository.findRestaurantReviewById(ArgumentMatchers.anyLong())) .thenReturn(mockResult); // When - RestaurantReviewResponse.DetailDTO actual = restaurantReviewService.getDetail(reviewId); + DetailDTO actual = restaurantReviewService.getDetail(reviewId); // Then Assertions.assertAll( @@ -191,4 +201,4 @@ void failure_Test_FailedConvertToDTO() { restaurantReviewService.getDetail(reviewId)); } } -} \ No newline at end of file +} From a84a3ab1af6c1b2ccb019d0c1a642e2b6ffa7c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Tue, 26 Sep 2023 15:54:19 +0900 Subject: [PATCH 046/111] =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=EC=9D=8C=EC=8B=9D?= =?UTF-8?q?=EC=A0=90=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„ (#103) * Feat: ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#103) * Feat: ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต DTO ๊ตฌํ˜„ (#103) * Feat: ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#103) * Test: ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#103) --- .../RestaurantReviewController.java | 11 +++++ .../review/dto/RestaurantReviewResponse.java | 36 +++++++++++++++ .../RestaurantReviewProjectionRepository.java | 42 +++++++++++++++-- .../service/RestaurantReviewService.java | 12 +++++ .../RestaurantReviewControllerTest.java | 45 +++++++++++++++++-- .../service/RestaurantReviewServiceTest.java | 43 ++++++++++++++++++ 6 files changed, 182 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java index 54c53121..1ced95e8 100644 --- a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -45,6 +45,17 @@ public ResponseEntity>> listForRestaurant( + @PathVariable Long restaurantId, + @PageableDefault Pageable pageable) { + + List allListForRestaurant = + restaurantReviewService.getAllListForRestaurant(restaurantId, pageable); + + return ApiResponse.success(allListForRestaurant, HttpStatus.OK); + } + @GetMapping("/{reviewId}") public ResponseEntity> detail(@PathVariable Long reviewId) { diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java index f5161b83..2ed7783d 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -46,6 +46,42 @@ public static ListForBuildingDTO valueOf(RestaurantReviewProjection restaurantRe } } + @Getter + @Builder + public static class ListForRestaurantDTO { + + private final String memberName; + + private final Long restaurantId; + private final String restaurantName; + + private final Long reviewId; + private final LocalDateTime reviewCreatedAt; + private final String reviewDescription; + private final Evaluation reviewTaste; + private final Evaluation reviewAmount; + private final Evaluation reviewService; + private final Evaluation reviewSpeed; + + private List reviewImages; + + public static ListForRestaurantDTO valueOf(RestaurantReviewProjection restaurantReviewList, ImageSeparator imageSeparator) { + return ListForRestaurantDTO.builder() + .memberName(restaurantReviewList.getMemberName()) + .restaurantId(restaurantReviewList.getRestaurantId()) + .restaurantName(restaurantReviewList.getRestaurantName()) + .reviewId(restaurantReviewList.getReviewId()) + .reviewCreatedAt(restaurantReviewList.getReviewCreatedAt()) + .reviewDescription(restaurantReviewList.getReviewDescription()) + .reviewTaste(restaurantReviewList.getReviewTaste()) + .reviewAmount(restaurantReviewList.getReviewAmount()) + .reviewService(restaurantReviewList.getReviewService()) + .reviewSpeed(restaurantReviewList.getReviewSpeed()) + .reviewImages(imageSeparator.separateConcatenatedImages(restaurantReviewList.getImages())) + .build(); + } + } + @Getter @Builder @NoArgsConstructor diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java index f0a495e4..f3b575e7 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java @@ -13,10 +13,12 @@ @Repository public class RestaurantReviewProjectionRepository { - private static final String FIND_RESTAURANT_REVIEW_QUERY; + private static final String FIND_RESTAURANT_REVIEW_BY_BUILDING_ID_QUERY; + private static final String FIND_RESTAURANT_REVIEW_BY_RESTAURANT_ID_QUERY; + static { - FIND_RESTAURANT_REVIEW_QUERY = "SELECT " + + FIND_RESTAURANT_REVIEW_BY_BUILDING_ID_QUERY = "SELECT " + "m.name AS memberName, " + "res.id AS restaurantId, " + "res.name AS restaurantName, " + @@ -41,6 +43,29 @@ public class RestaurantReviewProjectionRepository { "ORDER BY r.created_at DESC " + "LIMIT :limit " + "OFFSET :offset"; + + FIND_RESTAURANT_REVIEW_BY_RESTAURANT_ID_QUERY = "SELECT " + + "m.name AS memberName, " + + "res.id AS restaurantId, " + + "res.name AS restaurantName, " + + "r.id AS reviewId, " + + "r.created_at AS reviewCreatedAt, " + + "r.description AS reviewDescription, " + + "rr.taste AS reviewTaste, " + + "rr.amount AS reviewAmount, " + + "rr.service AS reviewService, " + + "rr.speed AS reviewSpeed, " + + "GROUP_CONCAT(ri.url SEPARATOR :separator) AS images " + + "FROM review r " + + "INNER JOIN restaurant_review rr ON r.id = rr.id " + + "INNER JOIN member m ON r.member_id = m.id " + + "INNER JOIN restaurant res ON rr.restaurant_id = res.id " + + "LEFT JOIN review_image ri ON ri.review_id = r.id " + + "WHERE rr.restaurant_id = :restaurantId " + + "GROUP BY r.id, rr.id, m.id, res.id " + + "ORDER BY r.created_at DESC " + + "LIMIT :limit " + + "OFFSET :offset"; } @PersistenceContext @@ -48,7 +73,7 @@ public class RestaurantReviewProjectionRepository { public List findRestaurantReviewProjectionByBuildingId(Long buildingId, Pageable pageable) { - Query query = entityManager.createNativeQuery(FIND_RESTAURANT_REVIEW_QUERY, "RestaurantReviewListMapping") + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_REVIEW_BY_BUILDING_ID_QUERY, "RestaurantReviewListMapping") .setParameter("separator", ImageSeparator.IMAGE_SEPARATOR) .setParameter("buildingId", buildingId) .setParameter("limit", pageable.getPageSize()) @@ -56,4 +81,15 @@ public List findRestaurantReviewProjectionByBuilding return (List) query.getResultList(); } + + public List findRestaurantReviewProjectionByRestaurantId(Long restaurantId, Pageable pageable) { + + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_REVIEW_BY_RESTAURANT_ID_QUERY, "RestaurantReviewListMapping") + .setParameter("separator", ImageSeparator.IMAGE_SEPARATOR) + .setParameter("restaurantId", restaurantId) + .setParameter("limit", pageable.getPageSize()) + .setParameter("offset", pageable.getOffset()); + + return (List) query.getResultList(); + } } diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java index bbd2ab94..7757cd1e 100644 --- a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -37,6 +37,18 @@ public List getAllListForBuilding(L .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public List getAllListForRestaurant(Long restaurantId, Pageable pageable) { + + List reviewProjections = + restaurantProjectionRepository.findRestaurantReviewProjectionByRestaurantId(restaurantId, pageable); + + return reviewProjections.stream() + .map(reviewProjection -> + RestaurantReviewResponse.ListForRestaurantDTO.valueOf(reviewProjection, imageSeparator)) + .collect(Collectors.toList()); + } + @Transactional(readOnly = true) public Page getAllListForMenu(Long menuId, Pageable pageable) { return restaurantReviewRepository.findRestaurantReviewByMenuId(menuId, pageable); diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index 035c26a1..9a06de8f 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -1,7 +1,6 @@ package com.livable.server.review.controller; import com.livable.server.core.util.TestConfig; -import com.livable.server.review.dto.MyReviewResponse; import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.service.RestaurantReviewService; import org.junit.jupiter.api.DisplayName; @@ -56,7 +55,6 @@ void success_Test() throws Exception { RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(9L).build(), RestaurantReviewResponse.ListForBuildingDTO.builder().reviewId(10L).build() ); - Pageable pageable = PageRequest.of(0, 10); Mockito.when(restaurantReviewService.getAllListForBuilding( ArgumentMatchers.anyLong(), @@ -66,7 +64,46 @@ void success_Test() throws Exception { // When // Then mockMvc.perform(MockMvcRequestBuilders.get(uri) - .accept(MediaType.APPLICATION_JSON)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.length()").value(10)); + } + } + + @Nested + @DisplayName("ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class listForRestaurant { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/reviews/restaurants/1"; + + List mockList = List.of( + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(1L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(2L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(3L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(4L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(6L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(5L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(7L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(8L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(9L).build(), + RestaurantReviewResponse.ListForRestaurantDTO.builder().reviewId(10L).build() + ); + + Mockito.when(restaurantReviewService.getAllListForRestaurant( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(mockList); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.get(uri) + .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray()) @@ -138,4 +175,4 @@ void success_Test() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); } } -} \ No newline at end of file +} diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java index 3e68a55b..79754b7a 100644 --- a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -86,6 +86,49 @@ void success_Test() { } } + @Nested + @DisplayName("ํŠน์ • ์Œ์‹์ ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class listForRestaurant { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() { + // Given + Long restaurantId = 1L; + + List mockList = List.of( + RestaurantReviewProjection.builder().reviewId(1L).build(), + RestaurantReviewProjection.builder().reviewId(2L).build(), + RestaurantReviewProjection.builder().reviewId(3L).build(), + RestaurantReviewProjection.builder().reviewId(4L).build(), + RestaurantReviewProjection.builder().reviewId(5L).build(), + RestaurantReviewProjection.builder().reviewId(6L).build(), + RestaurantReviewProjection.builder().reviewId(7L).build(), + RestaurantReviewProjection.builder().reviewId(8L).build(), + RestaurantReviewProjection.builder().reviewId(9L).build(), + RestaurantReviewProjection.builder().reviewId(10L).build() + ); + Pageable pageable = PageRequest.of(0, 10); + + Mockito.when(imageSeparator.separateConcatenatedImages(null)) + .thenReturn(new ArrayList<>()); + + Mockito.when(restaurantReviewProjectionRepository.findRestaurantReviewProjectionByRestaurantId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(mockList); + + // When + List actual = + restaurantReviewService.getAllListForRestaurant(restaurantId, pageable); + + // Then + Assertions.assertAll( + () -> Assertions.assertEquals(10, actual.size()) + ); + } + } + @Nested @DisplayName("ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class listForMenu { From e2be4fa0d289243fa23a5edce4f83d35563ef0ce Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Tue, 26 Sep 2023 20:08:34 +0900 Subject: [PATCH 047/111] =?UTF-8?q?=EB=B0=A9=EB=AC=B8=EC=A6=9D=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20callback=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ฐฉ๋ฌธ์ž ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ callback ์—”๋“œ ํฌ์ธํŠธ ๊ตฌํ˜„ (#97) * Feat: ๋ฐฉ๋ฌธ์ž ํ† ํฐ callback ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ Service ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#97) * Test: ๋ฐฉ๋ฌธ์ž callback Controller ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#97) * Test: ๋ฐฉ๋ฌธ์ž callback Service ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#97) --- .../InvitationValidationController.java | 32 +++++++++++ .../service/InvitationValidationService.java | 25 +++++++++ .../InvitationValidationControllerTest.java | 53 +++++++++++++++++++ .../InvitationValidationServiceTest.java | 43 +++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java create mode 100644 src/main/java/com/livable/server/invitation/service/InvitationValidationService.java create mode 100644 src/test/java/com/livable/server/invitation/controller/InvitationValidationControllerTest.java create mode 100644 src/test/java/com/livable/server/invitation/service/InvitationValidationServiceTest.java diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java new file mode 100644 index 00000000..b004fa76 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java @@ -0,0 +1,32 @@ +package com.livable.server.invitation.controller; + +import com.livable.server.invitation.service.InvitationValidationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.servlet.http.HttpServletResponse; + +@RequiredArgsConstructor +@Controller +public class InvitationValidationController { + + private static final String visitationPageUrl = "https://livable.vercel.app/invitation/view"; + + private final InvitationValidationService invitationValidationService; + + @GetMapping("api/invitation/callback") + public String validateVisitor(@RequestParam String token, HttpServletResponse response) { + + invitationValidationService.validateVisitor(token); + + response.setHeader("Authorization", createBearerToken(token)); + + return "redirect:" + visitationPageUrl; + } + + private String createBearerToken(String token) { + return "Bearer " + token; + } +} diff --git a/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java b/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java new file mode 100644 index 00000000..cf176e00 --- /dev/null +++ b/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java @@ -0,0 +1,25 @@ +package com.livable.server.invitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.member.domain.MemberErrorCode; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class InvitationValidationService { + + private final JwtTokenProvider tokenProvider; + + public void validateVisitor(String token) { + Claims claims = tokenProvider.parseClaims(token); + + String actorType = claims.get("actorType", String.class); + if (!actorType.equals(ActorType.VISITOR.name())) { + throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN); + } + } +} diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationValidationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationValidationControllerTest.java new file mode 100644 index 00000000..83f44b8d --- /dev/null +++ b/src/test/java/com/livable/server/invitation/controller/InvitationValidationControllerTest.java @@ -0,0 +1,53 @@ +package com.livable.server.invitation.controller; + +import com.livable.server.core.util.ActorType; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.TestConfig; +import com.livable.server.invitation.service.InvitationValidationService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.Date; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(TestConfig.class) +@WebMvcTest(InvitationValidationController.class) +class InvitationValidationControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + JwtTokenProvider tokenProvider; + + @MockBean + InvitationValidationService invitationValidationService; + + @DisplayName("[์„ฑ๊ณต] ๋ฐฉ๋ฌธ์ž Callback - ์ •์ƒ ์‘๋‹ต") + @Test + void validateVisitorSuccess_01() throws Exception { + // Given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(Long.MAX_VALUE)); + + // When + ResultActions resultActions = mockMvc + .perform( + get("/api/invitation/callback") + .param("token", token) + ); + + // Then + resultActions + .andExpect(status().isFound()) + .andExpect(header().string("Authorization", "Bearer " + token)); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/invitation/service/InvitationValidationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationValidationServiceTest.java new file mode 100644 index 00000000..bb081005 --- /dev/null +++ b/src/test/java/com/livable/server/invitation/service/InvitationValidationServiceTest.java @@ -0,0 +1,43 @@ +package com.livable.server.invitation.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.member.domain.MemberErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + + +@ExtendWith(MockitoExtension.class) +class InvitationValidationServiceTest { + + @Mock + JwtTokenProvider tokenProvider; + + @InjectMocks + InvitationValidationService invitationValidationService; + + @DisplayName("[์‹คํŒจ] ๋ฐฉ๋ฌธ์ž callback - ์ž˜๋ชป๋œ ํ† ํฐ์œผ๋กœ ์š”์ฒญํ•œ ๊ฒฝ์šฐ") + @Test + void validateVisitorFail_01() { + // Given + String token = "token"; + given(tokenProvider.parseClaims(anyString())) + .willThrow(new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN)); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationValidationService.validateVisitor(token)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(MemberErrorCode.INVALID_TOKEN); + } +} \ No newline at end of file From 91f94c549266a619a0b1450d35cf89ed07961583 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:08:44 +0900 Subject: [PATCH 048/111] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Admin ActorType ์ถ”๊ฐ€ (#93) * Feat: Visitation ๋™์  ์ฟผ๋ฆฌ ์ž‘์„ฑ (#93) * Feat: ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค ๊ตฌํ˜„ (#93) --- .../admin/controller/AdminController.java | 39 ++++++++ .../server/admin/domain/AdminErrorCode.java | 17 ++++ .../server/admin/domain/VisitationQuery.java | 43 ++++++++ .../domain/VisitationQueryCondition.java | 11 +++ .../server/admin/dto/AdminResponse.java | 73 ++++++++++++++ .../repository/AdminQueryRepository.java | 13 +++ .../repository/AdminQueryRepositoryImpl.java | 98 +++++++++++++++++++ .../admin/repository/AdminRepository.java | 8 ++ .../server/admin/service/AdminService.java | 44 +++++++++ .../livable/server/core/util/ActorType.java | 2 +- .../server/core/util/JwtTokenProvider.java | 23 ++--- .../java/com/livable/server/entity/Admin.java | 20 ++++ 12 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/livable/server/admin/controller/AdminController.java create mode 100644 src/main/java/com/livable/server/admin/domain/AdminErrorCode.java create mode 100644 src/main/java/com/livable/server/admin/domain/VisitationQuery.java create mode 100644 src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java create mode 100644 src/main/java/com/livable/server/admin/dto/AdminResponse.java create mode 100644 src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java create mode 100644 src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java create mode 100644 src/main/java/com/livable/server/admin/repository/AdminRepository.java create mode 100644 src/main/java/com/livable/server/admin/service/AdminService.java create mode 100644 src/main/java/com/livable/server/entity/Admin.java diff --git a/src/main/java/com/livable/server/admin/controller/AdminController.java b/src/main/java/com/livable/server/admin/controller/AdminController.java new file mode 100644 index 00000000..0eed85f5 --- /dev/null +++ b/src/main/java/com/livable/server/admin/controller/AdminController.java @@ -0,0 +1,39 @@ +package com.livable.server.admin.controller; + +import com.livable.server.admin.domain.VisitationQuery; +import com.livable.server.admin.service.AdminService; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class AdminController { + + private final AdminService adminService; + + /** + * http://localhost:8080/api/admin/visitation?page=10&size=1&queryCondition=COMPANY&query=sixsense&startDate=2023-09-24&endDate=2023-09-25 + * @param pageable + * @param visitationQueryCondition + */ + + @GetMapping("/visitation") + public ResponseEntity getVisitationList( + Pageable pageable, VisitationQuery visitationQueryCondition, @LoginActor Actor actor + ) { + + JwtTokenProvider.checkAdminToken(actor); + + Long adminId = actor.getId(); + + return adminService.getVisitationList(pageable, visitationQueryCondition, adminId); + } +} diff --git a/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java b/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java new file mode 100644 index 00000000..09599d2a --- /dev/null +++ b/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java @@ -0,0 +1,17 @@ +package com.livable.server.admin.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum AdminErrorCode implements ErrorCode { + NOT_EXIST_ADMIN(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ด€๋ฆฌ์ž ์ž…๋‹ˆ๋‹ค."), + INVALID_QUERY(HttpStatus.BAD_REQUEST, "๊ฒ€์ƒ‰ ์กฐ๊ฑด์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/admin/domain/VisitationQuery.java b/src/main/java/com/livable/server/admin/domain/VisitationQuery.java new file mode 100644 index 00000000..1bfe8c46 --- /dev/null +++ b/src/main/java/com/livable/server/admin/domain/VisitationQuery.java @@ -0,0 +1,43 @@ +package com.livable.server.admin.domain; + +import com.livable.server.core.exception.GlobalRuntimeException; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +@Setter +@ToString +public class VisitationQuery { + + private VisitationQueryCondition queryCondition; + private String query; + private LocalDate startDate; + private LocalDate endDate; + + public void setDefaultDate() { + this.startDate = LocalDate.now(); + this.endDate = LocalDate.now(); + } + + public void validate() { + if (startDate == null && endDate == null) { + setDefaultDate(); + return; + } + + if (startDate != null && endDate != null) { + checkDateTime(startDate, endDate); + } + } + + private void checkDateTime(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate) || endDate.isBefore(startDate)) { + throw new GlobalRuntimeException(AdminErrorCode.INVALID_QUERY); + } + } +} diff --git a/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java b/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java new file mode 100644 index 00000000..77385457 --- /dev/null +++ b/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java @@ -0,0 +1,11 @@ +package com.livable.server.admin.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum VisitationQueryCondition { + COMPANY, VISITOR; +} diff --git a/src/main/java/com/livable/server/admin/dto/AdminResponse.java b/src/main/java/com/livable/server/admin/dto/AdminResponse.java new file mode 100644 index 00000000..b63dd8c8 --- /dev/null +++ b/src/main/java/com/livable/server/admin/dto/AdminResponse.java @@ -0,0 +1,73 @@ +package com.livable.server.admin.dto; + +import lombok.*; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AdminResponse { + + @Getter + @Builder + public static class ListDTO { + private Long invitationId; + private String company; + private String host; + private Long visitorId; + private LocalDateTime startDateTime; + private LocalDateTime visitTime; + private String visitorName; + private String officeName; + private String carNumber; + private LocalDateTime inTime; + private LocalDateTime outTime; + private Integer stayTime; + } + + @Getter + @AllArgsConstructor + public static class ProjectionForListDTO { + private Long invitationId; + private String company; + private String host; + private Long visitorId; + private LocalDate startDate; + private LocalTime startTime; + private LocalDateTime visitTime; + private String visitorName; + private String officeName; + private String carNumber; + private LocalDateTime inTime; + private LocalDateTime outTime; + + public ListDTO toListDTO() { + return ListDTO.builder() + .invitationId(invitationId) + .company(company) + .host(host) + .visitorId(visitorId) + .startDateTime(LocalDateTime.of(startDate, startTime)) + .visitTime(visitTime) + .visitorName(visitorName) + .officeName(officeName) + .carNumber(carNumber) + .inTime(inTime) + .outTime(outTime) + .stayTime(calculateStayTime(inTime, outTime)) + .build(); + } + + private Integer calculateStayTime(LocalDateTime inTime, LocalDateTime outTime) { + if (inTime != null && outTime != null) { + Duration duration = Duration.between(inTime, outTime); + + return Long.valueOf(duration.getSeconds()).intValue() / 60; + } + + return null; + } + } +} diff --git a/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java b/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java new file mode 100644 index 00000000..630b4ed1 --- /dev/null +++ b/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java @@ -0,0 +1,13 @@ +package com.livable.server.admin.repository; + +import com.livable.server.admin.domain.VisitationQuery; +import com.livable.server.admin.dto.AdminResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface AdminQueryRepository { + + Page findVisitationWithQuery( + Pageable pageable, VisitationQuery visitationQuery, Long buildingId + ); +} diff --git a/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java b/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java new file mode 100644 index 00000000..5f677287 --- /dev/null +++ b/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java @@ -0,0 +1,98 @@ +package com.livable.server.admin.repository; + + +import com.livable.server.admin.domain.VisitationQuery; +import com.livable.server.admin.domain.VisitationQueryCondition; +import com.livable.server.admin.dto.AdminResponse; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.livable.server.entity.QCompany.company; +import static com.livable.server.entity.QParkingLog.parkingLog; +import static com.livable.server.entity.QVisitor.visitor; +import static com.livable.server.entity.QInvitation.invitation; +import static com.livable.server.entity.QMember.member; +import static com.livable.server.entity.QBuilding.building; + +@RequiredArgsConstructor +public class AdminQueryRepositoryImpl implements AdminQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findVisitationWithQuery( + Pageable pageable, VisitationQuery visitationQuery, Long buildingId + ) { + + BooleanBuilder dateTimeBuilder = new BooleanBuilder(); + + if (visitationQuery.getStartDate() != null) { + dateTimeBuilder.and(invitation.startDate.goe(visitationQuery.getStartDate())); + } + + if (visitationQuery.getEndDate() != null) { + dateTimeBuilder.and(invitation.startDate.loe(visitationQuery.getEndDate())); + } + + BooleanBuilder queryBuilder = new BooleanBuilder(); + + if (visitationQuery.getQueryCondition() != null + && visitationQuery.getQueryCondition().equals(VisitationQueryCondition.VISITOR) + && StringUtils.hasText(visitationQuery.getQuery())) { + queryBuilder.and(visitor.name.contains(visitationQuery.getQuery())); + } + + if (visitationQuery.getQueryCondition() != null + && visitationQuery.getQueryCondition().equals(VisitationQueryCondition.COMPANY) + && StringUtils.hasText(visitationQuery.getQuery())) { + queryBuilder.and(company.name.contains(visitationQuery.getQuery())); + } + + JPAQuery query = + queryFactory.select(Projections.constructor(AdminResponse.ProjectionForListDTO.class, + invitation.id, + company.name, + member.name, + visitor.id, + invitation.startDate, + invitation.startTime, + visitor.firstVisitedTime, + visitor.name, + invitation.officeName, + parkingLog.carNumber, + parkingLog.inTime, + parkingLog.outTime + )) + .from(visitor) + .leftJoin(parkingLog).on(parkingLog.visitor.id.eq(visitor.id)) + .join(invitation).on(dateTimeBuilder.and(visitor.invitation.id.eq(invitation.id))) + .join(member).on(invitation.member.id.eq(member.id)) + .join(company).on(member.company.id.eq(company.id)) + .join(building).on(company.building.id.eq(building.id), building.id.eq(buildingId)) + .where(queryBuilder) + .orderBy(invitation.startDate.asc()); + + List projection = query.offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List content = projection.stream() + .map(AdminResponse.ProjectionForListDTO::toListDTO) + .collect(Collectors.toList()); + + long count = query.fetchCount(); + + + return new PageImpl<>(content, pageable, count); + } +} diff --git a/src/main/java/com/livable/server/admin/repository/AdminRepository.java b/src/main/java/com/livable/server/admin/repository/AdminRepository.java new file mode 100644 index 00000000..c6cf0b3b --- /dev/null +++ b/src/main/java/com/livable/server/admin/repository/AdminRepository.java @@ -0,0 +1,8 @@ +package com.livable.server.admin.repository; + +import com.livable.server.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface AdminRepository extends JpaRepository, AdminQueryRepository { +} diff --git a/src/main/java/com/livable/server/admin/service/AdminService.java b/src/main/java/com/livable/server/admin/service/AdminService.java new file mode 100644 index 00000000..9c13a8fd --- /dev/null +++ b/src/main/java/com/livable/server/admin/service/AdminService.java @@ -0,0 +1,44 @@ +package com.livable.server.admin.service; + +import com.livable.server.admin.domain.AdminErrorCode; +import com.livable.server.admin.domain.VisitationQuery; +import com.livable.server.admin.dto.AdminResponse; +import com.livable.server.admin.repository.AdminRepository; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.response.ApiResponse; +import com.livable.server.entity.Admin; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class AdminService { + + private final AdminRepository adminRepository; + + @Transactional(readOnly = true) + public ResponseEntity getVisitationList( + Pageable pageable, VisitationQuery visitationQuery, Long adminId + ) { + visitationQuery.validate(); + /** + * ํ•„์š” ํ…Œ์ŠคํŠธ 1: + */ + + Optional optionalAdmin = adminRepository.findById(adminId); + Admin admin = optionalAdmin.orElseThrow( + () -> new GlobalRuntimeException(AdminErrorCode.NOT_EXIST_ADMIN)); + + Page responseBody + = adminRepository.findVisitationWithQuery(pageable, visitationQuery, admin.getBuilding().getId()); + + return ApiResponse.success(responseBody, HttpStatus.OK); + } +} diff --git a/src/main/java/com/livable/server/core/util/ActorType.java b/src/main/java/com/livable/server/core/util/ActorType.java index 0d0fa7ab..e3f89125 100644 --- a/src/main/java/com/livable/server/core/util/ActorType.java +++ b/src/main/java/com/livable/server/core/util/ActorType.java @@ -6,7 +6,7 @@ import java.util.Arrays; public enum ActorType { - MEMBER, VISITOR; + MEMBER, VISITOR, ADMIN; public static ActorType of(String type) { return Arrays.stream(values()) diff --git a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java index b3ab044a..2a44ad2d 100644 --- a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java +++ b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java @@ -2,7 +2,10 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.member.domain.MemberErrorCode; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -19,13 +22,6 @@ public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes()); } - /** - * Actor ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ - * @param actorType: ์—‘ํ„ฐ์˜ ์ข…๋ฅ˜ - * @param actorId: ์—‘ํ„ฐ์˜ ์‹๋ณ„๊ฐ’ - * @param expireDate: ํ† ํฐ์˜ ๋งŒ๋ฃŒ์ผ - * @return ์ž…๋ ฅ๋œ ์ •๋ณด๋กœ ๋งŒ๋“  ํ† ํฐ์„ ๋ฐ˜ํ™˜ - */ public String createActorToken(ActorType actorType, Long actorId, Date expireDate) { Claims claims = Jwts.claims(); @@ -39,11 +35,6 @@ public String createActorToken(ActorType actorType, Long actorId, Date expireDat .compact(); } - /** - * ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ - * @param token: JWT ํ† ํฐ ์ž…๋ ฅ - * @return ์ž…๋ ฅํ•œ ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด true, ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด false - */ public boolean isValidateToken(String token) { try { Jws claimsJws = Jwts.parserBuilder() @@ -82,4 +73,10 @@ public static void checkVisitorToken(Actor actor) { } } + public static void checkAdminToken(Actor actor) { + if (actor.getActorType() != ActorType.ADMIN) { + throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN); + } + } + } diff --git a/src/main/java/com/livable/server/entity/Admin.java b/src/main/java/com/livable/server/entity/Admin.java new file mode 100644 index 00000000..b9f16c99 --- /dev/null +++ b/src/main/java/com/livable/server/entity/Admin.java @@ -0,0 +1,20 @@ +package com.livable.server.entity; + +import lombok.Getter; + +import javax.persistence.*; + +@Getter +@Entity +public class Admin extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, unique = true) + private Building building; +} From 4a5ef37995332719603571c69a961cfa7c678a74 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:35:25 +0900 Subject: [PATCH 049/111] =?UTF-8?q?=EC=B2=98=EC=9D=8C=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?QR=EC=9D=84=20=EC=B0=8D=EC=97=88=EC=9D=84=20=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=A9=EB=AC=B8=20=EC=8B=9C=EA=B0=84=20Update=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Update QrCode ๋กœ์ง ๊ตฌํ˜„ (#104) * Refactor: ์ถ”๊ฐ€ ๋กœ์ง ๊ตฌํ˜„์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ˆ˜์ • (#104) --- src/main/java/com/livable/server/entity/Visitor.java | 4 ++++ .../visitation/controller/VisitationController.java | 6 ++++-- .../visitation/service/VisitationFacadeService.java | 3 ++- .../server/visitation/service/VisitorService.java | 10 ++++++++++ .../controller/VisitationControllerTest.java | 10 ++++++---- .../service/VisitationFacadeServiceTest.java | 2 +- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/livable/server/entity/Visitor.java b/src/main/java/com/livable/server/entity/Visitor.java index 9e397149..b117fca5 100644 --- a/src/main/java/com/livable/server/entity/Visitor.java +++ b/src/main/java/com/livable/server/entity/Visitor.java @@ -28,4 +28,8 @@ public class Visitor extends BaseTimeEntity { @Column private LocalDateTime firstVisitedTime; + + public void updateFirstVisitedTime() { + this.firstVisitedTime = LocalDateTime.now(); + } } diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index 2093d2a4..d1498fc8 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -46,10 +46,12 @@ public ResponseEntity> createQrCode(@LoginActor Acto @PostMapping("/qr") public ResponseEntity> validateQrCode( - @RequestBody @Valid VisitationRequest.ValidateQrCodeDto validateQrCodeDto + @RequestBody @Valid VisitationRequest.ValidateQrCodeDto validateQrCodeDto, + @LoginActor Actor actor ) { + JwtTokenProvider.checkVisitorToken(actor); - visitationFacadeService.validateQrCode(validateQrCodeDto.getQr()); + visitationFacadeService.validateQrCode(validateQrCodeDto.getQr(), actor.getId()); return ApiResponse.success(HttpStatus.OK); } diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 82acad20..ea76b465 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -29,8 +29,9 @@ public String createQrCode(final Long visitorId) { return visitationService.createQrCode(invitationTime.getStartDateTime(), invitationTime.getEndDateTime()); } - public void validateQrCode(final String qr) { + public void validateQrCode(final String qr, final Long visitorId) { visitationService.validateQrCode(qr); + visitorService.updateFirstEntranceTime(visitorId); } @Transactional diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java index 261d6863..5404f285 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitorService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -14,6 +14,16 @@ public class VisitorService { private final VisitorRepository visitorRepository; + public void updateFirstEntranceTime(final Long visitorId) { + + Visitor visitor = visitorRepository.findById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + + if (visitor.getFirstVisitedTime() == null) { + visitor.updateFirstVisitedTime(); + } + } + public Long findInvitationId(final Long visitorId) { return visitorRepository.findById(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)) diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 07bdf03b..4dd5ac9e 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -102,7 +102,7 @@ void validateQrCodeSuccess() throws Exception { String qr = "qr"; VisitationRequest.ValidateQrCodeDto validateQrCodeSuccessMockRequest = new MockValidateQrCodeDto(qr); - willDoNothing().given(visitationFacadeService).validateQrCode(anyString()); + willDoNothing().given(visitationFacadeService).validateQrCode(anyString(), anyLong()); // when ResultActions resultActions = mockMvc.perform( @@ -115,7 +115,7 @@ void validateQrCodeSuccess() throws Exception { // then resultActions.andExpect(status().isOk()); - then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); + then(visitationFacadeService).should(times(1)).validateQrCode(anyString(), anyLong()); } @DisplayName("[POST][/api/visitation/qr] - QR์ธ์ฆ ์„ฑ๊ณต") @@ -129,7 +129,9 @@ void validateQrCodeFail() throws Exception { VisitationRequest.ValidateQrCodeDto validateQrCodeSuccessMockRequest = new MockValidateQrCodeDto(qr); - willThrow(new GlobalRuntimeException(errorCode)).given(visitationFacadeService).validateQrCode(anyString()); + willThrow(new GlobalRuntimeException(errorCode)) + .given(visitationFacadeService) + .validateQrCode(anyString(), anyLong()); // when ResultActions resultActions = mockMvc.perform( @@ -144,7 +146,7 @@ void validateQrCodeFail() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(errorMessage)); - then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); + then(visitationFacadeService).should(times(1)).validateQrCode(anyString(), anyLong()); } @DisplayName("[POST][/api/visitation/parking] - ์ฐจ๋Ÿ‰ ๋“ฑ๋ก ์„ฑ๊ณต") diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 0cc6aae6..0eaac4b4 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -93,7 +93,7 @@ void validateQrCodeSuccessTest() { willDoNothing().given(visitationService).validateQrCode(anyString()); // When - visitationFacadeService.validateQrCode(QR_CODE); + visitationFacadeService.validateQrCode(QR_CODE, 1L); // Then then(visitationService).should(times(1)).validateQrCode(anyString()); From 3c644f0988b4a85d6b5b4bdf7f9e251482537ea2 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:54:33 +0900 Subject: [PATCH 050/111] =?UTF-8?q?Fix=20:=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#110)=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/menu/repository/MenuRepository.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index 6576fe13..2549ad86 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -11,19 +11,10 @@ @Repository public interface MenuRepository extends JpaRepository { - @Query("SELECT distinct new com.livable.server.menu.dto.RouletteMenuProjection(menu.id, menu.name, mc.name) " + - "FROM Member m " + - "JOIN Company c " + - "ON c.id = m.company.id " + - "JOIN BuildingRestaurantMap brm " + - "ON brm.building.id = c.building.id " + - "JOIN RestaurantMenuMap rmm " + - "ON brm.restaurant.id = rmm.restaurant.id " + - "JOIN Menu menu " + - "ON rmm.menu.id = menu.id " + - "JOIN MenuCategory mc " + - "ON menu.menuCategory.id = mc.id " + - "WHERE m.id = :memberId" + @Query("SELECT distinct new com.livable.server.menu.dto.RouletteMenuProjection(m.id, m.name, mc.name) " + + "FROM Menu m " + + "JOIN MenuCategory mc " + + "ON m.menuCategory.id = mc.id" ) List findRouletteMenus(@Param("memberId") Long memberId); From d8bd624a1b7986b3a994eaebb0d3e2e3f8ebeee8 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:13:39 +0900 Subject: [PATCH 051/111] =?UTF-8?q?Fix=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B3=80=EA=B2=BD=20(#113)=20(#115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ์ฟผ๋ฆฌ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ๋ถˆํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ --- .../livable/server/menu/controller/MenuController.java | 4 +--- .../livable/server/menu/repository/MenuRepository.java | 3 +-- .../com/livable/server/menu/service/MenuService.java | 4 ++-- .../server/menu/controller/MenuControllerTest.java | 4 ++-- .../livable/server/menu/service/MenuServiceTest.java | 10 +++++----- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index 4e3c3990..3c973b4a 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -25,9 +25,7 @@ public ResponseEntity>> getRouletteMenus(@LoginAct JwtTokenProvider.checkMemberToken(actor); - Long memberId = actor.getId(); - - List rouletteMenuDTOs = menuService.getRouletteMenus(memberId); + List rouletteMenuDTOs = menuService.getRouletteMenus(); return ApiResponse.success(rouletteMenuDTOs, HttpStatus.OK); } diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index 2549ad86..036a0db8 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -5,7 +5,6 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -16,6 +15,6 @@ public interface MenuRepository extends JpaRepository { "JOIN MenuCategory mc " + "ON m.menuCategory.id = mc.id" ) - List findRouletteMenus(@Param("memberId") Long memberId); + List findRouletteMenus(); } diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java index 2203f385..3b4ca41a 100644 --- a/src/main/java/com/livable/server/menu/service/MenuService.java +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -21,9 +21,9 @@ public class MenuService { private final MenuRepository menuRepository; - public List getRouletteMenus(Long memberId) { + public List getRouletteMenus() { - List rouletteMenuProjections = menuRepository.findRouletteMenus(memberId); + List rouletteMenuProjections = menuRepository.findRouletteMenus(); isValidateRouletteMenus(rouletteMenuProjections); diff --git a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java index 5f03302c..2d46e4d8 100644 --- a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java +++ b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java @@ -47,7 +47,7 @@ void getRouletteMenusSuccess() throws Exception { List mockResponse = new ArrayList<>(); - given(menuService.getRouletteMenus(anyLong())) + given(menuService.getRouletteMenus()) .willReturn(mockResponse); //when & then @@ -66,7 +66,7 @@ void getRouletteMenusFailed() throws Exception { List mockResponse = new ArrayList<>(); - given(menuService.getRouletteMenus(anyLong())) + given(menuService.getRouletteMenus()) .willThrow(new GlobalRuntimeException(MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED)); //when & then diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java index 9e217b15..51ca3a2c 100644 --- a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -51,15 +51,15 @@ void getRouletteMenusSuccess() { RouletteMenuDTO expected = new RouletteMenuDTO(categoryName, result); - given(menuRepository.findRouletteMenus(anyLong())) + given(menuRepository.findRouletteMenus()) .willReturn(rouletteMenuProjections); //when List actual = - menuService.getRouletteMenus(1L); + menuService.getRouletteMenus(); //then - then(menuRepository).should(times(1)).findRouletteMenus(anyLong()); + then(menuRepository).should(times(1)).findRouletteMenus(); assertAll( () -> assertEquals(expected.getCategoryName(), actual.get(0).getCategoryName()), () -> assertEquals(expected.getMenus().size(), actual.get(0).getMenus().size()) @@ -71,11 +71,11 @@ void getRouletteMenusSuccess() { void getRouletteMenusFailed() { //given - given(menuRepository.findRouletteMenus(anyLong())) + given(menuRepository.findRouletteMenus()) .willReturn(new ArrayList<>()); //when assertThrows(GlobalRuntimeException.class, () -> - menuService.getRouletteMenus(1l)); + menuService.getRouletteMenus()); } } \ No newline at end of file From 5f7185a0e4e9a3d5c0fe07dc4f691e7f946f74f9 Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:14:49 +0900 Subject: [PATCH 052/111] =?UTF-8?q?=EC=98=A4=EC=A0=90=EC=99=84=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=93=B0=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20S3uploader=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#50) * Feat: ๋ฆฌ๋ทฐ ENUM ์—๋Ÿฌ ์ฝ”๋“œ ํƒ€์ž… ๊ตฌํ˜„ (#50) * Feat: ๋ฆฌ๋ทฐ ์ด๋ฏธ์ง€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ƒ์„ฑ (#50) * Feat: ๋ฆฌ๋ทฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ƒ์„ฑ (#50) * Feat: ๋ฆฌ๋ทฐ ์š”์ฒญ DTO ๊ธฐ๋Šฅ๊ตฌํ˜„ (#50) * Feat: ๋ฆฌ๋ทฐ ENUM ์‹๋‹น ์„ ํƒ ์ข…๋ฅ˜ ํƒ€์ž… ๊ตฌํ˜„ (#50) * Feat: ๋ฆฌ๋ทฐ ๋„์‹œ๋ฝ, ๊ตฌ๋‚ด์‹๋‹น, ์™ธ๋ถ€๋ฐฐ๋‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#50) * add: s3uploader null, empty ํŒ๋‹จ ๋ถ„๊ธฐ ์ถ”๊ฐ€ (#112) --------- Co-authored-by: edan_cafe --- .../livable/server/core/util/S3Uploader.java | 12 +- .../review/controller/ReviewController.java | 66 ++++++++ .../server/review/domain/ReviewErrorCode.java | 29 ++++ .../review/domain/ReviewSelectType.java | 18 +++ .../server/review/dto/ReviewRequest.java | 85 ++++++++++ .../repository/ReviewImageRepository.java | 7 + .../review/repository/ReviewRepository.java | 7 + .../server/review/service/ReviewService.java | 150 ++++++++++++++++++ 8 files changed, 370 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/livable/server/review/controller/ReviewController.java create mode 100644 src/main/java/com/livable/server/review/domain/ReviewErrorCode.java create mode 100644 src/main/java/com/livable/server/review/domain/ReviewSelectType.java create mode 100644 src/main/java/com/livable/server/review/dto/ReviewRequest.java create mode 100644 src/main/java/com/livable/server/review/repository/ReviewImageRepository.java create mode 100644 src/main/java/com/livable/server/review/repository/ReviewRepository.java create mode 100644 src/main/java/com/livable/server/review/service/ReviewService.java diff --git a/src/main/java/com/livable/server/core/util/S3Uploader.java b/src/main/java/com/livable/server/core/util/S3Uploader.java index e9711606..9f95b371 100644 --- a/src/main/java/com/livable/server/core/util/S3Uploader.java +++ b/src/main/java/com/livable/server/core/util/S3Uploader.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; @RequiredArgsConstructor @@ -25,13 +26,16 @@ public class S3Uploader { private String bucket; public List saveFile(List files) throws IOException { + if (Objects.isNull(files) || files.isEmpty()) { + return new ArrayList<>(); + } List accessUrls = new ArrayList<>(); - for (MultipartFile file : files) { - String accessUrl = saveFile(file); - accessUrls.add(accessUrl); - } + for (MultipartFile file : files) { + String accessUrl = saveFile(file); + accessUrls.add(accessUrl); + } return accessUrls; } diff --git a/src/main/java/com/livable/server/review/controller/ReviewController.java b/src/main/java/com/livable/server/review/controller/ReviewController.java new file mode 100644 index 00000000..e0611cbd --- /dev/null +++ b/src/main/java/com/livable/server/review/controller/ReviewController.java @@ -0,0 +1,66 @@ +package com.livable.server.review.controller; + +import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.Actor; +import com.livable.server.core.util.JwtTokenProvider; +import com.livable.server.core.util.LoginActor; +import com.livable.server.review.dto.ReviewRequest; +import com.livable.server.review.service.ReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/reviews") +public class ReviewController { + + private final ReviewService reviewService; + + @PostMapping(value = "/lunch-box", consumes = "multipart/form-data") + public ResponseEntity createLunchBoxReview( + @Valid @RequestPart("data") ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, + @RequestPart(value = "imageFiles") List files + ) throws IOException { + + Long memberId = 1L; + + reviewService.createLunchBoxReview(lunchBoxCreateDTO, memberId, files); + + return ApiResponse.success(HttpStatus.CREATED); + } + + @PostMapping(value = "/cafeteria", consumes = "multipart/form-data") + public ResponseEntity createCafeteriaReview( + @Valid @RequestPart("data") ReviewRequest.CafeteriaCreateDTO CafeteriaCreateDTO, + @RequestPart(value = "imageFiles") List files + ) throws IOException { + + Long memberId = 1L; + + reviewService.createCafeteriaReview(CafeteriaCreateDTO, memberId, files); + + return ApiResponse.success(HttpStatus.CREATED); + } + + @PostMapping(value = "/restaurant", consumes = "multipart/form-data") + public ResponseEntity createRestaurantReview( + @Valid @RequestPart("data") ReviewRequest.RestaurantCreateDTO restaurantCreateDTO, + @RequestPart(value = "imageFiles") List files, + @LoginActor Actor actor + ) throws IOException { + + JwtTokenProvider.checkMemberToken(actor); + Long memberId = actor.getId(); + + reviewService.createRestaurantReview(restaurantCreateDTO, memberId, files); + + return ApiResponse.success(HttpStatus.CREATED); + } +} diff --git a/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java b/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java new file mode 100644 index 00000000..70727f01 --- /dev/null +++ b/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java @@ -0,0 +1,29 @@ +package com.livable.server.review.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ReviewErrorCode implements ErrorCode { + MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + RESTAURANT_NOT_EXITST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์Œ์‹์  ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + MENUS_NOT_CHOICE(HttpStatus.BAD_REQUEST, "ํ•˜๋‚˜ ์ด์ƒ์˜ ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”."); + + + private final HttpStatus httpStatus; + private final String message; + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/livable/server/review/domain/ReviewSelectType.java b/src/main/java/com/livable/server/review/domain/ReviewSelectType.java new file mode 100644 index 00000000..6432df70 --- /dev/null +++ b/src/main/java/com/livable/server/review/domain/ReviewSelectType.java @@ -0,0 +1,18 @@ +package com.livable.server.review.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ReviewSelectType { + LUNCH_BOX("๋„์‹œ๋ฝ"), + CAFETERIA("๊ตฌ๋‚ด์‹๋‹น"); + + private final String message; + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/livable/server/review/dto/ReviewRequest.java b/src/main/java/com/livable/server/review/dto/ReviewRequest.java new file mode 100644 index 00000000..f5c7ec1d --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/ReviewRequest.java @@ -0,0 +1,85 @@ +package com.livable.server.review.dto; + +import com.livable.server.entity.*; +import lombok.*; +import lombok.extern.jackson.Jacksonized; +import org.hibernate.annotations.ColumnDefault; + +import javax.validation.constraints.NotNull; + +import java.util.List; + +import static com.livable.server.entity.Evaluation.GOOD; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReviewRequest { + + @Getter + @Builder + public static class LunchBoxCreateDTO { + // JWT ํ† ํฐ ์˜ค๊ณ  => MemberId + @NotNull(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.") + private String description; + + + public LunchBoxReview toEntity(Member member, String selectedDishes) { + return LunchBoxReview.builder() + .member(member) + .description(description) + .selectedDishes(selectedDishes) + .build(); + } + } + + @Getter + @Builder + public static class CafeteriaCreateDTO { + + private Evaluation taste; + + @NotNull(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.") + private String description; + + public CafeteriaReview toEntity(Member member, Building building, String selectedDishes) { + return CafeteriaReview.builder() + .member(member) + .taste(taste) + .description(description) + .selectedDishes(selectedDishes) + .building(building) + .build(); + } + } + + @Getter + @Builder + public static class RestaurantCreateDTO { + + private Long restaurantId; + + @NotNull(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.") + private String description; + + private Evaluation taste; + private Evaluation amount; + private Evaluation speed; + private Evaluation service; + + private List

menus; + private List customMenus; + + + public RestaurantReview toEntity(Member member, Restaurant restaurant, String selectedDishes) { + return RestaurantReview.builder() + .member(member) + .restaurant(restaurant) + .taste(taste) + .amount(amount) + .speed(speed) + .service(service) + .description(description) + .selectedDishes(selectedDishes) + .build(); + } + } +} diff --git a/src/main/java/com/livable/server/review/repository/ReviewImageRepository.java b/src/main/java/com/livable/server/review/repository/ReviewImageRepository.java new file mode 100644 index 00000000..0e6de810 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/ReviewImageRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.review.repository; + +import com.livable.server.entity.ReviewImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/review/repository/ReviewRepository.java b/src/main/java/com/livable/server/review/repository/ReviewRepository.java new file mode 100644 index 00000000..42289267 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/ReviewRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.review.repository; + +import com.livable.server.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java new file mode 100644 index 00000000..eeea30f1 --- /dev/null +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -0,0 +1,150 @@ +package com.livable.server.review.service; + +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.S3Uploader; +import com.livable.server.entity.*; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.restaurant.repository.RestaurantRepository; +import com.livable.server.review.domain.ReviewErrorCode; +import com.livable.server.review.dto.ReviewRequest; +import com.livable.server.review.repository.ReviewImageRepository; +import com.livable.server.review.repository.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.livable.server.review.domain.ReviewSelectType.*; + +@RequiredArgsConstructor +@Service +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final ReviewImageRepository reviewImageRepository; + private final RestaurantRepository restaurantRepository; + private final S3Uploader s3Uploader; + + @Transactional + public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, Long memberId, List files) throws IOException { + Member member = findMemberById(memberId); + Review review = lunchBoxCreateDTO.toEntity(member, LUNCH_BOX.getMessage()); + reviewRepository.save(review); + + List images = s3Uploader.saveFile(files); + + if (!images.isEmpty()) { + // add point + + // register image + List reviewImages = saveImageFiles(review, images); + + reviewImageRepository.saveAll(reviewImages); + } + } + + @Transactional + public void createCafeteriaReview(ReviewRequest.CafeteriaCreateDTO cafeteriaCreateDTO, Long memberId, List files) throws IOException { + Member member = findMemberById(memberId); + Building building = getBuildingByMember(member); + Review review = cafeteriaCreateDTO.toEntity(member, building, CAFETERIA.getMessage()); + reviewRepository.save(review); + + List images = s3Uploader.saveFile(files); + + if (!images.isEmpty()) { + // add point + + // register image + List reviewImages = saveImageFiles(review, images); + + reviewImageRepository.saveAll(reviewImages); + } + } + + @Transactional + public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantCreateDTO, Long memberId, List files) throws IOException { + String selectedDishes = ""; + StringBuffer sb = new StringBuffer(); + + List menu = restaurantCreateDTO.getMenus(); + Long restaurantId = restaurantCreateDTO.getRestaurantId(); + List customMenu = restaurantCreateDTO.getCustomMenus(); + + Member member = findMemberById(memberId); + Restaurant restaurant = findRestaurantById(restaurantId); + + if (menu.isEmpty() && customMenu.isEmpty()) { + throw new GlobalRuntimeException(ReviewErrorCode.MENUS_NOT_CHOICE); + } + + + menu.forEach(el -> { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(el.getName()); + }); + + customMenu.forEach(el -> { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(el); + }); + + selectedDishes = sb.substring(0, sb.length()); + + Review review = restaurantCreateDTO.toEntity(member, restaurant, selectedDishes); + reviewRepository.save(review); + + + List images = s3Uploader.saveFile(files); + List reviewImages = saveImageFiles(review, images); + + reviewImageRepository.saveAll(reviewImages); + } + + private Restaurant findRestaurantById(Long restaurantId) { + Optional restaurantOptional = restaurantRepository.findById(restaurantId); + + return restaurantOptional.orElseThrow(() -> new GlobalRuntimeException(ReviewErrorCode.RESTAURANT_NOT_EXITST)); + } + + private Member findMemberById(Long memberid) { + Optional memberOptional = memberRepository.findById(memberid); + + return memberOptional.orElseThrow(() -> new GlobalRuntimeException(ReviewErrorCode.MEMBER_NOT_EXIST)); + } + + private Building getBuildingByMember(Member member) { + Company company = checkExistMemberById(member.getId()).getCompany(); + + return company.getBuilding(); + } + + private Member checkExistMemberById(Long memberId) { + Optional memberOptional = memberRepository.findById(memberId); + + return memberOptional.orElseThrow(() -> new GlobalRuntimeException(ReviewErrorCode.MEMBER_NOT_EXIST)); + } + + + private List saveImageFiles(Review review, List images) { + List reviewImages = images.stream().map(image -> + ReviewImage.builder() + .review(review) + .url(image) + .build() + ).collect(Collectors.toList()); + + return reviewImages; + } +} \ No newline at end of file From 336eeae89b54f1081c61cad3e64de6a880740fd2 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:34:58 +0900 Subject: [PATCH 053/111] =?UTF-8?q?Jacoco=20=EC=97=B0=EB=8F=99=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: jacoco build.gradle ์ถ”๊ฐ€ (#118) * Chore: lombok.config ์ถ”๊ฐ€ (#118) lombok ์–ด๋…ธํ…Œ์ด์…˜ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •์—์„œ ์ œ์™ธ --- .gitignore | 3 +++ build.gradle | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ lombok.config | 1 + 3 files changed, 78 insertions(+) create mode 100644 lombok.config diff --git a/.gitignore b/.gitignore index 34047078..6f2d0d36 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### jacoco ### +jacoco/ + ### STS ### .apt_generated .classpath diff --git a/build.gradle b/build.gradle index 9bdfc282..f2c6efff 100644 --- a/build.gradle +++ b/build.gradle @@ -6,12 +6,85 @@ buildscript { plugins { id 'java' + id 'jacoco' //jacoco id 'org.springframework.boot' version '2.7.15' id 'io.spring.dependency-management' version '1.0.15.RELEASE' id 'org.asciidoctor.jvm.convert' version '3.3.2' id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } +jacoco { + toolVersion = "0.8.10" +} + +/*jacoco setting start*/ +jacocoTestReport { + dependsOn test + reports { + html.required = true + xml.required = true + } + + def Qdomains = [] + for (qPattern in "**/QA".."**/QZ") { + Qdomains.add(qPattern + "*") + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, + exclude: [] + Qdomains + ) + })) + } + finalizedBy jacocoTestCoverageVerification +} + +jacocoTestCoverageVerification { + def Qdomains = [] + for (qPattern in "*.QA".."*.QZ") { + Qdomains.add(qPattern + "*") + } + violationRules { + rule { + limit { + minimum = 0.30 + } + } + + rule { + enabled = true + + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.40 + } + + limit { + counter = 'METHOD' + value = 'COVEREDRATIO' + minimum = 0.80 + } + + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.50 + } + + limit { + counter = 'LINE' + value = 'TOTALCOUNT' + minimum = 200 + } + + excludes = [] + Qdomains + } + } +} +/*jacoco setting end*/ + group = 'com.livable' version = '0.0.1-SNAPSHOT' @@ -57,6 +130,7 @@ dependencies { tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() + finalizedBy jacocoTestReport } tasks.named('asciidoctor') { diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..8f7e8aa1 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file From 924cb30b7d2590e85d0a6b1d4d7c913fbd41371c Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:35:10 +0900 Subject: [PATCH 054/111] =?UTF-8?q?Test=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test: VisitorService ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ๊ตฌํ˜„ (#120) * Test: QrCodeManager ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ๊ตฌํ˜„ (#120) --- .../visitation/domain/QrCodeManagerTest.java | 11 ++++ .../service/VisitorServiceTest.java | 56 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java index 87cbcde5..e2c30756 100644 --- a/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java +++ b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java @@ -71,4 +71,15 @@ void createQrCodeFailTest_2() throws JsonProcessingException { // Then assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_QR_PERIOD); } + + @DisplayName("QrCodeManager.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void validateQrCodeSuccessTest() { + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + + String qrCode = qrCodeManager.createQrCode(startDate, endDate); + + qrCodeManager.validateQrCode(qrCode); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java index 23267dc8..e462dd69 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java @@ -14,13 +14,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) @@ -109,4 +110,55 @@ void findVisitationDetailInformationByIdFailTest() { assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); then(visitorRepository).should(times(1)).findVisitationDetailInformationById(anyLong()); } + + @DisplayName("VisitorService.updateFirstEntranceTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void updateFirstEntranceTimeSuccessTest_1() { + // given + Visitor visitor = mock(Visitor.class); + given(visitorRepository.findById(anyLong())).willReturn(Optional.of(visitor)); + given(visitor.getFirstVisitedTime()).willReturn(null); + willDoNothing().given(visitor).updateFirstVisitedTime(); + + // when + visitorService.updateFirstEntranceTime(1L); + + // then + then(visitorRepository).should(times(1)).findById(anyLong()); + then(visitor).should(times(1)).getFirstVisitedTime(); + then(visitor).should(times(1)).updateFirstVisitedTime(); + } + + @DisplayName("VisitorService.updateFirstEntranceTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void updateFirstEntranceTimeSuccessTest_2() { + // given + Visitor visitor = mock(Visitor.class); + given(visitorRepository.findById(anyLong())).willReturn(Optional.of(visitor)); + given(visitor.getFirstVisitedTime()).willReturn(LocalDateTime.now()); + + // when + visitorService.updateFirstEntranceTime(1L); + + // then + then(visitorRepository).should(times(1)).findById(anyLong()); + then(visitor).should(times(1)).getFirstVisitedTime(); + then(visitor).should(times(0)).updateFirstVisitedTime(); + } + + @DisplayName("VisitorService.updateFirstEntranceTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void updateFirstEntranceTimeFailTest() { + // given + given(visitorRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when + GlobalRuntimeException globalRuntimeException = + assertThrows( + GlobalRuntimeException.class, () -> visitorService.updateFirstEntranceTime(1L) + ); + // then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(visitorRepository).should(times(1)).findById(anyLong()); + } } \ No newline at end of file From 962ae8f379f40094fc841c698bfa5ba828266fa1 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:36:48 +0900 Subject: [PATCH 055/111] =?UTF-8?q?Visitation=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=99=84=EB=A3=8C=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: QrCodeManager ์—ญํ•  ๋ถ„๋ฆฌ (#122) Encoder, Decoder ์ƒ์„ฑ * Refactor: QrCodeManager ์—ญํ•  ๋ถ„๋ฆฌ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง (#122) * Test: QrCodeManager ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#122) * Test: QrCodeEncoder ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#122) * Test: QrCodeDecoder ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#122) * Refactor: ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ˆ˜์ • (#122) * Refactor: ๋„๋ฉ”์ธ ๋‚ด๋ถ€ ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒ€์ฆ ์บก์Аํ™” (#122) * Test: VisitorService ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ๊ตฌํ˜„ (#122) * Refactor: ๋ฆฌํŒฉํ† ๋ง์œผ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์ˆ˜์ • (#122) * Test: Visitor ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#122) * Type: ๋ถˆํ•„์š”ํ•œ ์ฃผ์„ ์ œ๊ฑฐ (#122) * Refactor: ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๋กœ์ง ์บก์Аํ™” (#122) * Refactor: registerParkingLog ํ…Œ์ŠคํŠธ ๋ฆฌํŒฉํ„ฐ๋ง (#122) * Test: ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ์ •์  ๋ฉ”์„œํŠธ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#122) ParkingLog.create ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ --- build.gradle | 1 + .../server/admin/service/AdminService.java | 3 - .../com/livable/server/entity/ParkingLog.java | 7 ++ .../com/livable/server/entity/Visitor.java | 10 +- .../visitation/domain/QrCodeDecoder.java | 68 ++++++++++ .../visitation/domain/QrCodeEncoder.java | 87 +++++++++++++ .../visitation/domain/QrCodeManager.java | 119 +----------------- .../visitation/service/ParkingLogService.java | 5 +- .../service/VisitationFacadeService.java | 2 +- .../visitation/service/VisitorService.java | 6 +- .../livable/server/entity/ParkingLogTest.java | 24 ++++ .../livable/server/entity/VisitorTest.java | 42 +++++++ .../visitation/domain/QrCodeDecoderTest.java | 75 +++++++++++ .../visitation/domain/QrCodeEncoderTest.java | 76 +++++++++++ .../visitation/domain/QrCodeManagerTest.java | 106 +++++++++++++--- .../visitation/mock/MockParkingLog.java | 13 ++ .../server/visitation/mock/MockQrPayload.java | 26 ++++ .../service/ParkingLogServiceTest.java | 19 ++- .../service/VisitationFacadeServiceTest.java | 1 + .../service/VisitorServiceTest.java | 44 ++----- 20 files changed, 557 insertions(+), 177 deletions(-) create mode 100644 src/main/java/com/livable/server/visitation/domain/QrCodeDecoder.java create mode 100644 src/main/java/com/livable/server/visitation/domain/QrCodeEncoder.java create mode 100644 src/test/java/com/livable/server/entity/ParkingLogTest.java create mode 100644 src/test/java/com/livable/server/entity/VisitorTest.java create mode 100644 src/test/java/com/livable/server/visitation/domain/QrCodeDecoderTest.java create mode 100644 src/test/java/com/livable/server/visitation/domain/QrCodeEncoderTest.java create mode 100644 src/test/java/com/livable/server/visitation/mock/MockParkingLog.java create mode 100644 src/test/java/com/livable/server/visitation/mock/MockQrPayload.java diff --git a/build.gradle b/build.gradle index f2c6efff..0909bd96 100644 --- a/build.gradle +++ b/build.gradle @@ -118,6 +118,7 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'org.mockito:mockito-inline:3.6.0' implementation 'com.google.zxing:core:3.5.2' implementation 'com.google.zxing:javase:3.5.2' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' diff --git a/src/main/java/com/livable/server/admin/service/AdminService.java b/src/main/java/com/livable/server/admin/service/AdminService.java index 9c13a8fd..fe391850 100644 --- a/src/main/java/com/livable/server/admin/service/AdminService.java +++ b/src/main/java/com/livable/server/admin/service/AdminService.java @@ -28,9 +28,6 @@ public ResponseEntity getVisitationList( Pageable pageable, VisitationQuery visitationQuery, Long adminId ) { visitationQuery.validate(); - /** - * ํ•„์š” ํ…Œ์ŠคํŠธ 1: - */ Optional optionalAdmin = adminRepository.findById(adminId); Admin admin = optionalAdmin.orElseThrow( diff --git a/src/main/java/com/livable/server/entity/ParkingLog.java b/src/main/java/com/livable/server/entity/ParkingLog.java index 4bf5b9a7..efa86134 100644 --- a/src/main/java/com/livable/server/entity/ParkingLog.java +++ b/src/main/java/com/livable/server/entity/ParkingLog.java @@ -31,4 +31,11 @@ public class ParkingLog extends BaseTimeEntity { @Column private Integer stayTime; + + public static ParkingLog create(Visitor visitor, String carNumber) { + return ParkingLog.builder() + .carNumber(carNumber) + .visitor(visitor) + .build(); + } } diff --git a/src/main/java/com/livable/server/entity/Visitor.java b/src/main/java/com/livable/server/entity/Visitor.java index b117fca5..4f0f0661 100644 --- a/src/main/java/com/livable/server/entity/Visitor.java +++ b/src/main/java/com/livable/server/entity/Visitor.java @@ -29,7 +29,13 @@ public class Visitor extends BaseTimeEntity { @Column private LocalDateTime firstVisitedTime; - public void updateFirstVisitedTime() { - this.firstVisitedTime = LocalDateTime.now(); + public void entrance() { + if (isFirstEntrance()) { + this.firstVisitedTime = LocalDateTime.now(); + } + } + + private boolean isFirstEntrance() { + return this.firstVisitedTime == null; } } diff --git a/src/main/java/com/livable/server/visitation/domain/QrCodeDecoder.java b/src/main/java/com/livable/server/visitation/domain/QrCodeDecoder.java new file mode 100644 index 00000000..2029a801 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/QrCodeDecoder.java @@ -0,0 +1,68 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.zxing.*; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; +import com.livable.server.core.exception.GlobalRuntimeException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.EnumMap; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Component +public class QrCodeDecoder { + private static final String DEFAULT_CHARSET = "UTF-8"; + + private final ObjectMapper objectMapper; + + public QrPayload getQrPayload(final String base64QrCode) { + + Map hints = getDecodeHints(); + String decodeQrContent = getDecodeQrContent(base64QrCode, hints); + + try { + return objectMapper.readValue(decodeQrContent, QrPayload.class); + } catch (JsonProcessingException e) { + log.error("QrCodeManager.getQrPayload", e); + throw new GlobalRuntimeException(VisitationErrorCode.OBJECTMAPPER); + } + } + + private Map getDecodeHints() { + return new EnumMap<>(DecodeHintType.class) {{ + put(DecodeHintType.CHARACTER_SET, DEFAULT_CHARSET); + }}; + } + + private String getDecodeQrContent(final String base64QrCode, final Map hints) { + + Base64.Decoder decoder = Base64.getDecoder(); + byte[] imageBytes = decoder.decode(base64QrCode); + + try { + BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes)); + BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); + Result decode = new QRCodeReader().decode(binaryBitmap, hints); + + return decode.getText(); + } catch (IOException e) { + log.error("QrCodeManager.getDecodeQrContent", e); + throw new GlobalRuntimeException(VisitationErrorCode.IO); + } catch (ChecksumException | NotFoundException | FormatException e) { + log.error("QrCodeManager.getDecodeQrContent", e); + throw new GlobalRuntimeException(VisitationErrorCode.QR_DECODE); + } + } +} diff --git a/src/main/java/com/livable/server/visitation/domain/QrCodeEncoder.java b/src/main/java/com/livable/server/visitation/domain/QrCodeEncoder.java new file mode 100644 index 00000000..321287e0 --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/QrCodeEncoder.java @@ -0,0 +1,87 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.livable.server.core.exception.GlobalRuntimeException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Component +public class QrCodeEncoder { + + private static final int DEFAULT_WIDTH = 170; + private static final int DEFAULT_HEIGHT = 170; + private static final String EXPIRATION_START_DATE_KEY = "startDate"; + private static final String EXPIRATION_END_DATE_KEY = "endDate"; + private static final String DEFAULT_CHARSET = "UTF-8"; + private static final String DEFAULT_FORMAT = "png"; + + private final ObjectMapper objectMapper; + + public BufferedImage createQrCodeBufferdImage(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { + try { + + HashMap expirationPeriodMap = getExpirationPeriodMap(startDateTime, endDateTime); + String contents = objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(expirationPeriodMap); + + Map encodeHints = getEncodeHints(); + + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + + BitMatrix bitMatrix = qrCodeWriter.encode(contents, BarcodeFormat.QR_CODE, DEFAULT_WIDTH, DEFAULT_HEIGHT, encodeHints); + + return MatrixToImageWriter.toBufferedImage(bitMatrix); + } catch (JsonProcessingException | WriterException e) { + throw new RuntimeException(e); + } + } + + private HashMap getExpirationPeriodMap(final LocalDateTime startDate, final LocalDateTime endDate) { + return new HashMap<>() {{ + put(EXPIRATION_START_DATE_KEY, startDate); + put(EXPIRATION_END_DATE_KEY, endDate); + }}; + } + + private Map getEncodeHints() { + return new EnumMap<>(EncodeHintType.class) {{ + put(EncodeHintType.CHARACTER_SET, DEFAULT_CHARSET); + }}; + } + + public String encodeQrcodeToBase64(final BufferedImage bufferedImage) { + try { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + ImageIO.write(bufferedImage, DEFAULT_FORMAT, outputStream); + + byte[] imageBytes = outputStream.toByteArray(); + return Base64.getEncoder().encodeToString(imageBytes); + + } catch (IOException e) { + log.error("QrCodeManager.encodeQrcodeToBase64", e); + throw new GlobalRuntimeException(VisitationErrorCode.IO); + } + } +} diff --git a/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java index 849e89d5..c1eab816 100644 --- a/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java +++ b/src/main/java/com/livable/server/visitation/domain/QrCodeManager.java @@ -1,54 +1,31 @@ package com.livable.server.visitation.domain; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.zxing.*; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.qrcode.QRCodeReader; -import com.google.zxing.qrcode.QRCodeWriter; import com.livable.server.core.exception.GlobalRuntimeException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.time.LocalDateTime; -import java.util.Base64; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.Map; @Slf4j @RequiredArgsConstructor @Component public class QrCodeManager { - private static final int DEFAULT_WIDTH = 170; - private static final int DEFAULT_HEIGHT = 170; - private static final String DEFAULT_FORMAT = "png"; private static final int DEFAULT_QR_CODE_COLOR = 0xFFFFFFFF; private static final int DEFAULT_BACKGROUND_COLOR = 0xFF2563EA; - private static final String EXPIRATION_START_DATE_KEY = "startDate"; - private static final String EXPIRATION_END_DATE_KEY = "endDate"; - private static final String DEFAULT_CHARSET = "UTF-8"; - private final ObjectMapper objectMapper; + private final QrCodeEncoder qrCodeEncoder; + private final QrCodeDecoder qrCodeDecoder; public String createQrCode(final LocalDateTime startDate, final LocalDateTime endDate) { validatePeriod(startDate, endDate); - BufferedImage qrCode = createQrCodeImage(startDate, endDate); + BufferedImage qrCodeBufferdImage = qrCodeEncoder.createQrCodeBufferdImage(startDate, endDate); - return encodeQrcodeToBase64(qrCode); + return qrCodeEncoder.encodeQrcodeToBase64(qrCodeBufferdImage); } private void validatePeriod(final LocalDateTime startDate, final LocalDateTime endDate) { @@ -61,95 +38,9 @@ private void validatePeriod(final LocalDateTime startDate, final LocalDateTime e } } - private BufferedImage createQrCodeImage(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { - try { - - HashMap expirationPeriodMap = getExpirationPeriodMap(startDateTime, endDateTime); - String contents = objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(expirationPeriodMap); - - Map encodeHints = getEncodeHints(); - - QRCodeWriter qrCodeWriter = new QRCodeWriter(); - - BitMatrix bitMatrix = qrCodeWriter.encode(contents, BarcodeFormat.QR_CODE, DEFAULT_WIDTH, DEFAULT_HEIGHT, encodeHints); - - return MatrixToImageWriter.toBufferedImage(bitMatrix); - } catch (JsonProcessingException | WriterException e) { - throw new RuntimeException(e); - } - } - - private HashMap getExpirationPeriodMap(final LocalDateTime startDate, final LocalDateTime endDate) { - return new HashMap<>() {{ - put(EXPIRATION_START_DATE_KEY, startDate); - put(EXPIRATION_END_DATE_KEY, endDate); - }}; - } - - private Map getEncodeHints() { - return new EnumMap<>(EncodeHintType.class) {{ - put(EncodeHintType.CHARACTER_SET, DEFAULT_CHARSET); - }}; - } - - private String encodeQrcodeToBase64(final BufferedImage bufferedImage) { - try { - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - - ImageIO.write(bufferedImage, DEFAULT_FORMAT, outputStream); - - byte[] imageBytes = outputStream.toByteArray(); - return Base64.getEncoder().encodeToString(imageBytes); - - } catch (IOException e) { - log.error("QrCodeManager.encodeQrcodeToBase64", e); - throw new GlobalRuntimeException(VisitationErrorCode.IO); - } - } - public void validateQrCode(final String base64QrCode) { - QrPayload qrPayload = getQrPayload(base64QrCode); + QrPayload qrPayload = qrCodeDecoder.getQrPayload(base64QrCode); validatePeriod(qrPayload.getStartDate(), qrPayload.getEndDate()); } - - private QrPayload getQrPayload(final String base64QrCode) { - - Map hints = getDecodeHints(); - String decodeQrContent = getDecodeQrContent(base64QrCode, hints); - - try { - return objectMapper.readValue(decodeQrContent, QrPayload.class); - } catch (JsonProcessingException e) { - log.error("QrCodeManager.getQrPayload", e); - throw new GlobalRuntimeException(VisitationErrorCode.OBJECTMAPPER); - } - } - - private Map getDecodeHints() { - return new EnumMap<>(DecodeHintType.class) {{ - put(DecodeHintType.CHARACTER_SET, DEFAULT_CHARSET); - }}; - } - - private String getDecodeQrContent(final String base64QrCode, final Map hints) { - - Base64.Decoder decoder = Base64.getDecoder(); - byte[] imageBytes = decoder.decode(base64QrCode); - - try { - BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes)); - BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); - Result decode = new QRCodeReader().decode(binaryBitmap, hints); - - return decode.getText(); - } catch (IOException e) { - log.error("QrCodeManager.getDecodeQrContent", e); - throw new GlobalRuntimeException(VisitationErrorCode.IO); - } catch (ChecksumException | NotFoundException | FormatException e) { - log.error("QrCodeManager.getDecodeQrContent", e); - throw new GlobalRuntimeException(VisitationErrorCode.QR_DECODE); - } - } } diff --git a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java index 094f3740..3e1af5b4 100644 --- a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java +++ b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java @@ -19,10 +19,7 @@ public Optional findParkingLogByVisitorId(final Long visitorId) { } public void registerParkingLog(final Visitor visitor, final String carNumber) { - ParkingLog parkingLog = ParkingLog.builder() - .carNumber(carNumber) - .visitor(visitor) - .build(); + ParkingLog parkingLog = ParkingLog.create(visitor, carNumber); parkingLogRepository.save(parkingLog); } diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index ea76b465..bf1a51d8 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -31,7 +31,7 @@ public String createQrCode(final Long visitorId) { public void validateQrCode(final String qr, final Long visitorId) { visitationService.validateQrCode(qr); - visitorService.updateFirstEntranceTime(visitorId); + visitorService.doEntrance(visitorId); } @Transactional diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java index 5404f285..d9fe49ed 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitorService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -14,14 +14,12 @@ public class VisitorService { private final VisitorRepository visitorRepository; - public void updateFirstEntranceTime(final Long visitorId) { + public void doEntrance(final Long visitorId) { Visitor visitor = visitorRepository.findById(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); - if (visitor.getFirstVisitedTime() == null) { - visitor.updateFirstVisitedTime(); - } + visitor.entrance(); } public Long findInvitationId(final Long visitorId) { diff --git a/src/test/java/com/livable/server/entity/ParkingLogTest.java b/src/test/java/com/livable/server/entity/ParkingLogTest.java new file mode 100644 index 00000000..78b3883c --- /dev/null +++ b/src/test/java/com/livable/server/entity/ParkingLogTest.java @@ -0,0 +1,24 @@ +package com.livable.server.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ParkingLogTest { + + @DisplayName("ParkingLog.create ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void createSuccessTest() { + Visitor visitor = Visitor.builder() + .build(); + String carNumber = "12๊ฐ€3456"; + + ParkingLog parkingLog = ParkingLog.create(visitor, carNumber); + + assertThat(parkingLog.getVisitor()).isEqualTo(visitor); + assertThat(parkingLog.getCarNumber()).isEqualTo(carNumber); + } + +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/entity/VisitorTest.java b/src/test/java/com/livable/server/entity/VisitorTest.java new file mode 100644 index 00000000..3d5fc4ad --- /dev/null +++ b/src/test/java/com/livable/server/entity/VisitorTest.java @@ -0,0 +1,42 @@ +package com.livable.server.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class VisitorTest { + + @DisplayName("Visitor.entrance ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_1") + @Test + void entranceSuccessTest_1() { + // given + Visitor visitor = Visitor.builder() + .build(); + + // when + visitor.entrance(); + + // then + assertThat(visitor.getFirstVisitedTime()).isNotNull(); + } + + @DisplayName("Visitor.entrance ์„ฑ๊ณต ํ…Œ์ŠคํŠธ_1") + @Test + void entranceSuccessTest_2() { + // given + LocalDateTime now = LocalDateTime.now(); + Visitor visitor = Visitor.builder() + .firstVisitedTime(now) + .build(); + + // when + visitor.entrance(); + + // then + assertThat(visitor.getFirstVisitedTime()).isEqualTo(now); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/domain/QrCodeDecoderTest.java b/src/test/java/com/livable/server/visitation/domain/QrCodeDecoderTest.java new file mode 100644 index 00000000..cf4a139b --- /dev/null +++ b/src/test/java/com/livable/server/visitation/domain/QrCodeDecoderTest.java @@ -0,0 +1,75 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.visitation.mock.MockQrPayload; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class QrCodeDecoderTest { + + private static String qrCode; + + @InjectMocks + QrCodeDecoder qrCodeDecoder; + + @Mock + ObjectMapper objectMapper; + + @BeforeAll + static void beforeAll() { + QrCodeManager qrCodeManager = new QrCodeManager( + new QrCodeEncoder(new ObjectMapper()), new QrCodeDecoder(new ObjectMapper()) + ); + qrCode = qrCodeManager.createQrCode(LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1)); + } + + @DisplayName("QrCodeDecoder.getQrPayload ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void getQrPayloadSuccessTest() throws JsonProcessingException { + + // given + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + QrPayload qrPayload = new MockQrPayload(startDate, endDate); + given(objectMapper.readValue(anyString(), any(Class.class))).willReturn(qrPayload); + + // when + QrPayload result = qrCodeDecoder.getQrPayload(qrCode); + + + // then + assertThat(result).isEqualTo(qrPayload); + } + + @DisplayName("QrCodeDecoder.getQrPayload ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void getQrPayloadFailTest() throws JsonProcessingException { + + // given + given(objectMapper.readValue(anyString(), any(Class.class))).willThrow(JsonProcessingException.class); + + // when + GlobalRuntimeException globalRuntimeException = + assertThrows(GlobalRuntimeException.class, () -> qrCodeDecoder.getQrPayload(qrCode)); + + + // then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.OBJECTMAPPER); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/domain/QrCodeEncoderTest.java b/src/test/java/com/livable/server/visitation/domain/QrCodeEncoderTest.java new file mode 100644 index 00000000..6956851b --- /dev/null +++ b/src/test/java/com/livable/server/visitation/domain/QrCodeEncoderTest.java @@ -0,0 +1,76 @@ +package com.livable.server.visitation.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.awt.image.BufferedImage; +import java.time.LocalDateTime; +import java.util.HashMap; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class QrCodeEncoderTest { + + @InjectMocks + QrCodeEncoder qrCodeEncoder; + + @Spy + ObjectMapper objectMapper; + + @DisplayName("QrCodeEncoder.createQrCodeBufferdImage ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void createQrCodeBufferdImageSuceessTest() throws JsonProcessingException { + // given + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + BufferedImage bufferedImage = new BufferedImage(10, 10, 10); + MockedStatic imageWriter = mockStatic(MatrixToImageWriter.class); + given(MatrixToImageWriter.toBufferedImage(any(BitMatrix.class))).willReturn(bufferedImage); + + // when + BufferedImage qrCodeBufferdImage = qrCodeEncoder.createQrCodeBufferdImage(startDate, endDate); + + // then + assertThat(qrCodeBufferdImage).isEqualTo(bufferedImage); + then(objectMapper).should(times(1)).registerModule(any(JavaTimeModule.class)); + then(objectMapper).should(times(1)).writeValueAsString(any(HashMap.class)); + + imageWriter.close(); + } + + @DisplayName("QrCodeEncoder.createQrCodeBufferdImage ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void createQrCodeBufferdImageFailTest() throws JsonProcessingException { + // given + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + given(objectMapper.writeValueAsString(any(HashMap.class))).willThrow(JsonProcessingException.class); + + // when + RuntimeException runtimeException = + assertThrows(RuntimeException.class, () -> qrCodeEncoder.createQrCodeBufferdImage(startDate, endDate)); + + // then + assertThat(runtimeException.getStackTrace()).isNotNull(); + then(objectMapper).should(times(1)).registerModule(any(JavaTimeModule.class)); + then(objectMapper).should(times(1)).writeValueAsString(any(HashMap.class)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java index e2c30756..fb8a5049 100644 --- a/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java +++ b/src/test/java/com/livable/server/visitation/domain/QrCodeManagerTest.java @@ -1,20 +1,22 @@ package com.livable.server.visitation.domain; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.visitation.mock.MockQrPayload; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; -import org.mockito.Spy; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.awt.image.BufferedImage; import java.time.LocalDateTime; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; @@ -24,27 +26,36 @@ class QrCodeManagerTest { @InjectMocks QrCodeManager qrCodeManager; - @Spy - ObjectMapper objectMapper; + @Mock + QrCodeEncoder qrCodeEncoder; + + @Mock + QrCodeDecoder qrCodeDecoder; @DisplayName("QrCodeManager.createQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test - void createQrCodeSuccessTest() throws JsonProcessingException { + void createQrCodeSuccessTest() { // Given - // When + String qrCode = "QR_CODE"; LocalDateTime startDate = LocalDateTime.now().minusDays(1); LocalDateTime endDate = LocalDateTime.now().plusDays(1); - String qrCode = qrCodeManager.createQrCode(startDate, endDate); + BufferedImage bufferedImage = new BufferedImage(10, 10, 1); + given(qrCodeEncoder.createQrCodeBufferdImage(any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(bufferedImage); + given(qrCodeEncoder.encodeQrcodeToBase64(any(BufferedImage.class))).willReturn(qrCode); + + // When + String result = qrCodeManager.createQrCode(startDate, endDate); // Then - assertThat(qrCode).isNotNull(); - then(objectMapper).should(times(1)).writeValueAsString(any()); + assertThat(result).isEqualTo(qrCode); + then(qrCodeEncoder).should(times(1)).createQrCodeBufferdImage(any(LocalDateTime.class), any(LocalDateTime.class)); + then(qrCodeEncoder).should(times(1)).encodeQrcodeToBase64(any(BufferedImage.class)); } @DisplayName("QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_1") @Test - void createQrCodeFailTest_1() throws JsonProcessingException { + void createQrCodeFailTest_1() { // Given LocalDateTime startDate = LocalDateTime.now().plusDays(1); @@ -59,7 +70,7 @@ void createQrCodeFailTest_1() throws JsonProcessingException { @DisplayName("QrCodeManager.createQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_2") @Test - void createQrCodeFailTest_2() throws JsonProcessingException { + void createQrCodeFailTest_2() { // Given LocalDateTime startDate = LocalDateTime.now().minusDays(2); @@ -75,11 +86,78 @@ void createQrCodeFailTest_2() throws JsonProcessingException { @DisplayName("QrCodeManager.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void validateQrCodeSuccessTest() { + + // Given + String qrCode = "QR_CODE"; LocalDateTime startDate = LocalDateTime.now().minusDays(1); LocalDateTime endDate = LocalDateTime.now().plusDays(1); + QrPayload qrPayload = new MockQrPayload(startDate, endDate); + given(qrCodeDecoder.getQrPayload(anyString())).willReturn(qrPayload); - String qrCode = qrCodeManager.createQrCode(startDate, endDate); - + // When qrCodeManager.validateQrCode(qrCode); + + // Then + then(qrCodeDecoder).should(times(1)).getQrPayload(anyString()); + } + + @DisplayName("QrCodeManager.validateQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_1") + @Test + void validateQrCodeFailTest_1() { + + // Given + String qrCode = "QR_CODE"; + LocalDateTime startDate = LocalDateTime.now().plusDays(1); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + QrPayload qrPayload = new MockQrPayload(startDate, endDate); + given(qrCodeDecoder.getQrPayload(anyString())).willReturn(qrPayload); + + // When + GlobalRuntimeException globalRuntimeException = + assertThrows(GlobalRuntimeException.class, () -> qrCodeManager.validateQrCode(qrCode)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_PERIOD); + then(qrCodeDecoder).should(times(1)).getQrPayload(anyString()); + } + + @DisplayName("QrCodeManager.validateQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_2") + @Test + void validateQrCodeFailTest_2() { + + // Given + String qrCode = "QR_CODE"; + LocalDateTime startDate = LocalDateTime.now().minusDays(2); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + QrPayload qrPayload = new MockQrPayload(startDate, endDate); + given(qrCodeDecoder.getQrPayload(anyString())).willReturn(qrPayload); + + // When + GlobalRuntimeException globalRuntimeException = + assertThrows(GlobalRuntimeException.class, () -> qrCodeManager.validateQrCode(qrCode)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_QR_PERIOD); + then(qrCodeDecoder).should(times(1)).getQrPayload(anyString()); + } + + @DisplayName("QrCodeManager.validateQrCode ์‹คํŒจ ํ…Œ์ŠคํŠธ_3") + @Test + void validateQrCodeFailTest_3() { + + // Given + String qrCode = "QR_CODE"; + LocalDateTime startDate = LocalDateTime.now().minusDays(2); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + QrPayload qrPayload = new MockQrPayload(startDate, endDate); + given(qrCodeDecoder.getQrPayload(anyString())).willReturn(qrPayload); + + // When + GlobalRuntimeException globalRuntimeException = + assertThrows(GlobalRuntimeException.class, () -> qrCodeManager.validateQrCode(qrCode)); + + // Then + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.INVALID_QR_PERIOD); + then(qrCodeDecoder).should(times(1)).getQrPayload(anyString()); } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/mock/MockParkingLog.java b/src/test/java/com/livable/server/visitation/mock/MockParkingLog.java new file mode 100644 index 00000000..6f90e3df --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockParkingLog.java @@ -0,0 +1,13 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; + +import java.time.LocalDateTime; + +public class MockParkingLog extends ParkingLog { + + public MockParkingLog(Long id, Visitor visitor, String carNumber, LocalDateTime inTime, LocalDateTime outTime, Integer stayTime) { + super(id, visitor, carNumber, inTime, outTime, stayTime); + } +} diff --git a/src/test/java/com/livable/server/visitation/mock/MockQrPayload.java b/src/test/java/com/livable/server/visitation/mock/MockQrPayload.java new file mode 100644 index 00000000..11a7cf37 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockQrPayload.java @@ -0,0 +1,26 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.visitation.domain.QrPayload; + +import java.time.LocalDateTime; + +public class MockQrPayload extends QrPayload { + + private final LocalDateTime startDate; + private final LocalDateTime endDate; + + public MockQrPayload(LocalDateTime startDate, LocalDateTime endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + @Override + public LocalDateTime getStartDate() { + return startDate; + } + + @Override + public LocalDateTime getEndDate() { + return endDate; + } +} diff --git a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java index 9d573cf0..70dd9ab8 100644 --- a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java @@ -2,12 +2,14 @@ import com.livable.server.entity.ParkingLog; import com.livable.server.entity.Visitor; +import com.livable.server.visitation.mock.MockParkingLog; import com.livable.server.visitation.repository.ParkingLogRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; @@ -49,15 +51,20 @@ void findParkingLogByVisitorIdSuccessTest() { @DisplayName("ParkingLogService.registerParkingLog ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void registerParkingLogSuccessTest() { - ParkingLog parkingLog = ParkingLog.builder() - .build(); + String carNumber = "testCarNumber"; + + MockedStatic parkingLogMockedStatic = mockStatic(ParkingLog.class); Visitor visitor = Visitor.builder() .build(); - String carNumber = "testCarNumber"; + ParkingLog parkingLog = + new MockParkingLog( + null, visitor, carNumber, null, null, null + ); // Given + given(ParkingLog.create(any(Visitor.class), anyString())).willReturn(parkingLog); given(parkingLogRepository.save(any())).willReturn(parkingLog); // When @@ -65,5 +72,11 @@ void registerParkingLogSuccessTest() { // Then then(parkingLogRepository).should(times(1)).save(any()); + parkingLogMockedStatic.verify( + () -> ParkingLog.create(any(Visitor.class), anyString()), + times(1) + ); + + parkingLogMockedStatic.close(); } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 0eaac4b4..52d48073 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -91,6 +91,7 @@ void validateQrCodeSuccessTest() { // Given willDoNothing().given(visitationService).validateQrCode(anyString()); + willDoNothing().given(visitorService).doEntrance(anyLong()); // When visitationFacadeService.validateQrCode(QR_CODE, 1L); diff --git a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java index e462dd69..182c73da 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitorServiceTest.java @@ -21,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.*; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) @@ -111,54 +111,34 @@ void findVisitationDetailInformationByIdFailTest() { then(visitorRepository).should(times(1)).findVisitationDetailInformationById(anyLong()); } - @DisplayName("VisitorService.updateFirstEntranceTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @DisplayName("VisitorService.doEntrance ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test - void updateFirstEntranceTimeSuccessTest_1() { + void doEntranceSuccessTest() { // given - Visitor visitor = mock(Visitor.class); + Visitor visitor = spy(Visitor.class); given(visitorRepository.findById(anyLong())).willReturn(Optional.of(visitor)); - given(visitor.getFirstVisitedTime()).willReturn(null); - willDoNothing().given(visitor).updateFirstVisitedTime(); + willDoNothing().given(visitor).entrance(); // when - visitorService.updateFirstEntranceTime(1L); + visitorService.doEntrance(1L); // then then(visitorRepository).should(times(1)).findById(anyLong()); - then(visitor).should(times(1)).getFirstVisitedTime(); - then(visitor).should(times(1)).updateFirstVisitedTime(); + then(visitor).should(times(1)).entrance(); } - @DisplayName("VisitorService.updateFirstEntranceTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @DisplayName("VisitorService.doEntrance ์‹คํŒจ ํ…Œ์ŠคํŠธ") @Test - void updateFirstEntranceTimeSuccessTest_2() { - // given - Visitor visitor = mock(Visitor.class); - given(visitorRepository.findById(anyLong())).willReturn(Optional.of(visitor)); - given(visitor.getFirstVisitedTime()).willReturn(LocalDateTime.now()); - - // when - visitorService.updateFirstEntranceTime(1L); - - // then - then(visitorRepository).should(times(1)).findById(anyLong()); - then(visitor).should(times(1)).getFirstVisitedTime(); - then(visitor).should(times(0)).updateFirstVisitedTime(); - } - - @DisplayName("VisitorService.updateFirstEntranceTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") - @Test - void updateFirstEntranceTimeFailTest() { + void doEntranceFailTest() { // given given(visitorRepository.findById(anyLong())).willReturn(Optional.empty()); // when GlobalRuntimeException globalRuntimeException = - assertThrows( - GlobalRuntimeException.class, () -> visitorService.updateFirstEntranceTime(1L) - ); + assertThrows(GlobalRuntimeException.class, () -> visitorService.doEntrance(1L)); + // then - assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); then(visitorRepository).should(times(1)).findById(anyLong()); + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); } } \ No newline at end of file From e4f9cf0dccffad5a827ae4a322ad074d03f13b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Wed, 27 Sep 2023 19:25:16 +0900 Subject: [PATCH 056/111] =?UTF-8?q?=EB=AA=A9=ED=91=9C=20=EB=8B=AC=EC=84=B1?= =?UTF-8?q?=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A7=80=EA=B8=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„ (#106) * Feat: ํฌ์ธํŠธ ๋กœ๊ทธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ (#106) * Feat: ํฌ์ธํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#106) * Feat: ํฌ์ธํŠธ ์ง€๊ธ‰ ์‘๋‹ตDTO์™€ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋งคํ•‘DTO ๊ตฌํ˜„ (#106) * Feat: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ENUM ์ถ”๊ฐ€ (#106) * Feat: LocalDateTime์—์„œ ์ˆœ์ˆ˜ํ•œ LocalDate๋งŒ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#106) * Feat: ํฌ์ธํŠธ ์ฆ๊ฐ€ ์—”ํ‹ฐํ‹ฐ ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (#106) * Feat: PointCode์— 28์ผ์ฐจ ๋ชฉํ‘œ๋‹ฌ์„ฑ ์ฝ”๋“œ ์ถ”๊ฐ€ (#106) * Feat: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ ๊ตฌํ˜„ (#106) * Feat: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#106) --- .../java/com/livable/server/entity/Point.java | 4 + .../com/livable/server/entity/PointCode.java | 7 ++ .../point/controller/PointController.java | 11 +++ .../server/point/domain/DateFactory.java | 8 ++ .../server/point/domain/PointAchievement.java | 51 ++++++++++++ .../server/point/domain/PointErrorCode.java | 5 +- .../server/point/dto/PointProjection.java | 26 ++++++ .../server/point/dto/PointResponse.java | 10 +++ .../point/repository/PointLogRepository.java | 15 ++++ .../point/repository/PointRepository.java | 26 +++++- .../server/point/service/PointService.java | 81 ++++++++++++++++++- 11 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/livable/server/point/domain/PointAchievement.java create mode 100644 src/main/java/com/livable/server/point/dto/PointProjection.java create mode 100644 src/main/java/com/livable/server/point/repository/PointLogRepository.java diff --git a/src/main/java/com/livable/server/entity/Point.java b/src/main/java/com/livable/server/entity/Point.java index 1dfdf0a8..230aac55 100644 --- a/src/main/java/com/livable/server/entity/Point.java +++ b/src/main/java/com/livable/server/entity/Point.java @@ -21,4 +21,8 @@ public class Point extends BaseTimeEntity { @Column(nullable = false) private Integer balance; + + public void plusPoint(Integer amount) { + this.balance += amount; + } } diff --git a/src/main/java/com/livable/server/entity/PointCode.java b/src/main/java/com/livable/server/entity/PointCode.java index 3c419a72..3d9518f7 100644 --- a/src/main/java/com/livable/server/entity/PointCode.java +++ b/src/main/java/com/livable/server/entity/PointCode.java @@ -3,6 +3,8 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.List; + @Getter @AllArgsConstructor public enum PointCode { @@ -13,7 +15,12 @@ public enum PointCode { PA03("์˜ค์ ์™„ 7์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), PA04("์˜ค์ ์™„ 14์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), PA05("์˜ค์ ์™„ 21์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), + PA06("์˜ค์ ์™„ 28์ผ์ฐจ ๋‹ฌ์„ฑ ๋ณด์ƒ"), PM00("์ œํœด ์นดํŽ˜ ๋ฉ”๋‰ด ํ• ์ธ์— ๋Œ€ํ•œ ํฌ์ธํŠธ ์‚ฌ์šฉ"); private final String description; + + public static List getReviewPointCodes() { + return List.of(PA00, PA01, PA02); + } } diff --git a/src/main/java/com/livable/server/point/controller/PointController.java b/src/main/java/com/livable/server/point/controller/PointController.java index efa042f3..a2affe3a 100644 --- a/src/main/java/com/livable/server/point/controller/PointController.java +++ b/src/main/java/com/livable/server/point/controller/PointController.java @@ -10,6 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; @@ -31,4 +32,14 @@ public ResponseEntity> getMyRe PointResponse.ReviewCountDTO myReviewCount = pointService.getMyReviewCount(memberId, currentDate); return ApiResponse.success(myReviewCount, HttpStatus.OK); } + + @PostMapping("/api/points/logs/members") + public ResponseEntity> getAchievementPoint() { + + Long memberId = 1L; // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ + LocalDateTime requestDateTime = LocalDateTime.now(); + + pointService.getAchievementPoint(memberId, requestDateTime); + return ApiResponse.success(HttpStatus.CREATED); + } } diff --git a/src/main/java/com/livable/server/point/domain/DateFactory.java b/src/main/java/com/livable/server/point/domain/DateFactory.java index cbdde715..c526360c 100644 --- a/src/main/java/com/livable/server/point/domain/DateFactory.java +++ b/src/main/java/com/livable/server/point/domain/DateFactory.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; @@ -10,6 +11,7 @@ public class DateFactory { /** * ๊ธฐ์ค€์ด ๋˜๋Š” ๋‚ ์งœ ์ •๋ณด๋ฅผ ๋ฐ›์•„ ํ•ด๋‹น month์˜ ์‹œ์ž‘๊ณผ ๋ ๋ฒ”์œ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * * @param localDateTime * @return ํ•œ๋‹ฌ ๋ฒ”์œ„์˜ ์‹œ์ž‘๊ณผ ๋ ๋‚ ์งœ ๋ฐ์ดํ„ฐ */ @@ -20,4 +22,10 @@ public DateRange getMonthRangeOf(LocalDateTime localDateTime) { return new DateRange(startDate, endDate); } + + public LocalDate getPureDate(LocalDateTime localDateTime) { + return LocalDate.of(localDateTime.getYear(), + localDateTime.getMonth(), + localDateTime.getDayOfMonth()); + } } diff --git a/src/main/java/com/livable/server/point/domain/PointAchievement.java b/src/main/java/com/livable/server/point/domain/PointAchievement.java new file mode 100644 index 00000000..138108a3 --- /dev/null +++ b/src/main/java/com/livable/server/point/domain/PointAchievement.java @@ -0,0 +1,51 @@ +package com.livable.server.point.domain; + +import com.livable.server.entity.PointCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.InputMismatchException; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@AllArgsConstructor +public enum PointAchievement { + + DAY07(7, 100, PointCode.PA03), + DAY14(14, 100, PointCode.PA04), + DAY21(21, 100, PointCode.PA05), + DAY28(28, 100, PointCode.PA06); + + private final Integer dateCount; + private final Integer amount; + private final PointCode pointCode; + + public static final List DAY_COUNTS; + public static final List POINT_CODES; + + static { + DAY_COUNTS = Arrays.stream(PointAchievement.values()) + .map(PointAchievement::getDateCount) + .collect(Collectors.toList()); + + POINT_CODES = Arrays.stream(PointAchievement.values()) + .map(PointAchievement::getPointCode) + .collect(Collectors.toList()); + } + + public static PointAchievement valueOf(Integer count) throws InputMismatchException, IllegalArgumentException { + if (!DAY_COUNTS.contains(count)) { + throw new InputMismatchException(); + } + + for (PointAchievement pointAchievement : PointAchievement.values()) { + Integer dateCount = pointAchievement.getDateCount(); + if (dateCount.equals(count)) { + return pointAchievement; + } + } + throw new IllegalArgumentException(); + } +} diff --git a/src/main/java/com/livable/server/point/domain/PointErrorCode.java b/src/main/java/com/livable/server/point/domain/PointErrorCode.java index 01cb8f02..53a91c36 100644 --- a/src/main/java/com/livable/server/point/domain/PointErrorCode.java +++ b/src/main/java/com/livable/server/point/domain/PointErrorCode.java @@ -10,7 +10,10 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum PointErrorCode implements ErrorCode { - POINT_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํฌ์ธํŠธ ์ •๋ณด์ž…๋‹ˆ๋‹ค."); + POINT_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํฌ์ธํŠธ ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + ACHIEVEMENT_POINT_PAID_FAILED(HttpStatus.BAD_REQUEST, "๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋Š” ๋‹น์ผ์—๋งŒ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."), + ACHIEVEMENT_POINT_PAID_ALREADY(HttpStatus.BAD_REQUEST, "๊ธˆ์ผ ๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ด๋ฏธ ์ง€๊ธ‰ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค."), + ACHIEVEMENT_POINT_NOT_MATCHED(HttpStatus.BAD_REQUEST, "๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/point/dto/PointProjection.java b/src/main/java/com/livable/server/point/dto/PointProjection.java new file mode 100644 index 00000000..d40d1486 --- /dev/null +++ b/src/main/java/com/livable/server/point/dto/PointProjection.java @@ -0,0 +1,26 @@ +package com.livable.server.point.dto; + +import com.livable.server.entity.Review; +import lombok.*; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PointProjection { + + @Getter + @AllArgsConstructor + public static class CountAndDateDTO { + + private Long count; + private LocalDateTime mostRecentCreatedDate; + } + + @Getter + @AllArgsConstructor + public static class ReviewAndDateDTO { + + private Review review; + private LocalDateTime createdAt; + } +} diff --git a/src/main/java/com/livable/server/point/dto/PointResponse.java b/src/main/java/com/livable/server/point/dto/PointResponse.java index 27eddd8d..90bcf792 100644 --- a/src/main/java/com/livable/server/point/dto/PointResponse.java +++ b/src/main/java/com/livable/server/point/dto/PointResponse.java @@ -2,6 +2,8 @@ import lombok.*; +import java.time.LocalDateTime; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class PointResponse { @@ -11,4 +13,12 @@ public static class ReviewCountDTO { private Long count; } + + @Getter + @AllArgsConstructor + public static class CountAndDateDTO { + + private Long count; + private LocalDateTime mostRecentCreatedDate; + } } diff --git a/src/main/java/com/livable/server/point/repository/PointLogRepository.java b/src/main/java/com/livable/server/point/repository/PointLogRepository.java new file mode 100644 index 00000000..84063d78 --- /dev/null +++ b/src/main/java/com/livable/server/point/repository/PointLogRepository.java @@ -0,0 +1,15 @@ +package com.livable.server.point.repository; + +import com.livable.server.entity.PointLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; + +public interface PointLogRepository extends JpaRepository { + + @Query(value = "SELECT * FROM point_log WHERE DATE(created_at) = :date", nativeQuery = true) + List findLogsByDate(@Param("date") LocalDate date); +} diff --git a/src/main/java/com/livable/server/point/repository/PointRepository.java b/src/main/java/com/livable/server/point/repository/PointRepository.java index 47580a9b..143a01c8 100644 --- a/src/main/java/com/livable/server/point/repository/PointRepository.java +++ b/src/main/java/com/livable/server/point/repository/PointRepository.java @@ -2,6 +2,7 @@ import com.livable.server.entity.Point; import com.livable.server.entity.PointCode; +import com.livable.server.point.dto.PointProjection; import com.livable.server.point.dto.PointResponse; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -13,7 +14,7 @@ public interface PointRepository extends JpaRepository { - Optional findByMember_Id(Long memberId); + Optional findByMemberId(Long memberId); @Query("SELECT new com.livable.server.point.dto.PointResponse$ReviewCountDTO(COUNT(pl.id)) " + "FROM PointLog pl " + @@ -25,4 +26,27 @@ PointResponse.ReviewCountDTO findPointCountById( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("codes") List codes); + + @Query("SELECT new com.livable.server.point.dto.PointProjection$CountAndDateDTO(COUNT(pl.id), MAX(pl.createdAt)) " + + "FROM PointLog pl " + + "WHERE pl.point.id = :pointId " + + "AND pl.createdAt BETWEEN :startDate AND :endDate " + + "AND pl.code IN (:codes)") + PointProjection.CountAndDateDTO findCountAndDateById( + @Param("pointId") Long pointId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("codes") List codes); + + @Query("SELECT new com.livable.server.point.dto.PointProjection$ReviewAndDateDTO(pl.review, pl.createdAt) " + + "FROM PointLog pl " + + "WHERE pl.point.id = :pointId " + + "AND pl.createdAt BETWEEN :startDate AND :endDate " + + "AND pl.code IN (:codes)" + + "ORDER BY pl.createdAt DESC") + List findReviewAndDateById( + @Param("pointId") Long pointId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("codes") List codes); } diff --git a/src/main/java/com/livable/server/point/service/PointService.java b/src/main/java/com/livable/server/point/service/PointService.java index ef755ec4..67488666 100644 --- a/src/main/java/com/livable/server/point/service/PointService.java +++ b/src/main/java/com/livable/server/point/service/PointService.java @@ -3,17 +3,25 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Point; import com.livable.server.entity.PointCode; -import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.entity.PointLog; +import com.livable.server.entity.Review; import com.livable.server.point.domain.DateFactory; import com.livable.server.point.domain.DateRange; +import com.livable.server.point.domain.PointAchievement; import com.livable.server.point.domain.PointErrorCode; +import com.livable.server.point.dto.PointProjection; import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.repository.PointLogRepository; import com.livable.server.point.repository.PointRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.InputMismatchException; import java.util.List; @RequiredArgsConstructor @@ -21,12 +29,13 @@ public class PointService { private final PointRepository pointRepository; + private final PointLogRepository pointLogRepository; private final DateFactory dateFactory; @Transactional(readOnly = true) public PointResponse.ReviewCountDTO getMyReviewCount(Long memberId, LocalDateTime currentDate) { - Point point = pointRepository.findByMember_Id(memberId).orElseThrow(() -> + Point point = pointRepository.findByMemberId(memberId).orElseThrow(() -> new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); DateRange dateRange = dateFactory.getMonthRangeOf(currentDate); @@ -35,7 +44,73 @@ public PointResponse.ReviewCountDTO getMyReviewCount(Long memberId, LocalDateTim point.getId(), dateRange.getStartDate(), dateRange.getEndDate(), - List.of(PointCode.PA00, PointCode.PA01, PointCode.PA02) + PointCode.getReviewPointCodes() ); } + + @Transactional + public void getAchievementPoint(Long memberId, LocalDateTime requestDateTime) { + + System.out.println("requestDateTime:" + requestDateTime); + + // ํšŒ์› ์ •๋ณด๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆ + Point point = pointRepository.findByMemberId(memberId).orElseThrow(() -> + new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); + + // ๊ธˆ์ผ ์ด๋ฏธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์•˜๋Š”์ง€ ํ™•์ธ + LocalDate requestDate = dateFactory.getPureDate(requestDateTime); + List logsByDate = pointLogRepository.findLogsByDate(requestDate); + System.out.println(logsByDate.get(0).getCode()); + logsByDate.forEach(pointLog -> { + if (PointAchievement.POINT_CODES.contains(pointLog.getCode())) { + throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY); + } + }); + + // ํ˜„์žฌ์˜ ๋…„-์›” ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ๋ฆฌ๋ทฐ๋ฅผ ์กฐํšŒ + DateRange requestedDateOfMonthRange = dateFactory.getMonthRangeOf(requestDateTime); + List reviewAndDates = pointRepository.findReviewAndDateById( + point.getId(), + requestedDateOfMonthRange.getStartDate(), + requestedDateOfMonthRange.getEndDate(), + PointCode.getReviewPointCodes() + ); + + PointProjection.ReviewAndDateDTO lastReview = reviewAndDates.get(0); + + Review review = lastReview.getReview(); + LocalDateTime lastCreatedDate = lastReview.getCreatedAt(); + Integer count = reviewAndDates.size(); + PointAchievement pointAchievement; + + // ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ๋ชฉํ‘œ ๋‹ฌ์„ฑ์œผ๋กœ ์น˜ํ™˜๋˜๋Š”์ง€ ํ™•์ธ + try { + pointAchievement = PointAchievement.valueOf(count); + } catch (InputMismatchException exception) { + throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED); + } + + // ๋ชฉํ‘œ ํฌ์ธํŠธ ์ง€๊ธ‰ ์š”์ฒญ ๋‚ ์งœ๊ฐ€ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋‚ ์งœ์ธ์ง€ ํ™•์ธ + if (lastCreatedDate.getDayOfMonth() != requestDateTime.getDayOfMonth()) { + throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_FAILED); + } + + // ํฌ์ธํŠธ ์ง€๊ธ‰ + this.paidPoints(point, pointAchievement, review); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) + public void paidPoints(Point point, PointAchievement pointAchievement, Review review) { + + Integer amount = pointAchievement.getAmount(); + + point.plusPoint(amount); + PointLog pointLog = PointLog.builder() + .point(point) + .review(review) + .code(pointAchievement.getPointCode()) + .amount(pointAchievement.getAmount()) + .build(); + pointLogRepository.save(pointLog); + } } From 8b70667faa3cd1cd9e431f9f7b1f9b0bb86a1181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Wed, 27 Sep 2023 19:40:02 +0900 Subject: [PATCH 057/111] =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20=EB=88=84=EB=9D=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B9=8C=EB=93=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: ํฌ์ธํŠธ ์กฐํšŒ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์˜ค๋ฅ˜ ์ˆ˜์ • (#127) --- .../com/livable/server/point/service/PointServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/livable/server/point/service/PointServiceTest.java b/src/test/java/com/livable/server/point/service/PointServiceTest.java index 94b202d0..4d1df611 100644 --- a/src/test/java/com/livable/server/point/service/PointServiceTest.java +++ b/src/test/java/com/livable/server/point/service/PointServiceTest.java @@ -49,7 +49,7 @@ void success_Test() { Mockito.when(dateFactory.getMonthRangeOf(ArgumentMatchers.any(LocalDateTime.class))) .thenReturn(dateRange); - Mockito.when(pointRepository.findByMember_Id(ArgumentMatchers.anyLong())) + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) .thenReturn(Optional.of(point)); Mockito.when(pointRepository.findPointCountById( @@ -73,7 +73,7 @@ void failure_Test_existPointData() { Long memberId = 1L; LocalDateTime currentDate = LocalDateTime.now(); - Mockito.when(pointRepository.findByMember_Id(ArgumentMatchers.anyLong())) + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) .thenReturn(Optional.empty()); // When @@ -82,4 +82,4 @@ void failure_Test_existPointData() { pointService.getMyReviewCount(memberId, currentDate)); } } -} \ No newline at end of file +} From 5a0cee383094fea6b8f99ef988c507d198099038 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 27 Sep 2023 20:05:25 +0900 Subject: [PATCH 058/111] =?UTF-8?q?=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EB=B9=84=EC=9C=A8=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/build.gradle b/build.gradle index 0909bd96..c38bc92b 100644 --- a/build.gradle +++ b/build.gradle @@ -55,30 +55,13 @@ jacocoTestCoverageVerification { rule { enabled = true - limit { - counter = 'BRANCH' - value = 'COVEREDRATIO' - minimum = 0.40 - } limit { counter = 'METHOD' value = 'COVEREDRATIO' - minimum = 0.80 - } - - limit { - counter = 'LINE' - value = 'COVEREDRATIO' minimum = 0.50 } - limit { - counter = 'LINE' - value = 'TOTALCOUNT' - minimum = 200 - } - excludes = [] + Qdomains } } From b5b9f43ddc64ab078d25e672befc21c85450428f Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:18:19 +0900 Subject: [PATCH 059/111] =?UTF-8?q?Fix=20:=20Gradle=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=A4=ED=8C=A8=20=EB=8C=80=EC=9D=91=20(#129)=20?= =?UTF-8?q?(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Request์˜ Jacskon lib์˜ ๋งคํ•‘์‹คํŒจ DTO์— - @NoArgsConstructor - @AllArgsConstructor ์ถ”๊ฐ€ --- .../livable/server/invitation/dto/InvitationRequest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java index f43491a8..064812af 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java @@ -4,6 +4,7 @@ import com.livable.server.entity.Member; import com.livable.server.entity.Visitor; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -22,6 +23,8 @@ public class InvitationRequest { @Getter @Builder + @NoArgsConstructor + @AllArgsConstructor public static class CreateDTO { @NotNull(message = NOT_NULL) @@ -61,6 +64,8 @@ public Invitation toEntity(Member member) { @Getter @Builder + @NoArgsConstructor + @AllArgsConstructor public static class VisitorCreateDTO { @NotNull private String name; @@ -80,6 +85,8 @@ public Visitor toEntity(Invitation invitation) { @Getter @Builder + @NoArgsConstructor + @AllArgsConstructor public static class UpdateDTO { private Long commonPlaceId; private String description; @@ -99,6 +106,8 @@ public static class UpdateDTO { @Getter @Builder + @NoArgsConstructor + @AllArgsConstructor public static class VisitorForUpdateDTO { @NotNull(message = NOT_NULL) private String name; From 236cd365f166e2f9ea4e61bf69af619d759dc502 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:33:39 +0900 Subject: [PATCH 060/111] =?UTF-8?q?Test=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=EC=84=9C=EC=9D=98=20TimeZone=20=EB=B6=88=EC=9D=BC=EC=B9=98?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ src/test/resources/application.yml | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c38bc92b..1820af90 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,10 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' } +test { + systemProperty 'user.timezone', 'Asia/Seoul' +} + tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 3a0d2226..912c7561 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -5,4 +5,8 @@ spring: properties: hibernate: show_sql: true - format_sql: true \ No newline at end of file + format_sql: true + +logging: + pattern: + dateformat: yyyy-MM-dd HH:mm:ss.SSSz,Asia/Seoul \ No newline at end of file From 3bfb3fb6f92fd3dabbf0472f1bc8fcdef93543bb Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:20:51 +0900 Subject: [PATCH 061/111] =?UTF-8?q?=EB=A9=94=EB=89=B4=EB=A5=BC=20=ED=8C=90?= =?UTF-8?q?=EB=A7=A4=ED=95=98=EB=8A=94=20=EC=8B=9D=EB=8B=B9=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต์šฉ DTO ์ถ”๊ฐ€ (#100) * Feat : Native Query๋ฅผ ์œ„ํ•œ Entity ์ˆ˜์ • (#100) * Feat : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต์šฉ Repository ๊ตฌํ˜„ (#100) * Feat : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต์šฉ Service ๊ตฌํ˜„ (#100) * Feat : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต์šฉ Controller ๊ตฌํ˜„ (#100) ๋ฉ”์„œ๋“œ๋ณ„ ์—”๋“œํฌ์ธํŠธ ๊ตฌ๋ถ„์„ ์œ„ํ•ด RequestMapping์˜ value ์ˆ˜์ • ๋ฐ GetMapping์˜ value ์ถ”๊ฐ€ * Test : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต Controller ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (#100) * Style : Style ์ˆ˜์ • (#100) * Test : ๋ฉ”๋‰ด๋ฅผ ํŒ๋งค ํ•˜๊ณ  ์žˆ๋Š” ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต Service ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (#100) --- .../com/livable/server/entity/Restaurant.java | 34 ++++++- .../controller/RestaurantController.java | 20 +++- .../domain/RestaurantErrorCode.java | 4 +- .../dto/RestaurantByMenuProjection.java | 19 ++++ .../restaurant/dto/RestaurantResponse.java | 63 +++++++++++++ ...aurantGroupByMenuProjectionRepository.java | 53 +++++++++++ .../restaurant/service/RestaurantService.java | 30 +++++- .../controller/RestaurantControllerTest.java | 73 +++++++++++++-- .../mock/MockRestaurantByMenuDto.java | 7 ++ .../service/RestaurantServiceTest.java | 92 ++++++++++++++++--- 10 files changed, 367 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/livable/server/restaurant/dto/RestaurantByMenuProjection.java create mode 100644 src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java create mode 100644 src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java diff --git a/src/main/java/com/livable/server/entity/Restaurant.java b/src/main/java/com/livable/server/entity/Restaurant.java index 3e4e1a28..368d0d11 100644 --- a/src/main/java/com/livable/server/entity/Restaurant.java +++ b/src/main/java/com/livable/server/entity/Restaurant.java @@ -1,8 +1,38 @@ package com.livable.server.entity; -import lombok.*; +import com.livable.server.restaurant.dto.RestaurantByMenuProjection; +import javax.persistence.Column; +import javax.persistence.ColumnResult; +import javax.persistence.ConstructorResult; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SqlResultSetMapping; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; -import javax.persistence.*; +@SqlResultSetMapping( + name ="RestaurantsByMenuMapping", + classes = @ConstructorResult( + targetClass = RestaurantByMenuProjection.class, + columns = { + @ColumnResult(name = "restaurantId", type = Long.class), + @ColumnResult(name = "restaurantName", type = String.class), + @ColumnResult(name = "restaurantThumbnailUrl", type = String.class), + @ColumnResult(name = "address", type = String.class), + @ColumnResult(name = "inBuilding", type = Boolean.class), + @ColumnResult(name = "distance", type = Integer.class), + @ColumnResult(name = "review", type = String.class), + @ColumnResult(name = "tastePercentage", type = Integer.class), + } + ) +) @Getter @Builder diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index a91fd956..25e871da 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -1,11 +1,13 @@ package com.livable.server.restaurant.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.response.ApiResponse.Success; import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.service.RestaurantService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -19,12 +21,12 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/api/restaurant") +@RequestMapping("/api") public class RestaurantController { private final RestaurantService restaurantService; - @GetMapping + @GetMapping("/restaurant") public ResponseEntity> findRestaurantByCategory( @RequestParam("type") RestaurantCategory restaurantCategory, @LoginActor Actor actor ) { @@ -38,4 +40,18 @@ public ResponseEntity> findRestaurantByCategory( return ApiResponse.success(result, HttpStatus.OK); } + + @GetMapping("/restaurants") + public ResponseEntity>> getRestaurantsByMenu( + @RequestParam("menuId") Long menuId, @LoginActor Actor actor + ) { + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + List restaurantsByMenuDtos = restaurantService.findRestaurantByMenuId(menuId, memberId); + + return ApiResponse.success(restaurantsByMenuDtos, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java b/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java index 4f7d0561..8596e34e 100644 --- a/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java +++ b/src/main/java/com/livable/server/restaurant/domain/RestaurantErrorCode.java @@ -9,7 +9,9 @@ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum RestaurantErrorCode implements ErrorCode { - NOT_FOUND_CATEGORY(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹๋‹น ์ข…๋ฅ˜์ž…๋‹ˆ๋‹ค."); + NOT_FOUND_CATEGORY(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹๋‹น ์ข…๋ฅ˜์ž…๋‹ˆ๋‹ค."), + + NOT_FOUND_RESTAURANT_BY_MENU(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋ฉ”๋‰ด๋ฅผ ์ œ๊ณตํ•˜๋Š” ์‹๋‹น์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantByMenuProjection.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantByMenuProjection.java new file mode 100644 index 00000000..6db24956 --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantByMenuProjection.java @@ -0,0 +1,19 @@ +package com.livable.server.restaurant.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RestaurantByMenuProjection { + + Long restaurantId; + String restaurantName; + String restaurantThumbnailUrl; + String address; + Boolean inBuilding; + Integer distance; + String review; + Integer tastePercentage; + +} diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java index ffedf080..84fe2c77 100644 --- a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java @@ -1,8 +1,11 @@ package com.livable.server.restaurant.dto; import com.livable.server.entity.RestaurantCategory; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -25,4 +28,64 @@ public static class NearRestaurantDto { private String url; } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RestaurantsByMenuDto { + + Long restaurantId; + String restaurantName; + Integer tastePercentage; + String representativeImageUrl; + String address; + Integer floor; + Boolean inBuilding; + Integer estimatedTime; + String review; + + public static RestaurantsByMenuDto from(RestaurantByMenuProjection restaurantByMenuProjection) { + return RestaurantsByMenuDto.builder() + .restaurantId(restaurantByMenuProjection.getRestaurantId()) + .restaurantName(restaurantByMenuProjection.getRestaurantName()) + .tastePercentage(restaurantByMenuProjection.getTastePercentage()) + .representativeImageUrl(restaurantByMenuProjection.getRestaurantThumbnailUrl()) + .address(restaurantByMenuProjection.getAddress()) + .floor(getFloorFromAddress(restaurantByMenuProjection.getAddress())) + .inBuilding(restaurantByMenuProjection.getInBuilding()) + .estimatedTime(calcEstimatedTime(restaurantByMenuProjection.getDistance())) + .review(restaurantByMenuProjection.getReview()) + .build(); + } + + private static Integer calcEstimatedTime(Integer distance) { + int averageWalkSpeedPerMin = 80; + return distance / averageWalkSpeedPerMin; + } + + private static Integer getFloorFromAddress(String address) { + + int floor = 0; + + if (address.contains("์ธต")) { + String pattern = "\\s(\\d+)์ธต"; + Pattern regexPattern = Pattern.compile(pattern, Pattern.CANON_EQ); + Matcher matcher = regexPattern.matcher(address); + if (matcher.find() && matcher.group(1) != null) { + + floor = Integer.parseInt(matcher.group(1)); + if (address.contains("์ง€ํ•˜")) { + floor *= -1; + } + + } + + } + + return floor; + } + + } + } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java new file mode 100644 index 00000000..503eab1b --- /dev/null +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java @@ -0,0 +1,53 @@ +package com.livable.server.restaurant.repository; + +import com.livable.server.restaurant.dto.RestaurantByMenuProjection; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class RestaurantGroupByMenuProjectionRepository { + + private static final String FIND_RESTAURANT_BY_MENU_ID_QUERY; + + static { + FIND_RESTAURANT_BY_MENU_ID_QUERY = "SELECT res.id as restaurantId, res.name as restaurantName, res.thumbnail_image_url as restaurantThumbnailUrl, res.address as address, " + + "brm.in_building as inBuilding, brm.distance as distance, " + + "MAX(" + + "(SELECT r.description " + + " FROM review r " + + " WHERE r.id = rsv.id " + + " ORDER BY r.created_at DESC " + + " LIMIT 1)) as review, " + + "(SELECT ROUND(SUM(CASE WHEN rsv2.taste = 'good' THEN 1 ELSE 0 END) / COUNT(rsv2.id) * 100, 0) " + + "FROM restaurant_review rsv2 " + + "WHERE rsv2.restaurant_id = res.id) as tastePercentage " + + "FROM member m " + + "JOIN company c " + + "ON m.company_id = c.id " + + "JOIN building_restaurant_map brm " + + "ON c.building_id = brm.building_id " + + "JOIN restaurant res ON brm.restaurant_id = res.id " + + "JOIN restaurant_menu_map rmm " + + "ON res.id = rmm.restaurant_id " + + "LEFT JOIN restaurant_review rsv ON res.id = rsv.restaurant_id " + + "WHERE rmm.menu_id = :menuId AND m.id = :memberId " + + "GROUP BY res.id, res.name, res.thumbnail_image_url, res.address, brm.in_building, brm.distance"; + } + + @PersistenceContext + private EntityManager entityManager; + + public List findRestaurantByMenuId(Long menuId, Long memberId) { + + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_BY_MENU_ID_QUERY, "RestaurantsByMenuMapping") + .setParameter("menuId", menuId) + .setParameter("memberId", memberId); + + return (List) query.getResultList(); + } + + +} diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java index 917107f6..8906e29d 100644 --- a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -3,13 +3,17 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.domain.RandomGenerator; +import com.livable.server.restaurant.domain.RestaurantErrorCode; +import com.livable.server.restaurant.dto.RestaurantByMenuProjection; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; +import com.livable.server.restaurant.repository.RestaurantGroupByMenuProjectionRepository; import com.livable.server.restaurant.repository.RestaurantRepository; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; +import java.util.ArrayList; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,6 +30,7 @@ public class RestaurantService { private final RestaurantRepository restaurantRepository; private final VisitorRepository visitorRepository; private final BuildingRestaurantMapRepository buildingRestaurantMapRepository; + private final RestaurantGroupByMenuProjectionRepository restaurantGroupByMenuProjectionRepository; public List findNearRestaurantByVisitorIdAndRestaurantCategory( Long visitorId, RestaurantCategory category @@ -43,4 +48,27 @@ public List findNearRestaurantByVisitorIdA buildingId, category, randomGenerator.getRandom(nearRestaurantCount) ); } + + public List findRestaurantByMenuId(Long menuId, Long memberId) { + List restaurantByMenuProjections = restaurantGroupByMenuProjectionRepository.findRestaurantByMenuId(menuId, memberId); + + if (restaurantByMenuProjections.isEmpty()) { + throw new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU); + } + + return getRestaurantsByMenu(restaurantByMenuProjections); + } + + private List getRestaurantsByMenu( + List restaurantByMenuProjections) { + + List restaurantsByMenuDtos = new ArrayList<>(); + + for (RestaurantByMenuProjection restaurantByMenuProjection : restaurantByMenuProjections) { + restaurantsByMenuDtos.add(RestaurantsByMenuDto.from(restaurantByMenuProjection)); + } + + return restaurantsByMenuDtos; + } + } diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java index dca41875..2bfa7b9e 100644 --- a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -1,13 +1,28 @@ package com.livable.server.restaurant.controller; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.util.ActorType; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.TestConfig; import com.livable.server.entity.RestaurantCategory; +import com.livable.server.restaurant.domain.RestaurantErrorCode; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.mock.MockNearRestaurantDto; +import com.livable.server.restaurant.mock.MockRestaurantByMenuDto; import com.livable.server.restaurant.service.RestaurantService; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; @@ -16,16 +31,7 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; - -import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.mockito.BDDMockito.given; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @Import(TestConfig.class) @WebMvcTest(RestaurantController.class) @@ -68,4 +74,51 @@ void findRestaurantByCategorySuccessTest(String category) throws Exception { resultActions.andExpect(status().isOk()) .andExpect(jsonPath("$.data").isArray()); } + + @DisplayName("SUCCESS - ๋ฉ”๋‰ด๋ณ„ ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void findRestaurantByMenuSuccess() throws Exception { + + // given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + List result = + IntStream.range(1, 10) + .mapToObj(idx -> new MockRestaurantByMenuDto()) + .collect(Collectors.toList()); + + given(restaurantService.findRestaurantByMenuId(anyLong(), anyLong())) + .willReturn(result); + + // when & then + mockMvc.perform(get("/api/restaurants") + .header("Authorization", "Bearer " + token) + .queryParam("menuId", "1") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()); + + } + + @DisplayName("FAILED - ๋ฉ”๋‰ด๋ณ„ ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void findRestaurantByMenuFailed() throws Exception { + + //given + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, + new Date(new Date().getTime() + 10000000)); + + given(restaurantService.findRestaurantByMenuId(anyLong(), anyLong())) + .willThrow( + new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/restaurants") + .header("Authorization", "Bearer " + token) + .queryParam("menuId", "1") + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value( + RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU.getMessage())); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java b/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java new file mode 100644 index 00000000..bc8fcd9a --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java @@ -0,0 +1,7 @@ +package com.livable.server.restaurant.mock; + +import com.livable.server.restaurant.dto.RestaurantResponse; + +public class MockRestaurantByMenuDto extends RestaurantResponse.RestaurantsByMenuDto { + +} diff --git a/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java index 203df0ff..c831a493 100644 --- a/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java +++ b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java @@ -1,13 +1,33 @@ package com.livable.server.restaurant.service; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.domain.RandomGenerator; +import com.livable.server.restaurant.dto.RestaurantByMenuProjection; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; +import com.livable.server.restaurant.repository.RestaurantGroupByMenuProjectionRepository; import com.livable.server.restaurant.repository.RestaurantRepository; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,18 +37,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.times; - @ExtendWith(MockitoExtension.class) class RestaurantServiceTest { @@ -40,6 +48,8 @@ class RestaurantServiceTest { @Mock RestaurantRepository restaurantRepository; @Mock + RestaurantGroupByMenuProjectionRepository restaurantGroupByMenuProjectionRepository; + @Mock VisitorRepository visitorRepository; @Mock BuildingRestaurantMapRepository buildingRestaurantMapRepository; @@ -109,4 +119,62 @@ void findNearRestaurantByVisitorIdAndRestaurantCategoryFailTest() { assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); then(visitorRepository).should(times(1)).findBuildingIdById(anyLong()); } + + @DisplayName("SUCCESS - ๋ฉ”๋‰ด๋ณ„ ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void findRestaurantByMenuSuccess() { + + //given + Long restaurantId = 1L; + String restaurantName = "๋ ˆ์Šคํ† ๋ž‘"; + String restaurantThumbnailUrl = "/restaurantImg.com"; + String address ="address for restaurant"; + Boolean inBuilding = true; + Integer distance = 100; + String review = "this is review"; + Integer tastePercentage = 30; + + List projections = List.of( + new RestaurantByMenuProjection(restaurantId, restaurantName, restaurantThumbnailUrl, + address, inBuilding, distance, review, tastePercentage) + ); + + given(restaurantGroupByMenuProjectionRepository.findRestaurantByMenuId(anyLong(), anyLong())) + .willReturn(projections); + + // when + List actual = + restaurantService.findRestaurantByMenuId(anyLong(), anyLong()); + + // then + assertAll( + () -> assertEquals(restaurantId, actual.get(0).getRestaurantId()), + () -> assertEquals(restaurantName, actual.get(0).getRestaurantName()), + () -> assertEquals(restaurantThumbnailUrl, actual.get(0).getRepresentativeImageUrl()), + () -> assertEquals(address, actual.get(0).getAddress()), + () -> assertEquals(inBuilding, actual.get(0).getInBuilding()), + () -> assertEquals(distance / 80, actual.get(0).getEstimatedTime()), + () -> assertEquals(review, actual.get(0).getReview()), + () -> assertEquals(tastePercentage, actual.get(0).getTastePercentage()) + ); + + } + + @DisplayName("FAIELD - ๋ฉ”๋‰ด๋ณ„ ์‹๋‹น ๋ชฉ๋ก ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void findRestaurantByMenuFailed() { + + // given + + + // when + given(restaurantGroupByMenuProjectionRepository.findRestaurantByMenuId(anyLong(), anyLong())) + .willReturn(new ArrayList<>()); + + // then + Assertions.assertThrows(GlobalRuntimeException.class, () -> + restaurantService.findRestaurantByMenuId(1L, 1L)); + + } + } \ No newline at end of file From fe70bbe38cad5b3be6a6792cb55450e0116f231e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 28 Sep 2023 21:47:13 +0900 Subject: [PATCH 062/111] =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8,=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=EC=97=90=20JWT=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=99=84=EB=A3=8C=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ๋”๋ฏธ ์ฝ”๋“œ๋กœ ์ง„ํ–‰๋˜๋˜ memberId ์ถ”์ถœํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์‹ค์ œ ๋™์ž‘ํ•˜๋Š” ์ฝ”๋“œ๋กœ ๋ณ€๊ฒฝ (#136) --- .../livable/server/point/controller/PointController.java | 8 +++++--- .../com/livable/server/point/service/PointService.java | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/livable/server/point/controller/PointController.java b/src/main/java/com/livable/server/point/controller/PointController.java index a2affe3a..dca4e857 100644 --- a/src/main/java/com/livable/server/point/controller/PointController.java +++ b/src/main/java/com/livable/server/point/controller/PointController.java @@ -26,7 +26,7 @@ public ResponseEntity> getMyRe JwtTokenProvider.checkMemberToken(actor); - Long memberId = actor.getId(); // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ + Long memberId = actor.getId(); LocalDateTime currentDate = LocalDateTime.now(); PointResponse.ReviewCountDTO myReviewCount = pointService.getMyReviewCount(memberId, currentDate); @@ -34,9 +34,11 @@ public ResponseEntity> getMyRe } @PostMapping("/api/points/logs/members") - public ResponseEntity> getAchievementPoint() { + public ResponseEntity> getAchievementPoint(@LoginActor Actor actor) { - Long memberId = 1L; // TODO: ํ† ํฐ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ๊ฒƒ + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); LocalDateTime requestDateTime = LocalDateTime.now(); pointService.getAchievementPoint(memberId, requestDateTime); diff --git a/src/main/java/com/livable/server/point/service/PointService.java b/src/main/java/com/livable/server/point/service/PointService.java index 67488666..343e808e 100644 --- a/src/main/java/com/livable/server/point/service/PointService.java +++ b/src/main/java/com/livable/server/point/service/PointService.java @@ -60,7 +60,6 @@ public void getAchievementPoint(Long memberId, LocalDateTime requestDateTime) { // ๊ธˆ์ผ ์ด๋ฏธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์•˜๋Š”์ง€ ํ™•์ธ LocalDate requestDate = dateFactory.getPureDate(requestDateTime); List logsByDate = pointLogRepository.findLogsByDate(requestDate); - System.out.println(logsByDate.get(0).getCode()); logsByDate.forEach(pointLog -> { if (PointAchievement.POINT_CODES.contains(pointLog.getCode())) { throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY); From 154382aea2c8a951e57a761b99692bb8da854fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 28 Sep 2023 23:59:14 +0900 Subject: [PATCH 063/111] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=9D=8C=EC=8B=9D?= =?UTF-8?q?=EC=A0=90,=20=ED=8A=B9=EC=A0=95=20=EB=A9=94=EB=89=B4=EC=9D=98?= =?UTF-8?q?=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ๊ฐ์ฒด ์ˆ˜์ • (#139) * Refactor: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ๊ฐ์ฒด ์ˆ˜์ •์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ๋ณ€๊ฒฝ (#139) * Refactor: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๋ณ€๊ฒฝ (#139) * Refactor: ํŠน์ • ๋ฉ”๋‰ด์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๋ณ€๊ฒฝ (#139) * Refactor: ๋ชจ๋“  ๋ ˆ์Šคํ† ๋ž‘ ๋ฆฌ๋ทฐ ์‘๋‹ต ์‹œ ํšŒ์› ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋„ ํ•จ๊ป˜ ์‘๋‹ตํ•˜๋„๋ก ๋ณ€๊ฒฝ (#140) --- .../livable/server/entity/ReviewImage.java | 1 + .../RestaurantReviewController.java | 5 +- .../dto/RestaurantReviewProjection.java | 3 ++ .../review/dto/RestaurantReviewResponse.java | 46 +++++++++++++------ .../RestaurantReviewProjectionRepository.java | 43 ++++++++++++++++- .../service/RestaurantReviewService.java | 10 +++- .../RestaurantReviewControllerTest.java | 7 ++- .../service/RestaurantReviewServiceTest.java | 32 ++++++------- 8 files changed, 107 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/livable/server/entity/ReviewImage.java b/src/main/java/com/livable/server/entity/ReviewImage.java index a7791fa3..9fd5c499 100644 --- a/src/main/java/com/livable/server/entity/ReviewImage.java +++ b/src/main/java/com/livable/server/entity/ReviewImage.java @@ -12,6 +12,7 @@ targetClass = RestaurantReviewProjection.class, columns = { @ColumnResult(name = "memberName", type = String.class), + @ColumnResult(name = "memberProfileImage", type = String.class), @ColumnResult(name = "restaurantId", type = Long.class), @ColumnResult(name = "restaurantName", type = String.class), @ColumnResult(name = "reviewId", type = Long.class), diff --git a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java index 1ced95e8..211d677d 100644 --- a/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java +++ b/src/main/java/com/livable/server/review/controller/RestaurantReviewController.java @@ -4,7 +4,6 @@ import com.livable.server.review.dto.RestaurantReviewResponse; import com.livable.server.review.service.RestaurantReviewService; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; @@ -35,11 +34,11 @@ public ResponseEntity>> listForMenu( + public ResponseEntity>> listForMenu( @PathVariable Long menuId, @PageableDefault Pageable pageable) { - Page allListForMenu = + List allListForMenu = restaurantReviewService.getAllListForMenu(menuId, pageable); return ApiResponse.success(allListForMenu, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java index 525e65f9..de23d11c 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewProjection.java @@ -13,6 +13,7 @@ public class RestaurantReviewProjection { private String memberName; + private String memberProfileImage; private Long restaurantId; private String restaurantName; @@ -28,6 +29,7 @@ public class RestaurantReviewProjection { private String images; public RestaurantReviewProjection(String memberName, + String memberProfileImage, Long restaurantId, String restaurantName, Long reviewId, @@ -40,6 +42,7 @@ public RestaurantReviewProjection(String memberName, String images) { this.memberName = memberName; + this.memberProfileImage = memberProfileImage; this.restaurantId = restaurantId; this.restaurantName = restaurantName; this.reviewId = reviewId; diff --git a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java index 2ed7783d..3b95fde1 100644 --- a/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/RestaurantReviewResponse.java @@ -15,6 +15,7 @@ public class RestaurantReviewResponse { public static class ListForBuildingDTO { private final String memberName; + private final String memberProfileImage; private final Long restaurantId; private final String restaurantName; @@ -32,6 +33,7 @@ public static class ListForBuildingDTO { public static ListForBuildingDTO valueOf(RestaurantReviewProjection restaurantReviewList, ImageSeparator imageSeparator) { return ListForBuildingDTO.builder() .memberName(restaurantReviewList.getMemberName()) + .memberProfileImage(restaurantReviewList.getMemberProfileImage()) .restaurantId(restaurantReviewList.getRestaurantId()) .restaurantName(restaurantReviewList.getRestaurantName()) .reviewId(restaurantReviewList.getReviewId()) @@ -51,6 +53,7 @@ public static ListForBuildingDTO valueOf(RestaurantReviewProjection restaurantRe public static class ListForRestaurantDTO { private final String memberName; + private final String memberProfileImage; private final Long restaurantId; private final String restaurantName; @@ -68,6 +71,7 @@ public static class ListForRestaurantDTO { public static ListForRestaurantDTO valueOf(RestaurantReviewProjection restaurantReviewList, ImageSeparator imageSeparator) { return ListForRestaurantDTO.builder() .memberName(restaurantReviewList.getMemberName()) + .memberProfileImage(restaurantReviewList.getMemberProfileImage()) .restaurantId(restaurantReviewList.getRestaurantId()) .restaurantName(restaurantReviewList.getRestaurantName()) .reviewId(restaurantReviewList.getReviewId()) @@ -84,24 +88,40 @@ public static ListForRestaurantDTO valueOf(RestaurantReviewProjection restaurant @Getter @Builder - @NoArgsConstructor - @AllArgsConstructor public static class ListForMenuDTO { - private Long reviewId; - private LocalDateTime reviewCreatedAt; - private String reviewDescription; + private final String memberName; + private final String memberProfileImage; - private Evaluation reviewTaste; - private Evaluation reviewAmount; - private Evaluation reviewService; - private Evaluation reviewSpeed; + private final Long restaurantId; + private final String restaurantName; - private Long restaurantId; - private String restaurantName; + private final Long reviewId; + private final LocalDateTime reviewCreatedAt; + private final String reviewDescription; + private final Evaluation reviewTaste; + private final Evaluation reviewAmount; + private final Evaluation reviewService; + private final Evaluation reviewSpeed; - private Long memberId; - private String memberName; + private List reviewImages; + + public static ListForMenuDTO valueOf(RestaurantReviewProjection restaurantReviewList, ImageSeparator imageSeparator) { + return ListForMenuDTO.builder() + .memberName(restaurantReviewList.getMemberName()) + .memberProfileImage(restaurantReviewList.getMemberProfileImage()) + .restaurantId(restaurantReviewList.getRestaurantId()) + .restaurantName(restaurantReviewList.getRestaurantName()) + .reviewId(restaurantReviewList.getReviewId()) + .reviewCreatedAt(restaurantReviewList.getReviewCreatedAt()) + .reviewDescription(restaurantReviewList.getReviewDescription()) + .reviewTaste(restaurantReviewList.getReviewTaste()) + .reviewAmount(restaurantReviewList.getReviewAmount()) + .reviewService(restaurantReviewList.getReviewService()) + .reviewSpeed(restaurantReviewList.getReviewSpeed()) + .reviewImages(imageSeparator.separateConcatenatedImages(restaurantReviewList.getImages())) + .build(); + } } @Getter diff --git a/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java index f3b575e7..6cfb2f53 100644 --- a/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java +++ b/src/main/java/com/livable/server/review/repository/RestaurantReviewProjectionRepository.java @@ -15,11 +15,12 @@ public class RestaurantReviewProjectionRepository { private static final String FIND_RESTAURANT_REVIEW_BY_BUILDING_ID_QUERY; private static final String FIND_RESTAURANT_REVIEW_BY_RESTAURANT_ID_QUERY; - + private static final String FIND_RESTAURANT_REVIEW_BY_MENU_ID_QUERY; static { FIND_RESTAURANT_REVIEW_BY_BUILDING_ID_QUERY = "SELECT " + "m.name AS memberName, " + + "m.profile_image_url AS memberProfileImage, " + "res.id AS restaurantId, " + "res.name AS restaurantName, " + "r.id AS reviewId, " + @@ -46,6 +47,7 @@ public class RestaurantReviewProjectionRepository { FIND_RESTAURANT_REVIEW_BY_RESTAURANT_ID_QUERY = "SELECT " + "m.name AS memberName, " + + "m.profile_image_url AS memberProfileImage, " + "res.id AS restaurantId, " + "res.name AS restaurantName, " + "r.id AS reviewId, " + @@ -66,6 +68,34 @@ public class RestaurantReviewProjectionRepository { "ORDER BY r.created_at DESC " + "LIMIT :limit " + "OFFSET :offset"; + + FIND_RESTAURANT_REVIEW_BY_MENU_ID_QUERY = "SELECT " + + "m.name AS memberName, " + + "m.profile_image_url AS memberProfileImage, " + + "res.id AS restaurantId, " + + "res.name AS restaurantName, " + + "r.id AS reviewId, " + + "r.created_at AS reviewCreatedAt, " + + "r.description AS reviewDescription, " + + "rr.taste AS reviewTaste, " + + "rr.amount AS reviewAmount, " + + "rr.service AS reviewService, " + + "rr.speed AS reviewSpeed, " + + "GROUP_CONCAT(ri.url SEPARATOR :separator) AS images " + + "FROM review r " + + "INNER JOIN restaurant_review rr ON rr.id = r.id " + + "INNER JOIN member m ON m.id = r.member_id " + + "INNER JOIN restaurant res ON rr.restaurant_id = res.id " + + "LEFT JOIN review_image ri ON ri.review_id = r.id " + + "WHERE r.id IN ( " + + "SELECT review_menu_map.review_id " + + "FROM review_menu_map " + + "WHERE review_menu_map.menu_id = :menuId " + + ") " + + "GROUP BY r.id, rr.id, m.id, res.id " + + "ORDER BY r.created_at DESC " + + "LIMIT :limit " + + "OFFSET :offset"; } @PersistenceContext @@ -92,4 +122,15 @@ public List findRestaurantReviewProjectionByRestaura return (List) query.getResultList(); } + + public List findRestaurantReviewProjectionByMenuId(Long menuId, Pageable pageable) { + + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_REVIEW_BY_MENU_ID_QUERY, "RestaurantReviewListMapping") + .setParameter("separator", ImageSeparator.IMAGE_SEPARATOR) + .setParameter("menuId", menuId) + .setParameter("limit", pageable.getPageSize()) + .setParameter("offset", pageable.getOffset()); + + return (List) query.getResultList(); + } } diff --git a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java index 7757cd1e..6864217a 100644 --- a/src/main/java/com/livable/server/review/service/RestaurantReviewService.java +++ b/src/main/java/com/livable/server/review/service/RestaurantReviewService.java @@ -50,8 +50,14 @@ public List getAllListForRestaura } @Transactional(readOnly = true) - public Page getAllListForMenu(Long menuId, Pageable pageable) { - return restaurantReviewRepository.findRestaurantReviewByMenuId(menuId, pageable); + public List getAllListForMenu(Long menuId, Pageable pageable) { + List reviewProjections = + restaurantProjectionRepository.findRestaurantReviewProjectionByMenuId(menuId, pageable); + + return reviewProjections.stream() + .map(reviewProjection -> + RestaurantReviewResponse.ListForMenuDTO.valueOf(reviewProjection, imageSeparator)) + .collect(Collectors.toList()); } @Transactional(readOnly = true) diff --git a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java index 9a06de8f..72f0ee2c 100644 --- a/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java +++ b/src/test/java/com/livable/server/review/controller/RestaurantReviewControllerTest.java @@ -134,11 +134,10 @@ void success_Test() throws Exception { RestaurantReviewResponse.ListForMenuDTO.builder().reviewId(10L).build() ); Pageable pageable = PageRequest.of(0, 10); - Page mockPage = new PageImpl<>(mockList, pageable, 1); Mockito.when(restaurantReviewService .getAllListForMenu(ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class))) - .thenReturn(mockPage); + .thenReturn(mockList); // When // Then @@ -146,8 +145,8 @@ void success_Test() throws Exception { .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()) - .andExpect(MockMvcResultMatchers.jsonPath("$.data.content").isArray()) - .andExpect(MockMvcResultMatchers.jsonPath("$.data.content.length()").value(10)); + .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.size()").value(10)); } } diff --git a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java index 79754b7a..34ba551a 100644 --- a/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java +++ b/src/test/java/com/livable/server/review/service/RestaurantReviewServiceTest.java @@ -139,34 +139,32 @@ void success_Test() { // Given Long menuId = 1L; - List mockList = List.of( - ListForMenuDTO.builder().reviewId(1L).build(), - ListForMenuDTO.builder().reviewId(2L).build(), - ListForMenuDTO.builder().reviewId(3L).build(), - ListForMenuDTO.builder().reviewId(4L).build(), - ListForMenuDTO.builder().reviewId(5L).build(), - ListForMenuDTO.builder().reviewId(6L).build(), - ListForMenuDTO.builder().reviewId(7L).build(), - ListForMenuDTO.builder().reviewId(8L).build(), - ListForMenuDTO.builder().reviewId(9L).build(), - ListForMenuDTO.builder().reviewId(10L).build() + List mockList = List.of( + RestaurantReviewProjection.builder().reviewId(1L).build(), + RestaurantReviewProjection.builder().reviewId(2L).build(), + RestaurantReviewProjection.builder().reviewId(3L).build(), + RestaurantReviewProjection.builder().reviewId(4L).build(), + RestaurantReviewProjection.builder().reviewId(5L).build(), + RestaurantReviewProjection.builder().reviewId(6L).build(), + RestaurantReviewProjection.builder().reviewId(7L).build(), + RestaurantReviewProjection.builder().reviewId(8L).build(), + RestaurantReviewProjection.builder().reviewId(9L).build(), + RestaurantReviewProjection.builder().reviewId(10L).build() ); Pageable pageable = PageRequest.of(0, 10); - Page mockPage = new PageImpl<>(mockList, pageable, 1); - Mockito.when(restaurantReviewRepository.findRestaurantReviewByMenuId( + Mockito.when(restaurantReviewProjectionRepository.findRestaurantReviewProjectionByMenuId( ArgumentMatchers.anyLong(), ArgumentMatchers.any(Pageable.class) - )).thenReturn(mockPage); + )).thenReturn(mockList); // When - Page actual = + List actual = restaurantReviewService.getAllListForMenu(menuId, pageable); // Then Assertions.assertAll( - () -> Assertions.assertEquals(10, actual.getSize()), - () -> Assertions.assertEquals(1, actual.getTotalPages()) + () -> Assertions.assertEquals(10, actual.size()) ); } } From 7e3390a4b1cb7e7d72dd470684c4eb7416a2229b Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 29 Sep 2023 13:56:23 +0900 Subject: [PATCH 064/111] =?UTF-8?q?Test=20Properties=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: TestProperties ์–ด๋…ธํ…Œ์ด์…˜ ๊ตฌํ˜„ ํ›„ BootTest์— ์ ์šฉ (#142) * Chore: test.properties gitignore ์ถ”๊ฐ€ (#142) --- .gitignore | 1 + .../server/LivableServerApplicationTests.java | 3 ++- .../livable/server/core/util/TestProperties.java | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/livable/server/core/util/TestProperties.java diff --git a/.gitignore b/.gitignore index 6f2d0d36..89fc3b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ out/ !**/src/test/**/out/ application-dev.yml application-prod.yml +/src/test/resources/test.properties ### NetBeans ### /nbproject/private/ diff --git a/src/test/java/com/livable/server/LivableServerApplicationTests.java b/src/test/java/com/livable/server/LivableServerApplicationTests.java index 52bd189e..ab1ac589 100644 --- a/src/test/java/com/livable/server/LivableServerApplicationTests.java +++ b/src/test/java/com/livable/server/LivableServerApplicationTests.java @@ -1,9 +1,10 @@ package com.livable.server; -import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.util.TestProperties; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +@TestProperties @SpringBootTest class LivableServerApplicationTests { diff --git a/src/test/java/com/livable/server/core/util/TestProperties.java b/src/test/java/com/livable/server/core/util/TestProperties.java new file mode 100644 index 00000000..63d9f5c1 --- /dev/null +++ b/src/test/java/com/livable/server/core/util/TestProperties.java @@ -0,0 +1,14 @@ +package com.livable.server.core.util; + +import org.springframework.test.context.TestPropertySource; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@TestPropertySource(locations = "classpath:test.properties") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestProperties { +} From 8bea9944a49895aa8884bc841c67c4570e03f0e3 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:17:47 +0900 Subject: [PATCH 065/111] =?UTF-8?q?API=20Response=20Format=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../visitation/controller/VisitationController.java | 4 ++-- .../server/visitation/dto/VisitationResponse.java | 13 +++++++++++++ .../visitation/service/VisitationFacadeService.java | 8 ++++++-- .../controller/VisitationControllerTest.java | 6 ++++-- .../service/VisitationFacadeServiceTest.java | 10 +++++++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index d1498fc8..97cc5b2d 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -39,9 +39,9 @@ public ResponseEntity> createQrCode(@LoginActor Acto JwtTokenProvider.checkVisitorToken(actor); Long visitorId = actor.getId(); - String base64QrCode = visitationFacadeService.createQrCode(visitorId); + VisitationResponse.Base64QrCode qrCode = visitationFacadeService.createQrCode(visitorId); - return ApiResponse.success(base64QrCode, HttpStatus.OK); + return ApiResponse.success(qrCode, HttpStatus.OK); } @PostMapping("/qr") diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java index a7bef65a..b01f8894 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -10,6 +10,19 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VisitationResponse { + @Getter + @NoArgsConstructor + public static class Base64QrCode { + + private String qr; + + public static Base64QrCode of(String base64QrCode) { + Base64QrCode code = new Base64QrCode(); + code.qr = base64QrCode; + return code; + } + } + @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index bf1a51d8..0ceb20d0 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -23,10 +23,14 @@ public VisitationResponse.DetailInformationDto findVisitationDetailInformation(L return visitorService.findVisitationDetailInformation(visitorId); } - public String createQrCode(final Long visitorId) { + public VisitationResponse.Base64QrCode createQrCode(final Long visitorId) { VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(visitorId); - return visitationService.createQrCode(invitationTime.getStartDateTime(), invitationTime.getEndDateTime()); + String base64QrCode = visitationService.createQrCode( + invitationTime.getStartDateTime(), invitationTime.getEndDateTime() + ); + + return VisitationResponse.Base64QrCode.of(base64QrCode); } public void validateQrCode(final String qr, final Long visitorId) { diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 4dd5ac9e..c26f8f9b 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -8,6 +8,7 @@ import com.livable.server.core.util.TestConfig; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.dto.VisitationResponse; import com.livable.server.visitation.mock.MockDetailInformationDto; import com.livable.server.visitation.mock.MockRegisterParkingDto; import com.livable.server.visitation.mock.MockValidateQrCodeDto; @@ -75,9 +76,10 @@ void findVisitationDetailInformationSuccessTest() throws Exception { void createQrCodeSuccessTest() throws Exception { String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); String base64QrCode = "base64QrCode์ž„ ใ…‹ใ…‹"; + VisitationResponse.Base64QrCode result = VisitationResponse.Base64QrCode.of(base64QrCode); // given - given(visitationFacadeService.createQrCode(1L)).willReturn(base64QrCode); + given(visitationFacadeService.createQrCode(1L)).willReturn(result); // when ResultActions resultActions = mockMvc.perform( @@ -89,7 +91,7 @@ void createQrCodeSuccessTest() throws Exception { // then resultActions.andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.data").value(base64QrCode)); + .andExpect(jsonPath("$.data.qr").value(base64QrCode)); then(visitationFacadeService).should(times(1)).createQrCode(1L); } diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 52d48073..599b7e9d 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -72,17 +72,21 @@ void createQrCodeSuccessTest() { .startTime(LocalTime.now()) .endTime(LocalTime.now()) .build(); + VisitationResponse.Base64QrCode expected = VisitationResponse.Base64QrCode.of(QR_CODE); given(invitationService.findInvitationTime(anyLong())).willReturn(invitationTimeDto); given(visitationService.createQrCode(any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(QR_CODE); + // When - String qrCode = visitationFacadeService.createQrCode(1L); + VisitationResponse.Base64QrCode qrCode = visitationFacadeService.createQrCode(1L); // Then - assertThat(qrCode).isEqualTo(QR_CODE); + assertThat(qrCode).usingRecursiveComparison().isEqualTo(expected); then(invitationService).should(times(1)).findInvitationTime(anyLong()); - then(visitationService).should(times(1)).createQrCode(any(LocalDateTime.class), any(LocalDateTime.class)); + then(visitationService).should(times(1)).createQrCode(any( + LocalDateTime.class), any(LocalDateTime.class) + ); } @DisplayName("VisitationFacadeService.validateQrCode ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") From 86eec31e48fabd4b5cd7798a366f670576a2f8a7 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sat, 30 Sep 2023 00:09:23 +0900 Subject: [PATCH 066/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ์ดˆ๋Œ€์žฅ ๋ชฉ์ ์„ ํ•œ๊ธ€์—์„œ ์˜์–ด๋กœ ๋ณ€๊ฒฝ (#149) * Refactor: ์ดˆ๋Œ€์žฅ ๋ชฉ์ ์— ๊ด€๋ จ๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฆฌํŽ™ํ† ๋ง (#149) --- .../invitation/domain/InvitationPurpose.java | 12 +++++----- .../controller/InvitationControllerTest.java | 6 ++--- .../repository/InvitationRepositoryTest.java | 2 +- .../service/InvitationServiceTest.java | 24 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java index 0e4049ef..a5d2c227 100644 --- a/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java +++ b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java @@ -8,12 +8,12 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum InvitationPurpose { - MEETING("ํšŒ์˜"), - INTERVIEW("๋ฉด์ ‘"), - PERIOD_WORK("๊ธฐ๊ฐ„ ๊ทผ๋ฌด"), - SEMINAR("์„ธ๋ฏธ๋‚˜"), - AFTER_SERVICE("AS/์ ๊ฒ€"), - ETC("๊ธฐํƒ€"); + MEETING("meeting"), + INTERVIEW("interview"), + PERIOD_WORK("fixedTermWork"), + SEMINAR("seminar"), + AFTER_SERVICE("as"), + ETC("etc"); private final String value; } diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index afad199d..f0384b3e 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -120,7 +120,7 @@ void getAvailablePlacesFail_01() throws Exception { void createInvitationFail_01() throws Exception { // Given InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") .startDate(LocalDateTime.of(2023, 9, 18, 10, 0, 0)) .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) @@ -152,7 +152,7 @@ void createInvitationFail_01() throws Exception { void createInvitationFail_02() throws Exception { // Given InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) @@ -184,7 +184,7 @@ void createInvitationFail_03() throws Exception { } InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) diff --git a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java index 88bd1e39..fdbff34f 100644 --- a/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java +++ b/src/test/java/com/livable/server/invitation/repository/InvitationRepositoryTest.java @@ -81,7 +81,7 @@ void dataInit() { .startTime(START_TIME) .endTime(END_TIME) .description("์•Œ์•„์„œ ์™€") - .purpose("INTERVIEW") + .purpose("interview") .officeName("ํŒจ์ŠคํŠธ์บ ํผ์Šค ์‚ฌ๋ฌด์‹ค") .build(); diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 87457d9a..76fd3647 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -130,7 +130,7 @@ void createInvitationFail_01() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .visitors(List.of( InvitationRequest.VisitorCreateDTO.builder().build(), InvitationRequest.VisitorCreateDTO.builder().build() @@ -150,7 +150,7 @@ void createInvitationFail_02() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) .build(); @@ -169,7 +169,7 @@ void createInvitationFail_03() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .startDate(LocalDateTime.of(2025, 10, 30, 0, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 29, 0, 0, 0)) .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) @@ -192,7 +192,7 @@ void createInvitationFail_04() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) @@ -215,7 +215,7 @@ void createInvitationFail_05() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .startDate(LocalDateTime.of(2025, 10, 30, 10, 30, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .visitors(List.of(InvitationRequest.VisitorCreateDTO.builder().build())) @@ -238,7 +238,7 @@ void createInvitationFail_06() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(1L) .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) @@ -267,7 +267,7 @@ void createInvitationFail_07() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(null) .startDate(LocalDateTime.of(2025, 10, 30, 10, 28, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) @@ -291,7 +291,7 @@ void createInvitationFail_08() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(null) .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 14, 0)) @@ -315,7 +315,7 @@ void createInvitationFail_09() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(null) .startDate(LocalDateTime.of(2025, 10, 30, 10, 28, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 38, 0)) @@ -339,7 +339,7 @@ void createInvitationSuccess_01() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(1L) .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) @@ -373,7 +373,7 @@ void createInvitationSuccess_02() { // Given Long memberId = 1L; InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() - .purpose("๋ฉด์ ‘") + .purpose("interview") .commonPlaceId(null) .startDate(LocalDateTime.of(2025, 10, 30, 10, 0, 0)) .endDate(LocalDateTime.of(2025, 10, 30, 12, 0, 0)) @@ -584,7 +584,7 @@ void updateInvitationFail_03() { given(invitationRepository.findById(anyLong())) .willReturn(Optional.of(Invitation.builder() .id(invitationId) - .purpose("๋ฉด์ ‘") + .purpose("interview") .startDate(dateAfterRequestDate) .member(member) .build() From 125a8c8bcc072edb711b1b4df330cfb42dc38f80 Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Sat, 30 Sep 2023 16:59:15 +0900 Subject: [PATCH 067/111] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=8C=90=EB=A7=A4=ED=95=98=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=9D=91=EB=8B=B5=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ์Œ์‹์ ์—์„œ ํŒ๋งคํ•˜๋Š” ๋ฉ”๋‰ด ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ (#112) * Add: ์Œ์‹์ ์—์„œ ํŒ๋งคํ•˜๋Š” ๋ฉ”๋‰ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ, ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#116) * Add: ์Œ์‹์ ์—์„œ ํŒ๋งคํ•˜๋Š” ๋ฉ”๋‰ด ์‘๋‹ต DTO ๊ธฐ๋Šฅ๊ตฌํ˜„ (#116) * Add: ์Œ์‹์ ์—์„œ ํŒ๋งคํ•˜๋Š” ๋ฉ”๋‰ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ธฐ๋Šฅ๊ตฌํ˜„ (#116) --------- Co-authored-by: edan_cafe --- .../controller/RestaurantController.java | 27 ++++++++++++++----- .../restaurant/dto/RestaurantResponse.java | 19 ++++++------- .../RestaurantCustomRepository.java | 2 ++ .../RestaurantCustomRepositoryImpl.java | 26 +++++++++++++++--- .../restaurant/service/RestaurantService.java | 20 +++++++++++++- 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 25e871da..9371ff5f 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -9,13 +9,11 @@ import com.livable.server.restaurant.dto.RestaurantResponse; import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.service.RestaurantService; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -41,11 +39,26 @@ public ResponseEntity> findRestaurantByCategory( return ApiResponse.success(result, HttpStatus.OK); } + @GetMapping("/{restaurantId}/menus") + public ResponseEntity> sellMenuByRestaurant ( + @PathVariable Long restaurantId, + @LoginActor Actor actor + ) { + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + List result = restaurantService.findMenuList(memberId, restaurantId); + + return ApiResponse.success(result, HttpStatus.OK); + } + @GetMapping("/restaurants") public ResponseEntity>> getRestaurantsByMenu( - @RequestParam("menuId") Long menuId, @LoginActor Actor actor - ) { - + @RequestParam("menuId") Long menuId, @LoginActor Actor actor + ) { + JwtTokenProvider.checkMemberToken(actor); Long memberId = actor.getId(); diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java index 84fe2c77..0609f0d7 100644 --- a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java @@ -1,13 +1,11 @@ package com.livable.server.restaurant.dto; +import com.livable.server.entity.Menu; import com.livable.server.entity.RestaurantCategory; +import lombok.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RestaurantResponse { @@ -29,6 +27,13 @@ public static class NearRestaurantDto { private String url; } + @Getter + @AllArgsConstructor + public static class listMenuDTO { + private Long menuId; + private String menuName; + } + @Getter @Builder @NoArgsConstructor @@ -78,14 +83,10 @@ private static Integer getFloorFromAddress(String address) { if (address.contains("์ง€ํ•˜")) { floor *= -1; } - } - } return floor; } - } - } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java index f9660e05..d002a187 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java @@ -13,4 +13,6 @@ List findRestaurantByBuildingIdAndRestaura RestaurantCategory category, Pageable pageable ); + + List findMenuList(Long restaurantId); } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java index ebbe9ae8..7c008ef4 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java @@ -1,9 +1,6 @@ package com.livable.server.restaurant.repository; -import com.livable.server.entity.QBuilding; -import com.livable.server.entity.QBuildingRestaurantMap; -import com.livable.server.entity.QRestaurant; -import com.livable.server.entity.RestaurantCategory; +import com.livable.server.entity.*; import com.livable.server.restaurant.dto.RestaurantResponse; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.CaseBuilder; @@ -73,4 +70,25 @@ public List findRestaurantByBuildingIdAndR return query.fetchJoin().fetch(); } + + @Override + public List findMenuList(Long restaurantId) { + + final QMenu menu = QMenu.menu; + final QRestaurantMenuMap restaurantMenuMap = QRestaurantMenuMap.restaurantMenuMap; + + JPAQuery query = queryFactory + .select(Projections.constructor(RestaurantResponse.listMenuDTO.class, + menu.id, + menu.name + )) + .from(menu) + .innerJoin(restaurantMenuMap) + .on(menu.id.eq(restaurantMenuMap.menu.id)) + .where(restaurantMenuMap.restaurant.id.eq(restaurantId)); + + return query.fetch(); + } + + } diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java index 8906e29d..67fe2b9a 100644 --- a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -1,7 +1,9 @@ package com.livable.server.restaurant.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Member; import com.livable.server.entity.RestaurantCategory; +import com.livable.server.member.repository.MemberRepository; import com.livable.server.restaurant.domain.RandomGenerator; import com.livable.server.restaurant.domain.RestaurantErrorCode; import com.livable.server.restaurant.dto.RestaurantByMenuProjection; @@ -10,6 +12,7 @@ import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; import com.livable.server.restaurant.repository.RestaurantGroupByMenuProjectionRepository; import com.livable.server.restaurant.repository.RestaurantRepository; +import com.livable.server.review.domain.ReviewErrorCode; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; import java.util.ArrayList; @@ -19,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; @RequiredArgsConstructor @Service @@ -30,6 +34,7 @@ public class RestaurantService { private final RestaurantRepository restaurantRepository; private final VisitorRepository visitorRepository; private final BuildingRestaurantMapRepository buildingRestaurantMapRepository; + private final MemberRepository memberRepository; private final RestaurantGroupByMenuProjectionRepository restaurantGroupByMenuProjectionRepository; public List findNearRestaurantByVisitorIdAndRestaurantCategory( @@ -49,6 +54,19 @@ public List findNearRestaurantByVisitorIdA ); } + + public List findMenuList(Long memberId, Long restaurantId) { + checkExistMemberById(memberId); + + return restaurantRepository.findMenuList(restaurantId); + } + + private Member checkExistMemberById(Long memberId) { + Optional memberOptional = memberRepository.findById(memberId); + + return memberOptional.orElseThrow(() -> new GlobalRuntimeException(ReviewErrorCode.MEMBER_NOT_EXIST)); + } + public List findRestaurantByMenuId(Long menuId, Long memberId) { List restaurantByMenuProjections = restaurantGroupByMenuProjectionRepository.findRestaurantByMenuId(menuId, memberId); @@ -71,4 +89,4 @@ private List getRestaurantsByMenu( return restaurantsByMenuDtos; } -} +} \ No newline at end of file From d24d24629e7718aa03c913453a923cd9d63d14b0 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:10:08 +0900 Subject: [PATCH 068/111] =?UTF-8?q?=EA=B0=80=EC=9E=A5=20=EB=A7=8E=EC=9D=B4?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=ED=95=9C=20=EB=A9=94=EB=89=B4=2010?= =?UTF-8?q?=EC=9C=84=EA=B9=8C=EC=A7=80=20=EC=9D=91=EB=8B=B5=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20#99=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix : #100์˜ ์ž˜๋ชป๋œ Request URI ์ˆ˜์ • (#99) #100์˜ ๋ฒ„๊ทธ ์ˆ˜์ • ์ž˜๋ชป๋œ Request URI ์ˆ˜์ • @RequestParam -> @PathVariable * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต์šฉ DTO ์ถ”๊ฐ€ (#99) * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต์šฉ Repository ๊ตฌํ˜„ (#99) * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต์šฉ Service ๊ตฌํ˜„ (#99) * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต์šฉ Controller ๊ตฌํ˜„ (#99) * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ (#99) Pageable ์œ„์น˜ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ์ฝ”๋“œ ์ˆ˜์ • * Feat : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ (#99) ๋ถˆํ•„์š”ํ•œ validation ์ œ๊ฑฐ ๋ถˆํ•„์š”ํ•œ Error code ์ œ๊ฑฐ * Type : ์ž˜๋ชป๋œ space ์ˆ˜์ • (#99) * Test : ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ž‘์„ฑ (#99) --------- Co-authored-by: jongy Co-authored-by: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> --- .../menu/controller/MenuController.java | 19 ++++ .../server/menu/domain/MenuPaging.java | 14 +++ .../livable/server/menu/dto/MenuResponse.java | 27 ++++++ .../menu/dto/MostSelectedMenuProjection.java | 17 ++++ .../menu/repository/MenuRepository.java | 13 +++ .../server/menu/service/MenuService.java | 82 ++++++++++------- .../controller/RestaurantController.java | 12 ++- .../menu/controller/MenuControllerTest.java | 26 +++++- .../server/menu/service/MenuServiceTest.java | 89 ++++++++++++++++++- .../controller/RestaurantControllerTest.java | 6 +- 10 files changed, 266 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/livable/server/menu/domain/MenuPaging.java create mode 100644 src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index 3c973b4a..1b868fbb 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -1,17 +1,23 @@ package com.livable.server.menu.controller; +import static com.livable.server.menu.domain.MenuPaging.MOST_SELECTED_MENU; + import com.livable.server.core.response.ApiResponse; import com.livable.server.core.response.ApiResponse.Success; import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; +import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.service.MenuService; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -30,4 +36,17 @@ public ResponseEntity>> getRouletteMenus(@LoginAct return ApiResponse.success(rouletteMenuDTOs, HttpStatus.OK); } + @GetMapping("/api/menus/buildings/{buildingId}") + public ResponseEntity>> getMostSelectedMenu(@PathVariable("buildingId") Long buildingId, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); + + //์ถ”ํ›„ ๊ฐ€์ ธ ์˜ค๋Š” ๋ฉ”๋‰ด ์ˆซ์ž ๋ณ€๊ฒฝ์‹œ ๋ณ€๊ฒฝ + Pageable pageable = PageRequest.of(0, MOST_SELECTED_MENU.getLimit()); + + List mostSelectedMenu = menuService.getMostSelectedMenu(buildingId, pageable); + + return ApiResponse.success(mostSelectedMenu, HttpStatus.OK); + } + } diff --git a/src/main/java/com/livable/server/menu/domain/MenuPaging.java b/src/main/java/com/livable/server/menu/domain/MenuPaging.java new file mode 100644 index 00000000..356bd2f4 --- /dev/null +++ b/src/main/java/com/livable/server/menu/domain/MenuPaging.java @@ -0,0 +1,14 @@ +package com.livable.server.menu.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MenuPaging { + + MOST_SELECTED_MENU(10); + + private final Integer limit; + +} diff --git a/src/main/java/com/livable/server/menu/dto/MenuResponse.java b/src/main/java/com/livable/server/menu/dto/MenuResponse.java index 1ba5ff92..0d649011 100644 --- a/src/main/java/com/livable/server/menu/dto/MenuResponse.java +++ b/src/main/java/com/livable/server/menu/dto/MenuResponse.java @@ -1,8 +1,10 @@ package com.livable.server.menu.dto; +import java.time.LocalDate; import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,4 +20,29 @@ public static class RouletteMenuDTO { } + @Getter + @Builder + @AllArgsConstructor + public static class MostSelectedMenuDTO { + + private LocalDate date; + private Integer count; + private Integer rank; + private Long menuId; + private String menuName; + private String menuImage; + + public static MostSelectedMenuDTO from(MostSelectedMenuProjection mostSelectedMenuProjection, Integer rank) { + return MostSelectedMenuDTO.builder() + .date(mostSelectedMenuProjection.getDate()) + .count(mostSelectedMenuProjection.getCount()) + .rank(rank) + .menuId(mostSelectedMenuProjection.getMenuId()) + .menuName(mostSelectedMenuProjection.menuName) + .menuImage(mostSelectedMenuProjection.getMenuImage()) + .build(); + } + + } + } diff --git a/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java b/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java new file mode 100644 index 00000000..910935ab --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java @@ -0,0 +1,17 @@ +package com.livable.server.menu.dto; + +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MostSelectedMenuProjection { + + Integer count; + LocalDate date; + Long menuId; + String menuName; + String menuImage; + +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index 036a0db8..15b0bf5d 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -1,10 +1,13 @@ package com.livable.server.menu.repository; import com.livable.server.entity.Menu; +import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenuProjection; import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -17,4 +20,14 @@ public interface MenuRepository extends JpaRepository { ) List findRouletteMenus(); + @Query( + "SELECT new com.livable.server.menu.dto.MostSelectedMenuProjection(mcr.count, mcr.date, mcr.menu.id, m.name, m.representativeImageUrl) " + + "FROM MenuChoiceResult mcr " + + "JOIN Menu m " + + "ON m.id = mcr.menu.id " + + "WHERE mcr.building.id = :buildingId AND mcr.date = CURRENT_DATE " + + "GROUP BY mcr.date, mcr.menu.id, m.name, m.representativeImageUrl " + + "ORDER BY mcr.count DESC " + ) + List findMostSelectedMenuOrderByCount(@Param("buildingId") Long buildingId, Pageable pageable); } diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java index 3b4ca41a..1ab21326 100644 --- a/src/main/java/com/livable/server/menu/service/MenuService.java +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -1,8 +1,11 @@ package com.livable.server.menu.service; +import com.livable.server.core.exception.ErrorCode; import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenu; import com.livable.server.menu.dto.RouletteMenuProjection; import com.livable.server.menu.repository.MenuRepository; @@ -12,6 +15,7 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -19,53 +23,71 @@ @Slf4j public class MenuService { - private final MenuRepository menuRepository; + private final MenuRepository menuRepository; - public List getRouletteMenus() { + public List getRouletteMenus() { - List rouletteMenuProjections = menuRepository.findRouletteMenus(); + List rouletteMenuProjections = menuRepository.findRouletteMenus(); - isValidateRouletteMenus(rouletteMenuProjections); + isValidateRouletteMenus(rouletteMenuProjections, MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED); - Map> rouletteMenuMap = getMenusGroupByMenuCategory(rouletteMenuProjections); + Map> rouletteMenuMap = getMenusGroupByMenuCategory(rouletteMenuProjections); - return convertToDTO(rouletteMenuMap); - } + return convertToDTO(rouletteMenuMap); + } - private void isValidateRouletteMenus(List rouletteMenuProjections) { - if (rouletteMenuProjections.isEmpty()) { - throw new GlobalRuntimeException((MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED)); - } + private void isValidateRouletteMenus(List projections, ErrorCode errorCode) { + if (projections.isEmpty()) { + throw new GlobalRuntimeException((errorCode)); } - - private Map> getMenusGroupByMenuCategory(List rouletteMenuProjections){ + } - Map> menuGroupByMenuCategoryMap = new LinkedHashMap<>(); + private Map> getMenusGroupByMenuCategory(List rouletteMenuProjections){ - for (RouletteMenuProjection rouletteMenuProjection : rouletteMenuProjections) { + Map> menuGroupByMenuCategoryMap = new LinkedHashMap<>(); - String menuCategoryName = rouletteMenuProjection.getMenuCategoryName(); + for (RouletteMenuProjection rouletteMenuProjection : rouletteMenuProjections) { - RouletteMenu rouletteMenus = RouletteMenu.from(rouletteMenuProjection); + String menuCategoryName = rouletteMenuProjection.getMenuCategoryName(); - menuGroupByMenuCategoryMap.computeIfAbsent( - menuCategoryName, k -> new ArrayList<>()) - .add(rouletteMenus); - } - - return menuGroupByMenuCategoryMap; + RouletteMenu rouletteMenus = RouletteMenu.from(rouletteMenuProjection); + menuGroupByMenuCategoryMap.computeIfAbsent( + menuCategoryName, k -> new ArrayList<>()) + .add(rouletteMenus); } - private List convertToDTO(Map> menuGroupByMenuCategoryMap) { + return menuGroupByMenuCategoryMap; + } - List rouletteMenuDTOS = new ArrayList<>(); + private List convertToDTO(Map> menuGroupByMenuCategoryMap) { - menuGroupByMenuCategoryMap.forEach((key, value) -> - rouletteMenuDTOS.add(new RouletteMenuDTO(key, value)) - ); + List rouletteMenuDTOS = new ArrayList<>(); - return rouletteMenuDTOS; - } + menuGroupByMenuCategoryMap.forEach((key, value) -> + rouletteMenuDTOS.add(new RouletteMenuDTO(key, value)) + ); + + return rouletteMenuDTOS; + } + public List getMostSelectedMenu(Long buildingId, Pageable pageable) { + + List mostSelectedMenuProjections = menuRepository.findMostSelectedMenuOrderByCount(buildingId, pageable); + + return convertToDTO(mostSelectedMenuProjections); + } + + private List convertToDTO(List mostSelectedMenuProjections) { + + List mostSelectedMenus = new ArrayList<>(); + + for (int i = 0; i < mostSelectedMenuProjections.size(); i++) { + int rank = i + 1; + MostSelectedMenuDTO mostSelectedMenuDTO = MostSelectedMenuDTO.from(mostSelectedMenuProjections.get(i), rank); + mostSelectedMenus.add(mostSelectedMenuDTO); + } + + return mostSelectedMenus; + } } diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 9371ff5f..02cc09de 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -13,6 +13,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -38,6 +43,7 @@ public ResponseEntity> findRestaurantByCategory( return ApiResponse.success(result, HttpStatus.OK); } + @GetMapping("/{restaurantId}/menus") public ResponseEntity> sellMenuByRestaurant ( @@ -54,15 +60,15 @@ public ResponseEntity> sellMenuByRestaurant ( return ApiResponse.success(result, HttpStatus.OK); } - @GetMapping("/restaurants") + @GetMapping("/restaurants/menus/{menuId}") public ResponseEntity>> getRestaurantsByMenu( - @RequestParam("menuId") Long menuId, @LoginActor Actor actor + @PathVariable("menuId") Long menuId, @LoginActor Actor actor ) { JwtTokenProvider.checkMemberToken(actor); Long memberId = actor.getId(); - + List restaurantsByMenuDtos = restaurantService.findRestaurantByMenuId(menuId, memberId); return ApiResponse.success(restaurantsByMenuDtos, HttpStatus.OK); diff --git a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java index 2d46e4d8..f3c03b20 100644 --- a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java +++ b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java @@ -1,6 +1,5 @@ package com.livable.server.menu.controller; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -11,6 +10,7 @@ import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.TestConfig; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.service.MenuService; import java.util.ArrayList; @@ -22,6 +22,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -77,4 +79,26 @@ void getRouletteMenusFailed() throws Exception { .andExpect(jsonPath("$.message").value(MenuErrorCode.RETRIEVE_ROULETTE_MENU_FAILED.getMessage())); } + @DisplayName("SUCCESS - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ") + @Test + void getMostSelectedMenusSuccess() throws Exception { + //given + String token = tokenProvider.createActorToken( + ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + Pageable pageable = PageRequest.of(0, 1); + + List mockResponse = new ArrayList<>(); + + given(menuService.getMostSelectedMenu(1L, pageable)) + .willReturn(mockResponse); + + //when & then + mockMvc.perform( + get("/api/menus/buildings/{buildingId}", 1) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + } \ No newline at end of file diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java index 51ca3a2c..10eae888 100644 --- a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -3,16 +3,18 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; +import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenu; import com.livable.server.menu.dto.RouletteMenuProjection; import com.livable.server.menu.repository.MenuRepository; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -21,6 +23,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; @ExtendWith(MockitoExtension.class) class MenuServiceTest { @@ -78,4 +82,87 @@ void getRouletteMenusFailed() { assertThrows(GlobalRuntimeException.class, () -> menuService.getRouletteMenus()); } + + @DisplayName("SUCCESS - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getMostSelectedMenusSuccess() { + + //given + + Long buildingId = 1L; + Integer count = 4; + LocalDate date = LocalDate.now(); + Long menuId = 1L; + String menuName = "๋ฉ”๋‰ด1"; + String menuUrl = "/dummyUrl"; + Integer pageLimit = 1; + Pageable pageable = PageRequest.of(0, pageLimit); + + + MostSelectedMenuProjection mostSelectedMenuProjection = new MostSelectedMenuProjection(count, + date, menuId, menuName, menuUrl); + + List mostSelectedMenuProjections = new ArrayList<>(); + mostSelectedMenuProjections.add(mostSelectedMenuProjection); + + List result = new ArrayList<>(); + + for (int i = 0; i < mostSelectedMenuProjections.size() ; i++) { + MostSelectedMenuDTO mostSelectedMenuDTO = MostSelectedMenuDTO.from(mostSelectedMenuProjection, i); + result.add(mostSelectedMenuDTO); + } + + MostSelectedMenuDTO expected = new MostSelectedMenuDTO(date, count, pageLimit, menuId, menuName, menuUrl); + + + + given(menuRepository.findMostSelectedMenuOrderByCount(buildingId, pageable)) + .willReturn(mostSelectedMenuProjections); + + //when + List actual = + menuService.getMostSelectedMenu(buildingId, pageable); + + //then + assertAll( + () -> assertEquals(expected.getCount(), actual.get(0).getCount()), + () -> assertEquals(expected.getMenuId(), actual.get(0).getMenuId()), + () -> assertEquals(expected.getRank(), actual.get(0).getRank()), + () -> assertEquals(expected.getMenuName(), actual.get(0).getMenuName()), + () -> assertEquals(expected.getMenuImage(), actual.get(0).getMenuImage()), + () -> assertEquals(expected.getDate(), actual.get(0).getDate()) + ); + } + + @DisplayName("FAIELD - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getMostSelectedMenusFailedThrowException() { + + //given + + Pageable pageable = PageRequest.of(0, 1); + + given(menuRepository.findMostSelectedMenuOrderByCount(1L, pageable)) + .willThrow(GlobalRuntimeException.class); + + //when + assertThrows(GlobalRuntimeException.class, () -> + menuService.getMostSelectedMenu(1L, pageable)); + } + + @DisplayName("FAIELD - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") + @Test + void getMostSelectedMenusFailedBadPagingLimit() { + + //given + + Pageable pageable = PageRequest.of(0, 1); + + given(menuRepository.findMostSelectedMenuOrderByCount(1L, pageable)) + .willThrow(GlobalRuntimeException.class); + + //when + assertThrows(GlobalRuntimeException.class, () -> + menuService.getMostSelectedMenu(1L, pageable)); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java index 2bfa7b9e..0a643364 100644 --- a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -91,9 +91,8 @@ void findRestaurantByMenuSuccess() throws Exception { .willReturn(result); // when & then - mockMvc.perform(get("/api/restaurants") + mockMvc.perform(get("/api/restaurants/menus/{menuId}", 1) .header("Authorization", "Bearer " + token) - .queryParam("menuId", "1") ) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").isArray()); @@ -113,9 +112,8 @@ void findRestaurantByMenuFailed() throws Exception { new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU)); // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/restaurants") + mockMvc.perform(get("/api/restaurants/menus/{menuId}", 1) .header("Authorization", "Bearer " + token) - .queryParam("menuId", "1") ) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value( From aaaf883580e2404d93808557b3f687d83f920b50 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:10:20 +0900 Subject: [PATCH 069/111] =?UTF-8?q?Reservation=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=99=84=EB=A3=8C=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ์ธก์ • TPS์— ๋”ฐ๋ฅธ ์ฟผ๋ฆฌ ํŠœ๋‹ (#135) ์ฟผ๋ฆฌ๋ฅผ 2๋ฒˆ ๋ณด๋‚ด๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ • * Test: Converter ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#135) StringToLocalDateConverter ์„ฑ๊ณต ํ…Œ์ŠคํŠธ --- .../InvitationReservationMapRepository.java | 5 +++++ .../ReservationQueryRepository.java | 4 ++++ .../ReservationQueryRepositoryImpl.java | 21 +++++++++++++++++++ .../service/ReservationService.java | 12 +++++++---- .../util/StringToLocalDateConverterTest.java | 21 +++++++++++++++++++ .../service/ReservationServiceTest.java | 14 +++++++++++-- 6 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/livable/server/core/util/StringToLocalDateConverterTest.java diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java index 45287517..4504ac11 100644 --- a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java +++ b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java @@ -5,9 +5,14 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import java.util.List; + public interface InvitationReservationMapRepository extends JpaRepository { @Modifying @Query("delete from InvitationReservationMap irm where irm.invitation.id = :invitationId") void deleteAllByInvitationId(Long invitationId); + + @Query("select ir.reservation.id from InvitationReservationMap ir") + List findAllReservationId(); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index 0b09aef2..a105ad22 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -16,4 +16,8 @@ List findReservationsByCommonPlaceIdAndStartDateAndEndDate( List findNotUsedReservationTime( Long companyId, Long commonPlaceId, LocalDate date ); + + List findNotUsedReservationTimeByUsedReservationIds( + Long companyId, Long commonPlaceId, LocalDate date, List reservationIds + ); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index b894c0a3..d7c7208f 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -96,4 +96,25 @@ public List findNotUsedReservationTime( ) .fetch(); } + + @Override + public List findNotUsedReservationTimeByUsedReservationIds( + Long companyId, Long commonPlaceId, LocalDate date, List reservationIds + ) { + return queryFactory + .select(Projections.constructor(AvailableReservationTimeProjection.class, + reservation.date, + reservation.time + ) + ) + .from(reservation) + .where(reservation.id.notIn( + reservationIds + ), + reservation.company.id.eq(companyId), + reservation.date.eq(date), + reservation.commonPlace.id.eq(commonPlaceId) + ) + .fetch(); + } } diff --git a/src/main/java/com/livable/server/reservation/service/ReservationService.java b/src/main/java/com/livable/server/reservation/service/ReservationService.java index 6f115b19..201e9a95 100644 --- a/src/main/java/com/livable/server/reservation/service/ReservationService.java +++ b/src/main/java/com/livable/server/reservation/service/ReservationService.java @@ -2,6 +2,7 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Member; +import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; import com.livable.server.reservation.dto.AvailableReservationTimeProjection; @@ -12,10 +13,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.*; -import java.util.stream.Collectors; @RequiredArgsConstructor @Service @@ -23,12 +21,15 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final MemberRepository memberRepository; + private final InvitationReservationMapRepository invitationReservationMapRepository; public List findAvailableReservationTimes( Long memberId, Long commonPlaceId, LocalDate date ) { + + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)); @@ -41,8 +42,11 @@ public List findAvailabl private AvailableReservationTimeProjections getAvailableReservationTimeProjections( Long companyId, Long commonPlaceId, LocalDate date ) { + List usedReservationIds = invitationReservationMapRepository.findAllReservationId(); List timeProjections = - reservationRepository.findNotUsedReservationTime(companyId, commonPlaceId, date); + reservationRepository.findNotUsedReservationTimeByUsedReservationIds( + companyId, commonPlaceId, date, usedReservationIds + ); return new AvailableReservationTimeProjections(timeProjections); } diff --git a/src/test/java/com/livable/server/core/util/StringToLocalDateConverterTest.java b/src/test/java/com/livable/server/core/util/StringToLocalDateConverterTest.java new file mode 100644 index 00000000..2416fcb1 --- /dev/null +++ b/src/test/java/com/livable/server/core/util/StringToLocalDateConverterTest.java @@ -0,0 +1,21 @@ +package com.livable.server.core.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class StringToLocalDateConverterTest { + + @DisplayName("StringToLocalDateConverter.convert ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void convertSuccessTest() { + StringToLocalDateConverter converter = new StringToLocalDateConverter(); + + String query = "2023-04-23"; + + assertThat(LocalDate.of(2023, 4, 23)).isEqualTo(converter.convert(query)); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java index 1b3e7118..15ca1658 100644 --- a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java @@ -2,7 +2,9 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Company; +import com.livable.server.entity.InvitationReservationMap; import com.livable.server.entity.Member; +import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; import com.livable.server.reservation.dto.AvailableReservationTimeProjection; @@ -43,6 +45,9 @@ class ReservationServiceTest { @Mock MemberRepository memberRepository; + @Mock + InvitationReservationMapRepository invitationReservationMapRepository; + @DisplayName("ReservationService.findAvailableReservationTimes ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") @Test void findAvailableReservationTimesSuccessTest() { @@ -66,8 +71,12 @@ void findAvailableReservationTimesSuccessTest() { AvailableReservationTimeProjections projections = new AvailableReservationTimeProjections(queryResult); + given(invitationReservationMapRepository.findAllReservationId()).willReturn(List.of(1L, 2L, 3L)); given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); - given(reservationRepository.findNotUsedReservationTime(anyLong(), anyLong(), any(LocalDate.class))) + given(reservationRepository.findNotUsedReservationTimeByUsedReservationIds( + anyLong(), anyLong(), any(LocalDate.class), any(List.class) + ) + ) .willReturn(queryResult); // when @@ -75,9 +84,10 @@ void findAvailableReservationTimesSuccessTest() { reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now()); // then + then(invitationReservationMapRepository).should(times(1)).findAllReservationId(); then(memberRepository).should(times(1)).findById(anyLong()); then(reservationRepository).should(times(1)) - .findNotUsedReservationTime(anyLong(), anyLong(), any(LocalDate.class)); + .findNotUsedReservationTimeByUsedReservationIds(anyLong(), anyLong(), any(LocalDate.class), any(List.class)); assertThat(result).usingRecursiveComparison().isEqualTo(projections.toDto()); } From ac73babd753de489e082dca19e865cefcac5d1c7 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Sat, 30 Sep 2023 19:35:50 +0900 Subject: [PATCH 070/111] =?UTF-8?q?=EC=A3=BC=EB=B3=80=20=EC=8B=9D=EB=8B=B9?= =?UTF-8?q?=20=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=84=20?= =?UTF-8?q?=EC=8B=9C=20=EB=9E=9C=EB=8D=A4=20=EC=B6=94=EC=B6=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/restaurant/domain/RandomPageGenerator.java | 6 +++++- .../repository/RestaurantCustomRepositoryImpl.java | 2 +- .../server/restaurant/service/RestaurantService.java | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java b/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java index 4009a41a..1e8eb3c8 100644 --- a/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java +++ b/src/main/java/com/livable/server/restaurant/domain/RandomPageGenerator.java @@ -16,7 +16,11 @@ public Pageable getRandom(int end) { @Override public Pageable getRandom(int start, int end) { - int randomNumber = getRandomNumber(start, end - DEFAULT_PAGE_SIZE); + if (end < DEFAULT_PAGE_SIZE) { + return PageRequest.of(0, DEFAULT_PAGE_SIZE); + } + + int randomNumber = getRandomNumber(start, end - DEFAULT_PAGE_SIZE + 1); return PageRequest.of(randomNumber, DEFAULT_PAGE_SIZE); } } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java index 7c008ef4..9384b1e8 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java @@ -39,7 +39,7 @@ public List findRestaurantByBuildingIdAndR JPAQuery query = queryFactory - .selectDistinct(Projections.constructor(RestaurantResponse.NearRestaurantDto.class, + .select(Projections.constructor(RestaurantResponse.NearRestaurantDto.class, restaurant.restaurantCategory, restaurant.name, restaurant.thumbnailImageUrl, diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java index 67fe2b9a..ba103fa3 100644 --- a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -15,12 +15,12 @@ import com.livable.server.review.domain.ReviewErrorCode; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; -import java.util.ArrayList; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -49,6 +49,10 @@ public List findNearRestaurantByVisitorIdA category ); + if (nearRestaurantCount == 0) { + return List.of(); + } + return restaurantRepository.findRestaurantByBuildingIdAndRestaurantCategory( buildingId, category, randomGenerator.getRandom(nearRestaurantCount) ); From f2fcc414f8c649e124bdfb6aa2f3ffa57646a8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Sat, 30 Sep 2023 21:51:31 +0900 Subject: [PATCH 071/111] =?UTF-8?q?=EB=AA=A9=ED=91=9C=20=EB=8B=AC=EC=84=B1?= =?UTF-8?q?=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=ED=9A=8D=EB=93=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: PointLog ์—”ํ‹ฐํ‹ฐ์— LocalDate๋ฅผ ๋ฐ›์•„ ํ•ด๋‹น ๋‚ ์งœ์— ์ž‘์„ฑ๋˜์—ˆ๋Š”์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#145) * feat: ํŠน์ • Point์˜ ์›”๋ณ„ ๋‹จ์œ„ PointLog๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ฟผ๋ฆฌ ์ถ”๊ฐ€ (#145) * Modify: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ฟผ๋ฆฌ ์‚ญ์ œ (#145) * Modify: ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ์‚ญ์ œ๋กœ ์ธํ•ด ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋งคํ•‘ ๊ฐ์ฒด ์‚ญ์ œ (#145) * Refactor: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋กœ์ง ์„ฑ๋Šฅ ๊ฐœ์„  (#145) ๊ธฐ์กด 4๋ฒˆ์˜ IO๊ฐ€ ๋ฐœ์ƒํ•˜๋˜ ๋กœ์ง์„ 2๋ฒˆ์˜ IO๊ฐ€ ๋ฐœ์ƒํ•˜๋„๋ก ๊ธฐ๋Šฅ ๊ฐœ์„  * Test: ํ…Œ์ŠคํŠธ ๋ฒ ์ด์Šค ์ฝ”๋“œ ์ž‘์„ฑ (#145) --- .../com/livable/server/entity/PointLog.java | 9 ++ .../server/point/dto/PointProjection.java | 26 ---- .../point/repository/PointLogRepository.java | 14 ++- .../point/repository/PointRepository.java | 24 ---- .../server/point/service/PointService.java | 116 ++++++++++++------ .../point/service/PointServiceTest.java | 55 ++++++++- 6 files changed, 154 insertions(+), 90 deletions(-) delete mode 100644 src/main/java/com/livable/server/point/dto/PointProjection.java diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 0b0d9eb2..e33e7fb2 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -3,6 +3,8 @@ import lombok.*; import javax.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; @Getter @Builder @@ -29,4 +31,11 @@ public class PointLog extends BaseTimeEntity { @Column(nullable = false) private Integer amount; + + public boolean isCreated(LocalDate date) { + LocalDateTime createdAt = this.getCreatedAt(); + LocalDate createdDate = LocalDate.of(createdAt.getYear(), createdAt.getMonth(), createdAt.getDayOfMonth()); + + return createdDate.equals(date); + } } diff --git a/src/main/java/com/livable/server/point/dto/PointProjection.java b/src/main/java/com/livable/server/point/dto/PointProjection.java deleted file mode 100644 index d40d1486..00000000 --- a/src/main/java/com/livable/server/point/dto/PointProjection.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.livable.server.point.dto; - -import com.livable.server.entity.Review; -import lombok.*; - -import java.time.LocalDateTime; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class PointProjection { - - @Getter - @AllArgsConstructor - public static class CountAndDateDTO { - - private Long count; - private LocalDateTime mostRecentCreatedDate; - } - - @Getter - @AllArgsConstructor - public static class ReviewAndDateDTO { - - private Review review; - private LocalDateTime createdAt; - } -} diff --git a/src/main/java/com/livable/server/point/repository/PointLogRepository.java b/src/main/java/com/livable/server/point/repository/PointLogRepository.java index 84063d78..bfe7869d 100644 --- a/src/main/java/com/livable/server/point/repository/PointLogRepository.java +++ b/src/main/java/com/livable/server/point/repository/PointLogRepository.java @@ -5,11 +5,19 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; public interface PointLogRepository extends JpaRepository { - @Query(value = "SELECT * FROM point_log WHERE DATE(created_at) = :date", nativeQuery = true) - List findLogsByDate(@Param("date") LocalDate date); + @Query(value = "SELECT * " + + "FROM point_log " + + "WHERE point_id = :pointId " + + "AND created_at BETWEEN :startDate AND :endDate " + + "ORDER BY created_at DESC", nativeQuery = true) + List findDateRangeOfPointLogByPointId( + @Param("pointId") Long pointId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate + ); } diff --git a/src/main/java/com/livable/server/point/repository/PointRepository.java b/src/main/java/com/livable/server/point/repository/PointRepository.java index 143a01c8..7d7c929d 100644 --- a/src/main/java/com/livable/server/point/repository/PointRepository.java +++ b/src/main/java/com/livable/server/point/repository/PointRepository.java @@ -2,7 +2,6 @@ import com.livable.server.entity.Point; import com.livable.server.entity.PointCode; -import com.livable.server.point.dto.PointProjection; import com.livable.server.point.dto.PointResponse; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -26,27 +25,4 @@ PointResponse.ReviewCountDTO findPointCountById( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("codes") List codes); - - @Query("SELECT new com.livable.server.point.dto.PointProjection$CountAndDateDTO(COUNT(pl.id), MAX(pl.createdAt)) " + - "FROM PointLog pl " + - "WHERE pl.point.id = :pointId " + - "AND pl.createdAt BETWEEN :startDate AND :endDate " + - "AND pl.code IN (:codes)") - PointProjection.CountAndDateDTO findCountAndDateById( - @Param("pointId") Long pointId, - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - @Param("codes") List codes); - - @Query("SELECT new com.livable.server.point.dto.PointProjection$ReviewAndDateDTO(pl.review, pl.createdAt) " + - "FROM PointLog pl " + - "WHERE pl.point.id = :pointId " + - "AND pl.createdAt BETWEEN :startDate AND :endDate " + - "AND pl.code IN (:codes)" + - "ORDER BY pl.createdAt DESC") - List findReviewAndDateById( - @Param("pointId") Long pointId, - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - @Param("codes") List codes); } diff --git a/src/main/java/com/livable/server/point/service/PointService.java b/src/main/java/com/livable/server/point/service/PointService.java index 343e808e..3077c8f8 100644 --- a/src/main/java/com/livable/server/point/service/PointService.java +++ b/src/main/java/com/livable/server/point/service/PointService.java @@ -9,7 +9,6 @@ import com.livable.server.point.domain.DateRange; import com.livable.server.point.domain.PointAchievement; import com.livable.server.point.domain.PointErrorCode; -import com.livable.server.point.dto.PointProjection; import com.livable.server.point.dto.PointResponse; import com.livable.server.point.repository.PointLogRepository; import com.livable.server.point.repository.PointRepository; @@ -51,65 +50,110 @@ public PointResponse.ReviewCountDTO getMyReviewCount(Long memberId, LocalDateTim @Transactional public void getAchievementPoint(Long memberId, LocalDateTime requestDateTime) { - System.out.println("requestDateTime:" + requestDateTime); - // ํšŒ์› ์ •๋ณด๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆ - Point point = pointRepository.findByMemberId(memberId).orElseThrow(() -> + final Point point = pointRepository.findByMemberId(memberId).orElseThrow(() -> new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); - // ๊ธˆ์ผ ์ด๋ฏธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์•˜๋Š”์ง€ ํ™•์ธ + // ํšŒ์›์˜ ์š”์ฒญ ๋‚ ์งœ์— ๋Œ€ํ•œ ํ•œ๋‹ฌ ๋ฒ”์œ„์˜ ํฌ์ธํŠธ ๋กœ๊ทธ๋ฅผ ๊ฒ€์ƒ‰ + final List pointLogs = this.getPointLogPerMonthBy(requestDateTime, point); + PointLog recentPointLog = this.getRecentPointLogFrom(pointLogs); LocalDate requestDate = dateFactory.getPureDate(requestDateTime); - List logsByDate = pointLogRepository.findLogsByDate(requestDate); - logsByDate.forEach(pointLog -> { - if (PointAchievement.POINT_CODES.contains(pointLog.getCode())) { - throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY); - } - }); - - // ํ˜„์žฌ์˜ ๋…„-์›” ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ๋ฆฌ๋ทฐ๋ฅผ ์กฐํšŒ - DateRange requestedDateOfMonthRange = dateFactory.getMonthRangeOf(requestDateTime); - List reviewAndDates = pointRepository.findReviewAndDateById( - point.getId(), - requestedDateOfMonthRange.getStartDate(), - requestedDateOfMonthRange.getEndDate(), - PointCode.getReviewPointCodes() - ); - PointProjection.ReviewAndDateDTO lastReview = reviewAndDates.get(0); + // ๊ธˆ์ผ ์ด๋ฏธ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์•˜๋Š”์ง€ ํ™•์ธ + this.validationAchievementPointAlreadyPaid(pointLogs, requestDate); - Review review = lastReview.getReview(); - LocalDateTime lastCreatedDate = lastReview.getCreatedAt(); - Integer count = reviewAndDates.size(); - PointAchievement pointAchievement; + // ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ๋ชฉํ‘œ ๋‹ฌ์„ฑ์œผ๋กœ ์น˜ํ™˜๋˜๋Š”์ง€ ํ™•์ธ (๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ–ˆ๋Š”์ง€ ํ™•์ธ) + PointAchievement pointAchievement = this.getPointAchievementFrom(pointLogs); - // ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ๋ชฉํ‘œ ๋‹ฌ์„ฑ์œผ๋กœ ์น˜ํ™˜๋˜๋Š”์ง€ ํ™•์ธ - try { - pointAchievement = PointAchievement.valueOf(count); - } catch (InputMismatchException exception) { - throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED); - } - - // ๋ชฉํ‘œ ํฌ์ธํŠธ ์ง€๊ธ‰ ์š”์ฒญ ๋‚ ์งœ๊ฐ€ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋‚ ์งœ์ธ์ง€ ํ™•์ธ - if (lastCreatedDate.getDayOfMonth() != requestDateTime.getDayOfMonth()) { + // ๋ชฉํ‘œ ํฌ์ธํŠธ ์ง€๊ธ‰ ์š”์ฒญ ๋‚ ์งœ๊ฐ€ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋‚ ์งœ์ธ์ง€ ํ™•์ธ + if (!recentPointLog.isCreated(requestDate)) { throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_FAILED); } // ํฌ์ธํŠธ ์ง€๊ธ‰ - this.paidPoints(point, pointAchievement, review); + this.paidPoints(point, pointAchievement, recentPointLog.getReview()); } + /** + * point์˜ ํฌ์ธํŠธ ๋กœ๊ทธ์ค‘ + * requestDateTime์˜ year-month์— ํ•ด๋‹นํ•˜๋Š” ํ•œ๋‹ฌ ๊ฐ„์˜ ํฌ์ธํŠธ ๋กœ๊ทธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
+ * ์ตœ์‹  ์ˆœ์œผ๋กœ ์ •๋ ฌ + * + * @param requestDateTime LocalDateTime + * @param point Point + * @return ํฌ์ธํŠธ ๋กœ๊ทธ์˜ ๋ฆฌ์ŠคํŠธ + */ + private List getPointLogPerMonthBy(LocalDateTime requestDateTime, Point point) { + + DateRange dateRangeOfMonth = dateFactory.getMonthRangeOf(requestDateTime); + + return pointLogRepository.findDateRangeOfPointLogByPointId( + point.getId(), dateRangeOfMonth.getStartDate(), dateRangeOfMonth.getEndDate()); + } + + /** + * ์ตœ์‹  ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ PointLog ๋ฆฌ์ŠคํŠธ ์ค‘ ๊ฐ€์žฅ ์ตœ๊ทผ์˜ ํฌ์ธํŠธ ๋กœ๊ทธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
+ * ํฌ์ธํŠธ ๋กœ๊ทธ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด์žˆ๋‹ค๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค. + * + * @param pointLogs + * @return PointLog + */ + private PointLog getRecentPointLogFrom(List pointLogs) throws GlobalRuntimeException { + return pointLogs.stream().findFirst() + .orElseThrow(() -> new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); + } + + /** + * PointLog ๋ฆฌ์ŠคํŠธ ์ค‘ requestDate์— ๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ ๋ฐ›์€ ์ด๋ ฅ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. + * + * @param pointLogs List + * @param requestDate LocalDate + */ + private void validationAchievementPointAlreadyPaid(List pointLogs, LocalDate requestDate) { + pointLogs.stream() + .filter(pointLog -> pointLog.isCreated(requestDate)) + .forEach(pointLog -> { + if (PointAchievement.POINT_CODES.contains(pointLog.getCode())) { + throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY); + } + }); + } + + /** + * PointLog ๋ฆฌ์ŠคํŠธ ์ค‘ ๋ฆฌ๋ทฐ๋ฅผ ํ†ตํ•ด ์–ป์€ ํฌ์ธํŠธ ๋กœ๊ทธ์˜ ๊ฐœ์ˆ˜๋ฅผ + * PointAchievement ๊ฐ์ฒด๋กœ ์น˜ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
+ * ์น˜ํ™˜์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค. + * + * @param pointLogs List + * @return PointAchievement + */ + private PointAchievement getPointAchievementFrom(List pointLogs) throws GlobalRuntimeException { + try { + int count = (int) pointLogs.stream().filter(pointLog -> { + PointCode code = pointLog.getCode(); + return PointCode.getReviewPointCodes().contains(code); + }).count(); + + return PointAchievement.valueOf(count); + } catch (InputMismatchException exception) { + throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED); + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) public void paidPoints(Point point, PointAchievement pointAchievement, Review review) { Integer amount = pointAchievement.getAmount(); - point.plusPoint(amount); + PointLog pointLog = PointLog.builder() .point(point) .review(review) .code(pointAchievement.getPointCode()) .amount(pointAchievement.getAmount()) .build(); + pointLogRepository.save(pointLog); } } diff --git a/src/test/java/com/livable/server/point/service/PointServiceTest.java b/src/test/java/com/livable/server/point/service/PointServiceTest.java index 4d1df611..8aafea24 100644 --- a/src/test/java/com/livable/server/point/service/PointServiceTest.java +++ b/src/test/java/com/livable/server/point/service/PointServiceTest.java @@ -2,11 +2,17 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Point; +import com.livable.server.entity.PointCode; +import com.livable.server.entity.PointLog; +import com.livable.server.entity.Review; import com.livable.server.point.domain.DateFactory; import com.livable.server.point.domain.DateRange; import com.livable.server.point.dto.PointResponse; import com.livable.server.point.repository.PointRepository; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; @@ -15,6 +21,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @ExtendWith(MockitoExtension.class) @@ -82,4 +89,50 @@ void failure_Test_existPointData() { pointService.getMyReviewCount(memberId, currentDate)); } } + + @Nested + @DisplayName("๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class GetAchievementPoint { + + @DisplayName("์„ฑ๊ณต - ์ผ๊ณฑ๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") + @Test + void success_Test_GivenSevenReviewPointLog_ThenPaidAchievementPoint() { + // Given + Point point = Point.builder().balance(10).build(); + Review review = Review.builder().id(1L).build(); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).build() + ); + // When + + // Then + } + + @DisplayName("์‹คํŒจ - ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑ ํ–ˆ์ง€๋งŒ ํฌ์ธํŠธ ์‹ ์ฒญ ๋‚ ์งœ๊ฐ€ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋‹น์ผ์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_GivenRequestNotAchievingDate_ThenThrowError() { + // Given + Point point = Point.builder().balance(10).build(); + Review review = Review.builder().id(1L).build(); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).build() + ); + + // When + + // Then + } + } } From 7bbfe5b2d5e678ab905349e516dd0923a8d1e641 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sat, 30 Sep 2023 22:54:08 +0900 Subject: [PATCH 072/111] =?UTF-8?q?invitation=20commonPlaceId=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: invitation commonPlaceId ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง ์ฃผ์„ ์ฒ˜๋ฆฌ (#160) --- .../livable/server/invitation/service/InvitationService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index f1941e89..56c914fd 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -290,7 +290,9 @@ public ResponseEntity updateInvitation(Long invitationId, InvitationRequest.U .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST)); checkInvitationStartDate(invitation); - checkModifiedCommonPlaceId(invitation, dto); + + // TODO: NullPointerException ๋ฐœ์ƒ (์ถ”ํ›„ ๋ฆฌํŽ™ํ† ๋ง) + // checkModifiedCommonPlaceId(invitation, dto); boolean shouldSendToAlreadyVisitor = false; boolean shouldSendToAddedVisitor = checkAddedVisitorsCount(invitation, dto); From 83cb4ed7e902cb61a93fa6365718158841218b0f Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sat, 30 Sep 2023 23:01:02 +0900 Subject: [PATCH 073/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20NullPointerException=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: invitation commonPlaceId ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง ์ฃผ์„ ์ฒ˜๋ฆฌ (#160) * Fix: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • NullPointerException ์ฒ˜๋ฆฌ (#160) --- .../server/invitation/service/InvitationService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 56c914fd..079c48a9 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -290,9 +290,7 @@ public ResponseEntity updateInvitation(Long invitationId, InvitationRequest.U .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST)); checkInvitationStartDate(invitation); - - // TODO: NullPointerException ๋ฐœ์ƒ (์ถ”ํ›„ ๋ฆฌํŽ™ํ† ๋ง) - // checkModifiedCommonPlaceId(invitation, dto); + checkModifiedCommonPlaceId(invitation, dto); boolean shouldSendToAlreadyVisitor = false; boolean shouldSendToAddedVisitor = checkAddedVisitorsCount(invitation, dto); @@ -351,7 +349,7 @@ private boolean isModifiedInvitationDateTime(Invitation invitation, InvitationRe private void checkModifiedCommonPlaceId(Invitation invitation, InvitationRequest.UpdateDTO dto) { Long currentCommonPlaceId = invitationRepository.getCommonPlaceIdByInvitationId(invitation.getId()); - if (!currentCommonPlaceId.equals(dto.getCommonPlaceId())) { + if (currentCommonPlaceId != null && !currentCommonPlaceId.equals(dto.getCommonPlaceId())) { throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); } } From dc305f726e73a15ad4f0c6265d6272ccb4f968f9 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 00:03:20 +0900 Subject: [PATCH 074/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9E=A5=EC=86=8C=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C=20(#164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • ์žฅ๋ณด ๋ณ€๊ฒฝ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€ (#163) * Test: ์ดˆ๋Œ€์žฅ ์ˆ˜์ • ์žฅ์†Œ ๋ณ€๊ฒฝ ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#163) --- .../invitation/service/InvitationService.java | 4 + .../service/InvitationServiceTest.java | 101 ++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java index 079c48a9..510d603c 100644 --- a/src/main/java/com/livable/server/invitation/service/InvitationService.java +++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java @@ -352,6 +352,10 @@ private void checkModifiedCommonPlaceId(Invitation invitation, InvitationRequest if (currentCommonPlaceId != null && !currentCommonPlaceId.equals(dto.getCommonPlaceId())) { throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); } + + if (currentCommonPlaceId == null && dto.getCommonPlaceId() != null) { + throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } } private void reserveNewCommonPlaces(InvitationRequest.UpdateDTO dto, Invitation invitation) { diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 76fd3647..606fc62c 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -634,6 +634,107 @@ void updateInvitationFail_04() { assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.INVALID_INVITATION_MAXIMUM_NUMBER); } + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๊ธฐ์กด CommonPlaceId๊ฐ€ null ์ด๊ณ , ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ commonPlaceId์— ๊ฐ’์ด ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_05() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .member(member) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(null); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๊ธฐ์กด CommonPlaceId์— ๊ฐ’์ด ์žˆ๊ณ , ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ commonPlaceId์ด null ์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_06() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Member member = Member.builder().id(memberId).build(); + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(null) + .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .member(member) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(1L); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ๊ธฐ์กด CommonPlaceId์— ๊ฐ’์ด ์žˆ๊ณ , ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ commonPlaceId์ด ๋‹ค๋ฅธ ๊ฐ’์ธ ๊ฒฝ์šฐ") + @Test + void updateInvitationFail_07() { + // Given + LocalDate requestDate = LocalDate.now(); + LocalDate dateAfterRequestDate = requestDate.plusDays(1L); + Long invitationId = 1L; + Long memberId = 1L; + Long commonPlaceId = 1L; + Member member = Member.builder().id(memberId).build(); + InvitationRequest.UpdateDTO dto = InvitationRequest.UpdateDTO.builder() + .commonPlaceId(commonPlaceId) + .visitors(List.of(InvitationRequest.VisitorForUpdateDTO.builder().build())) + .build(); + + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + given(invitationRepository.findById(anyLong())) + .willReturn(Optional.of(Invitation.builder() + .id(invitationId) + .purpose("ํšŒ์˜") + .startDate(dateAfterRequestDate) + .member(member) + .build() + )); + given(invitationRepository.getCommonPlaceIdByInvitationId(anyLong())).willReturn(2L); + + // When + GlobalRuntimeException exception = assertThrows(GlobalRuntimeException.class, + () -> invitationService.updateInvitation(invitationId, dto, memberId)); + + // Then + assertThat(exception.getErrorCode()).isEqualTo(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION); + } + @DisplayName("[์„ฑ๊ณต] ์ดˆ๋Œ€์žฅ ์ˆ˜์ • - ์‹œ๊ฐ„ ๋ณ€๊ฒฝ X, ์ธ์› ์ถ”๊ฐ€ X") @Test void updateInvitationSuccess_01() { From 2c340fea0d853904cbb6237c41edb5af1b2cbb79 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 16:02:07 +0900 Subject: [PATCH 075/111] =?UTF-8?q?Feat:=20SSH=20=EC=97=B0=EA=B2=B0=20Acti?= =?UTF-8?q?on=20ymal=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..5f645f93 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,24 @@ +name: ssh for release deploy + +on: + push: + branches: [ release ] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: SSH Remote Commands + uses: appleboy/ssh-action@v0.1.4 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + port: ${{ secrets.PORT }} + timeout: 40s + + script: | + echo "SSH ์—ฐ๊ฒฐ ๋˜์—ˆ๋‹ค!" From f250e3d04e854acd826ec3566795a73d5862be2f Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 16:04:07 +0900 Subject: [PATCH 076/111] =?UTF-8?q?Modify:=20Action=20on=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20release=EC=97=90=EC=84=9C=20develop?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5f645f93..95cbe904 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: ssh for release deploy on: push: - branches: [ release ] + branches: [ develop ] jobs: build: From f7983ef87174b2f8c0d14bc954f4e7cc1458d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Sun, 1 Oct 2023 16:19:26 +0900 Subject: [PATCH 077/111] =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify: PointLog ์—”ํ‹ฐํ‹ฐ์— PaidAt ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (#165) --- .../com/livable/server/entity/PointLog.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index e33e7fb2..4466e574 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -32,9 +32,24 @@ public class PointLog extends BaseTimeEntity { @Column(nullable = false) private Integer amount; + @Column(nullable = false) + private LocalDateTime paidAt; + + @PrePersist + private void prePersist() { + paidAt = LocalDateTime.now(); + } + + public boolean isPaid(LocalDate date) { + LocalDateTime paidDateTime = this.getPaidAt(); + LocalDate paidDate = LocalDate.of(paidDateTime.getYear(), paidDateTime.getMonth(), paidDateTime.getDayOfMonth()); + + return paidDate.equals(date); + } + public boolean isCreated(LocalDate date) { - LocalDateTime createdAt = this.getCreatedAt(); - LocalDate createdDate = LocalDate.of(createdAt.getYear(), createdAt.getMonth(), createdAt.getDayOfMonth()); + LocalDateTime createdDateTime = this.getCreatedAt(); + LocalDate createdDate = LocalDate.of(createdDateTime.getYear(), createdDateTime.getMonth(), createdDateTime.getDayOfMonth()); return createdDate.equals(date); } From 2c5dcaf00883ead71be34290a489ea06dbd4b277 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 17:01:48 +0900 Subject: [PATCH 078/111] =?UTF-8?q?Feat:=20Action=20Git=20Pull=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95cbe904..6ac094d6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,4 +21,6 @@ jobs: timeout: 40s script: | - echo "SSH ์—ฐ๊ฒฐ ๋˜์—ˆ๋‹ค!" + echo "Git Pull" + cd ~/livable/server + git pull origin develop From 9510efde1c39b50279707edbea07075268df3bbc Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 17:32:44 +0900 Subject: [PATCH 079/111] =?UTF-8?q?Feat:=20Github=20Action=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ac094d6..608da904 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,27 @@ jobs: timeout: 40s script: | - echo "Git Pull" + echo "1. project root๋กœ ์ด๋™" cd ~/livable/server + + echo "2. git pull" git pull origin develop + + echo "3. project build" + ./gradlew clean build + + if [ $? -eq 0 ]; then + # ๋นŒ๋“œ ์„ฑ๊ณต ์‹œ + echo "4. ์‹คํ–‰์ค‘์ธ ํ”„๋กœ์„ธ์Šค ํ™•์ธ" + CURRENT_PID=$(pgrep -f ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar) + + echo "5. ์‹คํ–‰์ค‘์ธ ํ”„๋กœ์„ธ์Šค ์ค‘์ง€" + sudo kill -9 $CURRENT_PID + + echo "6. ํ™ˆ ๊ฒฝ๋กœ ์ด๋™ ํ›„ ํ”„๋กœ์ ํŠธ ์žฌ์‹คํ–‰" + cd ~ + nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod & + else + echo "ํ”„๋กœ์ ํŠธ Build ์‹คํŒจ" + exit 1 + fi From 053e7e9bd82892300d998d9d1f59c5dfb6f9e0c7 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Sun, 1 Oct 2023 17:44:50 +0900 Subject: [PATCH 080/111] =?UTF-8?q?Modify:=20Shell=20Script=20=EB=B0=B1?= =?UTF-8?q?=EA=B7=B8=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 608da904..4ccdd11b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,7 +40,8 @@ jobs: echo "6. ํ™ˆ ๊ฒฝ๋กœ ์ด๋™ ํ›„ ํ”„๋กœ์ ํŠธ ์žฌ์‹คํ–‰" cd ~ - nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod & + nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod 2>&1 & + disown else echo "ํ”„๋กœ์ ํŠธ Build ์‹คํŒจ" exit 1 From 772c9d4d1201ab7f5b23672892bb9fae60817e4b Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Sun, 1 Oct 2023 20:38:06 +0900 Subject: [PATCH 081/111] =?UTF-8?q?Modify:=20=EB=B0=B1=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C,=20errorlog=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4ccdd11b..49eb7679 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,8 +40,7 @@ jobs: echo "6. ํ™ˆ ๊ฒฝ๋กœ ์ด๋™ ํ›„ ํ”„๋กœ์ ํŠธ ์žฌ์‹คํ–‰" cd ~ - nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod 2>&1 & - disown + nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > nohup.out 2> nohup.err < /dev/null & else echo "ํ”„๋กœ์ ํŠธ Build ์‹คํŒจ" exit 1 From 2fa6b8c7f8f6564bba905461dc32c01c5f1fb9ec Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:16:55 +0900 Subject: [PATCH 082/111] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add: ์Œ์‹์  ๊ฒ€์ƒ‰ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„ (#154) * Add: ์Œ์‹์  ๊ฒ€์ƒ‰ ์‘๋‹ตDTO ๊ตฌํ˜„ (#154) * Add: ์Œ์‹์  ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#154) * Add: ์Œ์‹์  ๊ฒ€์ƒ‰ ์ปจํŠธ๋กค๋Ÿฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#154) --------- Co-authored-by: edan_cafe --- .../controller/RestaurantController.java | 18 ++++- .../restaurant/dto/RestaurantResponse.java | 66 ++++++++++++++----- .../RestaurantCustomRepository.java | 2 + .../RestaurantCustomRepositoryImpl.java | 45 +++++++++++++ .../restaurant/service/RestaurantService.java | 14 ++++ 5 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 02cc09de..519e7246 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -45,8 +45,8 @@ public ResponseEntity> findRestaurantByCategory( } - @GetMapping("/{restaurantId}/menus") - public ResponseEntity> sellMenuByRestaurant ( + @GetMapping("restaurant/{restaurantId}/menus") + public ResponseEntity>> sellMenuByRestaurant ( @PathVariable Long restaurantId, @LoginActor Actor actor ) { @@ -73,4 +73,18 @@ public ResponseEntity>> getRestaurantsByMenu( return ApiResponse.success(restaurantsByMenuDtos, HttpStatus.OK); } + + @GetMapping("/restaurants/search") + public ResponseEntity>> searchRestaurant( + @RequestParam("query") String keyword, + @LoginActor Actor actor + ) { + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + List result = restaurantService.findRestaurantByKeyword(memberId, keyword); + + return ApiResponse.success(result, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java index 0609f0d7..4588b70f 100644 --- a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java @@ -1,6 +1,5 @@ package com.livable.server.restaurant.dto; -import com.livable.server.entity.Menu; import com.livable.server.entity.RestaurantCategory; import lombok.*; import java.util.regex.Matcher; @@ -28,6 +27,7 @@ public static class NearRestaurantDto { } @Getter + @NoArgsConstructor @AllArgsConstructor public static class listMenuDTO { private Long menuId; @@ -63,30 +63,62 @@ public static RestaurantsByMenuDto from(RestaurantByMenuProjection restaurantByM .review(restaurantByMenuProjection.getReview()) .build(); } + } - private static Integer calcEstimatedTime(Integer distance) { - int averageWalkSpeedPerMin = 80; - return distance / averageWalkSpeedPerMin; + @Getter + @Builder + @AllArgsConstructor + public static class SearchRestaurantsDTO { + + private final Long restaurantId; + private final String restaurantName; + private final RestaurantCategory restaurantCategory; + private final Boolean inBuilding; + private final Integer estimatedTime; + private final Integer floor; + private final String thumbnailImageUrl; + + public SearchRestaurantsDTO( + Long restaurantId, + String restaurantName, + RestaurantCategory restaurantCategory, + Boolean inBuilding, + String thumbnailImageUrl, + Integer distance, + String address + ) { + this.restaurantId = restaurantId; + this.restaurantName = restaurantName; + this.restaurantCategory = restaurantCategory; + this.inBuilding = inBuilding; + this.thumbnailImageUrl = thumbnailImageUrl; + this.floor = getFloorFromAddress(address); + this.estimatedTime = calcEstimatedTime(distance); } + } - private static Integer getFloorFromAddress(String address) { + private static Integer getFloorFromAddress(String address) { - int floor = 0; + int floor = 0; - if (address.contains("์ธต")) { - String pattern = "\\s(\\d+)์ธต"; - Pattern regexPattern = Pattern.compile(pattern, Pattern.CANON_EQ); - Matcher matcher = regexPattern.matcher(address); - if (matcher.find() && matcher.group(1) != null) { + if (address.contains("์ธต")) { + String pattern = "\\s(\\d+)์ธต"; + Pattern regexPattern = Pattern.compile(pattern, Pattern.CANON_EQ); + Matcher matcher = regexPattern.matcher(address); + if (matcher.find() && matcher.group(1) != null) { - floor = Integer.parseInt(matcher.group(1)); - if (address.contains("์ง€ํ•˜")) { - floor *= -1; - } + floor = Integer.parseInt(matcher.group(1)); + if (address.contains("์ง€ํ•˜")) { + floor *= -1; } } - - return floor; } + + return floor; + } + + private static Integer calcEstimatedTime(Integer distance) { + int averageWalkSpeedPerMin = 80; + return distance / averageWalkSpeedPerMin; } } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java index d002a187..8cbd5f4f 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java @@ -15,4 +15,6 @@ List findRestaurantByBuildingIdAndRestaura ); List findMenuList(Long restaurantId); + + List findRestaurantByKeyword(Long buildingId, String keyword); } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java index 9384b1e8..a9ff4802 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java @@ -6,6 +6,7 @@ import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -14,6 +15,13 @@ import java.util.List; +import static com.livable.server.entity.QBuilding.building; +import static com.livable.server.entity.QBuildingRestaurantMap.buildingRestaurantMap; +import static com.livable.server.entity.QMenu.menu; +import static com.livable.server.entity.QRestaurant.restaurant; +import static com.livable.server.entity.QRestaurantMenuMap.restaurantMenuMap; + + @Repository @RequiredArgsConstructor public class RestaurantCustomRepositoryImpl implements RestaurantCustomRepository { @@ -90,5 +98,42 @@ public List findMenuList(Long restaurantId) { return query.fetch(); } + @Override + public List findRestaurantByKeyword(Long buildingId, String keyword) { + + // TODO : ์„ฑ๋Šฅํ…Œ์ŠคํŠธ ํ•„์š” +// List subQuery = queryFactory +// .select(restaurantMenuMap.restaurant.id) +// .from(menu) +// .innerJoin(restaurantMenuMap).on(menu.id.eq(restaurantMenuMap.menu.id)) +// .where(menu.name.contains(keyword) +// .or(restaurant.name.contains(keyword))) +// .fetch(); + + return queryFactory + .select(Projections.constructor(RestaurantResponse.SearchRestaurantsDTO.class, + restaurant.id, + restaurant.name, + restaurant.restaurantCategory, + buildingRestaurantMap.inBuilding, + restaurant.thumbnailImageUrl, + buildingRestaurantMap.distance, + restaurant.address + )) + .from(building) + .innerJoin(buildingRestaurantMap).on(buildingRestaurantMap.building.id.eq(building.id)) + .innerJoin(restaurant).on(restaurant.id.eq(buildingRestaurantMap.restaurant.id)) + .where( + restaurant.id.in( + JPAExpressions + .select(restaurantMenuMap.restaurant.id) + .from(menu) + .innerJoin(restaurantMenuMap).on(menu.id.eq(restaurantMenuMap.menu.id)) + .where(menu.name.contains(keyword) + .or(restaurant.name.contains(keyword))) + ) + ) + .fetch(); + } } diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java index ba103fa3..9790f0f1 100644 --- a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -1,6 +1,8 @@ package com.livable.server.restaurant.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Building; +import com.livable.server.entity.Company; import com.livable.server.entity.Member; import com.livable.server.entity.RestaurantCategory; import com.livable.server.member.repository.MemberRepository; @@ -93,4 +95,16 @@ private List getRestaurantsByMenu( return restaurantsByMenuDtos; } + public List findRestaurantByKeyword(Long memberId, String keyword) { + checkExistMemberById(memberId); + + Long buildingId = getBuildingByMember(memberId); + + return restaurantRepository.findRestaurantByKeyword(buildingId, keyword); + } + + private Long getBuildingByMember(Long memberId) { + + return memberRepository.findBuildingInfoByMemberId(memberId).get().getBuildingId(); + } } \ No newline at end of file From 7c84f74daafc919df80d46ea3e622a166ef5c6cf Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:47:58 +0900 Subject: [PATCH 083/111] =?UTF-8?q?Reservation=20API=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/controller/ReservationController.java | 5 +++-- .../repository/ReservationQueryRepository.java | 2 +- .../repository/ReservationQueryRepositoryImpl.java | 4 ++-- .../server/reservation/service/ReservationService.java | 9 +++++---- .../controller/ReservationControllerTest.java | 5 +++-- .../reservation/service/ReservationServiceTest.java | 9 ++++----- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/livable/server/reservation/controller/ReservationController.java b/src/main/java/com/livable/server/reservation/controller/ReservationController.java index 30299814..2043f066 100644 --- a/src/main/java/com/livable/server/reservation/controller/ReservationController.java +++ b/src/main/java/com/livable/server/reservation/controller/ReservationController.java @@ -24,14 +24,15 @@ public class ReservationController { @GetMapping("/places/{commonPlaceId}") public ResponseEntity> findAvailableTimes( @PathVariable Long commonPlaceId, - @RequestParam("date") LocalDate localDate, + @RequestParam("startDate") LocalDate startDate, + @RequestParam("endDate") LocalDate endDate, @LoginActor Actor actor ) { JwtTokenProvider.checkMemberToken(actor); List result = - reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, localDate); + reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, startDate, endDate); return ApiResponse.success(result, HttpStatus.OK); } } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java index a105ad22..3db7e3fd 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepository.java @@ -18,6 +18,6 @@ List findNotUsedReservationTime( ); List findNotUsedReservationTimeByUsedReservationIds( - Long companyId, Long commonPlaceId, LocalDate date, List reservationIds + Long companyId, Long commonPlaceId, LocalDate startDate, LocalDate endDate, List reservationIds ); } diff --git a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java index d7c7208f..f8443d4f 100644 --- a/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java +++ b/src/main/java/com/livable/server/reservation/repository/ReservationQueryRepositoryImpl.java @@ -99,7 +99,7 @@ public List findNotUsedReservationTime( @Override public List findNotUsedReservationTimeByUsedReservationIds( - Long companyId, Long commonPlaceId, LocalDate date, List reservationIds + Long companyId, Long commonPlaceId, LocalDate startDate, LocalDate endDate, List reservationIds ) { return queryFactory .select(Projections.constructor(AvailableReservationTimeProjection.class, @@ -112,7 +112,7 @@ public List findNotUsedReservationTimeByUsed reservationIds ), reservation.company.id.eq(companyId), - reservation.date.eq(date), + reservation.date.goe(startDate).and(reservation.date.loe(endDate)), reservation.commonPlace.id.eq(commonPlaceId) ) .fetch(); diff --git a/src/main/java/com/livable/server/reservation/service/ReservationService.java b/src/main/java/com/livable/server/reservation/service/ReservationService.java index 201e9a95..8e6a5173 100644 --- a/src/main/java/com/livable/server/reservation/service/ReservationService.java +++ b/src/main/java/com/livable/server/reservation/service/ReservationService.java @@ -26,7 +26,8 @@ public class ReservationService { public List findAvailableReservationTimes( Long memberId, Long commonPlaceId, - LocalDate date + LocalDate startDate, + LocalDate endDate ) { @@ -34,18 +35,18 @@ public List findAvailabl .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)); AvailableReservationTimeProjections availableReservationTimeProjections = - getAvailableReservationTimeProjections(member.getCompany().getId(), commonPlaceId, date); + getAvailableReservationTimeProjections(member.getCompany().getId(), commonPlaceId, startDate, endDate); return availableReservationTimeProjections.toDto(); } private AvailableReservationTimeProjections getAvailableReservationTimeProjections( - Long companyId, Long commonPlaceId, LocalDate date + Long companyId, Long commonPlaceId, LocalDate startDate, LocalDate endDate ) { List usedReservationIds = invitationReservationMapRepository.findAllReservationId(); List timeProjections = reservationRepository.findNotUsedReservationTimeByUsedReservationIds( - companyId, commonPlaceId, date, usedReservationIds + companyId, commonPlaceId, startDate, endDate, usedReservationIds ); return new AvailableReservationTimeProjections(timeProjections); diff --git a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java index efbd4769..15212a61 100644 --- a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java @@ -58,7 +58,7 @@ void findAvailableTimesSuccessTest() throws Exception { ) .collect(Collectors.toList()); - given(reservationService.findAvailableReservationTimes(anyLong(), anyLong(), any(LocalDate.class))) + given(reservationService.findAvailableReservationTimes(anyLong(), anyLong(), any(LocalDate.class), any(LocalDate.class))) .willReturn(result); // when @@ -66,7 +66,8 @@ void findAvailableTimesSuccessTest() throws Exception { get("/api/reservation/places/1") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) - .queryParam("date", "2023-09-22") + .queryParam("startDate", "2023-09-22") + .queryParam("endDate", "2023-09-22") ); // then diff --git a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java index 15ca1658..8971d4dd 100644 --- a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java @@ -2,7 +2,6 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.entity.Company; -import com.livable.server.entity.InvitationReservationMap; import com.livable.server.entity.Member; import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.domain.MemberErrorCode; @@ -74,20 +73,20 @@ void findAvailableReservationTimesSuccessTest() { given(invitationReservationMapRepository.findAllReservationId()).willReturn(List.of(1L, 2L, 3L)); given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(reservationRepository.findNotUsedReservationTimeByUsedReservationIds( - anyLong(), anyLong(), any(LocalDate.class), any(List.class) + anyLong(), anyLong(), any(LocalDate.class), any(LocalDate.class), any(List.class) ) ) .willReturn(queryResult); // when List result = - reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now()); + reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now(), LocalDate.now().plusDays(1)); // then then(invitationReservationMapRepository).should(times(1)).findAllReservationId(); then(memberRepository).should(times(1)).findById(anyLong()); then(reservationRepository).should(times(1)) - .findNotUsedReservationTimeByUsedReservationIds(anyLong(), anyLong(), any(LocalDate.class), any(List.class)); + .findNotUsedReservationTimeByUsedReservationIds(anyLong(), anyLong(), any(LocalDate.class), any(LocalDate.class), any(List.class)); assertThat(result).usingRecursiveComparison().isEqualTo(projections.toDto()); } @@ -101,7 +100,7 @@ void findAvailableReservationTimesFailTest() { // when GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> - reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now()) + reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now(), LocalDate.now()) ); // then From 7351c0712d33e1c3fb0cabdf9531071fbeba0c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Mon, 2 Oct 2023 01:17:27 +0900 Subject: [PATCH 084/111] =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EA=B0=9C=EC=84=A0=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=AA=A9=ED=91=9C=20=EB=8B=AC=EC=84=B1=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=A7=80=EA=B8=89=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ค๋ฅ˜ ์ถ”๊ฐ€ (#167) * Modify: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ ์‚ญ์ œ (#167) * Refactor: ๊ธฐ์กด์˜ created_at ์ปฌ๋Ÿผ์œผ๋กœ ๋ฒ”์œ„ ๊ฒ€์ƒ‰์„ ์ง€์ •ํ•˜๋˜ ๋ฐฉ์‹์„ paid_at์œผ๋กœ ๋ณ€๊ฒฝ (#167) * Refactor: created_at์œผ๋กœ ๋™์ž‘ํ•˜๋˜ ๋กœ์ง์„ paid_at์ปฌ๋Ÿผ์„ ๊ธฐ์ค€์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก ๋ณ€๊ฒฝ (#167) * Test: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ผ๋ถ€ ๊ตฌํ˜„ (#167) --- .../com/livable/server/entity/PointLog.java | 7 - .../server/point/domain/PointErrorCode.java | 1 + .../point/repository/PointLogRepository.java | 4 +- .../server/point/service/PointService.java | 7 +- .../point/service/PointServiceTest.java | 181 +++++++++++++++--- 5 files changed, 164 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java index 4466e574..cff5c029 100644 --- a/src/main/java/com/livable/server/entity/PointLog.java +++ b/src/main/java/com/livable/server/entity/PointLog.java @@ -46,11 +46,4 @@ public boolean isPaid(LocalDate date) { return paidDate.equals(date); } - - public boolean isCreated(LocalDate date) { - LocalDateTime createdDateTime = this.getCreatedAt(); - LocalDate createdDate = LocalDate.of(createdDateTime.getYear(), createdDateTime.getMonth(), createdDateTime.getDayOfMonth()); - - return createdDate.equals(date); - } } diff --git a/src/main/java/com/livable/server/point/domain/PointErrorCode.java b/src/main/java/com/livable/server/point/domain/PointErrorCode.java index 53a91c36..611bb23b 100644 --- a/src/main/java/com/livable/server/point/domain/PointErrorCode.java +++ b/src/main/java/com/livable/server/point/domain/PointErrorCode.java @@ -11,6 +11,7 @@ public enum PointErrorCode implements ErrorCode { POINT_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํฌ์ธํŠธ ์ •๋ณด์ž…๋‹ˆ๋‹ค."), + POINT_NOT_EXIST_FOR_CURRENT_MONTH(HttpStatus.BAD_REQUEST, "ํ˜„์žฌ ๋‹ฌ์— ์ง€๊ธ‰๋œ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), ACHIEVEMENT_POINT_PAID_FAILED(HttpStatus.BAD_REQUEST, "๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋Š” ๋‹น์ผ์—๋งŒ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."), ACHIEVEMENT_POINT_PAID_ALREADY(HttpStatus.BAD_REQUEST, "๊ธˆ์ผ ๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ด๋ฏธ ์ง€๊ธ‰ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค."), ACHIEVEMENT_POINT_NOT_MATCHED(HttpStatus.BAD_REQUEST, "๋ชฉํ‘œ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); diff --git a/src/main/java/com/livable/server/point/repository/PointLogRepository.java b/src/main/java/com/livable/server/point/repository/PointLogRepository.java index bfe7869d..57dbed80 100644 --- a/src/main/java/com/livable/server/point/repository/PointLogRepository.java +++ b/src/main/java/com/livable/server/point/repository/PointLogRepository.java @@ -13,8 +13,8 @@ public interface PointLogRepository extends JpaRepository { @Query(value = "SELECT * " + "FROM point_log " + "WHERE point_id = :pointId " + - "AND created_at BETWEEN :startDate AND :endDate " + - "ORDER BY created_at DESC", nativeQuery = true) + "AND paid_at BETWEEN :startDate AND :endDate " + + "ORDER BY paid_at DESC", nativeQuery = true) List findDateRangeOfPointLogByPointId( @Param("pointId") Long pointId, @Param("startDate") LocalDateTime startDate, diff --git a/src/main/java/com/livable/server/point/service/PointService.java b/src/main/java/com/livable/server/point/service/PointService.java index 3077c8f8..69c906df 100644 --- a/src/main/java/com/livable/server/point/service/PointService.java +++ b/src/main/java/com/livable/server/point/service/PointService.java @@ -66,7 +66,7 @@ public void getAchievementPoint(Long memberId, LocalDateTime requestDateTime) { PointAchievement pointAchievement = this.getPointAchievementFrom(pointLogs); // ๋ชฉํ‘œ ํฌ์ธํŠธ ์ง€๊ธ‰ ์š”์ฒญ ๋‚ ์งœ๊ฐ€ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋‚ ์งœ์ธ์ง€ ํ™•์ธ - if (!recentPointLog.isCreated(requestDate)) { + if (!recentPointLog.isPaid(requestDate)) { throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_FAILED); } @@ -100,7 +100,7 @@ private List getPointLogPerMonthBy(LocalDateTime requestDateTime, Poin */ private PointLog getRecentPointLogFrom(List pointLogs) throws GlobalRuntimeException { return pointLogs.stream().findFirst() - .orElseThrow(() -> new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST)); + .orElseThrow(() -> new GlobalRuntimeException(PointErrorCode.POINT_NOT_EXIST_FOR_CURRENT_MONTH)); } /** @@ -111,7 +111,7 @@ private PointLog getRecentPointLogFrom(List pointLogs) throws GlobalRu */ private void validationAchievementPointAlreadyPaid(List pointLogs, LocalDate requestDate) { pointLogs.stream() - .filter(pointLog -> pointLog.isCreated(requestDate)) + .filter(pointLog -> pointLog.isPaid(requestDate)) .forEach(pointLog -> { if (PointAchievement.POINT_CODES.contains(pointLog.getCode())) { throw new GlobalRuntimeException(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY); @@ -140,7 +140,6 @@ private PointAchievement getPointAchievementFrom(List pointLogs) throw } } - @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) public void paidPoints(Point point, PointAchievement pointAchievement, Review review) { diff --git a/src/test/java/com/livable/server/point/service/PointServiceTest.java b/src/test/java/com/livable/server/point/service/PointServiceTest.java index 8aafea24..e05adf36 100644 --- a/src/test/java/com/livable/server/point/service/PointServiceTest.java +++ b/src/test/java/com/livable/server/point/service/PointServiceTest.java @@ -7,12 +7,11 @@ import com.livable.server.entity.Review; import com.livable.server.point.domain.DateFactory; import com.livable.server.point.domain.DateRange; +import com.livable.server.point.domain.PointErrorCode; import com.livable.server.point.dto.PointResponse; +import com.livable.server.point.repository.PointLogRepository; import com.livable.server.point.repository.PointRepository; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; @@ -20,6 +19,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -30,6 +30,9 @@ class PointServiceTest { @Mock private PointRepository pointRepository; + @Mock + private PointLogRepository pointLogRepository; + @Mock private DateFactory dateFactory; @@ -94,45 +97,177 @@ void failure_Test_existPointData() { @DisplayName("๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class GetAchievementPoint { - @DisplayName("์„ฑ๊ณต - ์ผ๊ณฑ๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") + @BeforeEach + void setUp() { + LocalDateTime startDate = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime endDte = LocalDateTime.of(2023, 2, 1, 0, 0, 0); + DateRange dateRange = new DateRange(startDate, endDte); + + Mockito.when(dateFactory.getMonthRangeOf(ArgumentMatchers.any(LocalDateTime.class))) + .thenReturn(dateRange); + + LocalDate pureDate = LocalDate.of(2023, 1, 1); + Mockito.when(dateFactory.getPureDate(ArgumentMatchers.any(LocalDateTime.class))) + .thenReturn(pureDate); + } + + @DisplayName("์„ฑ๊ณต - 7๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") @Test void success_Test_GivenSevenReviewPointLog_ThenPaidAchievementPoint() { // Given - Point point = Point.builder().balance(10).build(); + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + Point point = Point.builder().id(1L).balance(10).build(); Review review = Review.builder().id(1L).build(); List MockList = List.of( - PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).build(), - PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).build(), - PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).build() + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).paidAt(paidTime).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build() ); + + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + // When + // Then + pointService.getAchievementPoint(memberId, requestTime); + } + + @DisplayName("์„ฑ๊ณต - 14๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") + @Test + void success_Test_GivenFourTeenReviewPointLog_ThenPaidAchievementPoint() { + // Given + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + Point point = Point.builder().id(1L).balance(10).build(); + Review review = Review.builder().id(1L).build(); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).paidAt(paidTime).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(8L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(9L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(10L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(11L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(12L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(13L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(14L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build() + ); + + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + + // When // Then + pointService.getAchievementPoint(memberId, requestTime); } - @DisplayName("์‹คํŒจ - ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑ ํ–ˆ์ง€๋งŒ ํฌ์ธํŠธ ์‹ ์ฒญ ๋‚ ์งœ๊ฐ€ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ๋‹น์ผ์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @DisplayName("์‹คํŒจ - 3๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๋ฉ”์‹œ์ง€ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") @Test - void failure_Test_GivenRequestNotAchievingDate_ThenThrowError() { + void failure_Test_GivenThreeReviewPointLog_ThenThrowsErrorWithMessage() { // Given - Point point = Point.builder().balance(10).build(); + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + Point point = Point.builder().id(1L).balance(10).build(); Review review = Review.builder().id(1L).build(); List MockList = List.of( - PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).build(), - PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).build(), - PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).build(), - PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).build() + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build() ); + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + // When + // Then + Assertions.assertThrows(GlobalRuntimeException.class, + () -> pointService.getAchievementPoint(memberId, requestTime)); + + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals( + PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED.getMessage(), exception.getMessage()); + } + } + @DisplayName("์‹คํŒจ - 11๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๋ฉ”์‹œ์ง€ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_GivenElevenReviewPointLog_ThenThrowsErrorWithMessage() { + // Given + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + Point point = Point.builder().id(1L).balance(10).build(); + Review review = Review.builder().id(1L).build(); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(8L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(9L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(10L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(11L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build() + ); + + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + + // When // Then + Assertions.assertThrows(GlobalRuntimeException.class, + () -> pointService.getAchievementPoint(memberId, requestTime)); + + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals( + PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED.getMessage(), exception.getMessage()); + } } } } From f6686e4098cb73eccf49843e3dd817c59d21155f Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Mon, 2 Oct 2023 03:53:59 +0900 Subject: [PATCH 085/111] =?UTF-8?q?=EC=98=A4=EC=A0=90=EC=99=84=20=EB=8B=AC?= =?UTF-8?q?=EB=A0=A5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add: ๋ฆฌ๋ทฐ sql result mapping ์ฝ”๋“œ ์ถ”๊ฐ€ (#169) * Add: ๋ฆฌ๋ทฐ ์˜ค์ ์™„ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ ์ถ”๊ฐ€ (#169) * Add: ๋ฆฌ๋ทฐ ์˜ค์ ์™„ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ๊ตฌํ˜„ (#169) * Add: ๋ฆฌ๋ทฐ ์˜ค์ ์™„ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์‘๋‹ต dto ๊ตฌํ˜„ (#169) * Add: ๋ฆฌ๋ทฐ ์˜ค์ ์™„ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„ (#169) --------- Co-authored-by: edan_cafe --- .../com/livable/server/entity/Review.java | 15 ++++ .../review/controller/ReviewController.java | 29 ++++++-- .../server/review/dto/ReviewResponse.java | 23 +++++++ .../ReviewProjectionRepository.java | 68 +++++++++++++++++++ .../server/review/service/ReviewService.java | 13 +++- 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/livable/server/review/dto/ReviewResponse.java create mode 100644 src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java index 00afb006..ff35bcb3 100644 --- a/src/main/java/com/livable/server/entity/Review.java +++ b/src/main/java/com/livable/server/entity/Review.java @@ -1,5 +1,6 @@ package com.livable.server.entity; +import com.livable.server.review.dto.ReviewResponse; import lombok.*; import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedDate; @@ -7,8 +8,21 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; +import java.time.LocalDate; import java.time.LocalDateTime; +@SqlResultSetMapping( + name = "ReviewAllList", + classes = @ConstructorResult( + targetClass = ReviewResponse.CalendarListDTO.class, + columns = { + @ColumnResult(name = "reviewId", type = Long.class), + @ColumnResult(name = "type", type = String.class), + @ColumnResult(name = "reviewImageUrl", type = String.class), + @ColumnResult(name = "reviewDate", type = LocalDate.class) + } + ) +) @Getter @SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -41,3 +55,4 @@ public class Review { @Column(nullable = false) private LocalDateTime updatedAt; } + diff --git a/src/main/java/com/livable/server/review/controller/ReviewController.java b/src/main/java/com/livable/server/review/controller/ReviewController.java index e0611cbd..90881cb3 100644 --- a/src/main/java/com/livable/server/review/controller/ReviewController.java +++ b/src/main/java/com/livable/server/review/controller/ReviewController.java @@ -1,10 +1,12 @@ package com.livable.server.review.controller; import com.livable.server.core.response.ApiResponse; +import com.livable.server.core.response.ApiResponse.Success; import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; import com.livable.server.review.dto.ReviewRequest; +import com.livable.server.review.dto.ReviewResponse; import com.livable.server.review.service.ReviewService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -26,10 +28,12 @@ public class ReviewController { @PostMapping(value = "/lunch-box", consumes = "multipart/form-data") public ResponseEntity createLunchBoxReview( @Valid @RequestPart("data") ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, - @RequestPart(value = "imageFiles") List files + @RequestPart(value = "imageFiles") List files, + @LoginActor Actor actor ) throws IOException { - Long memberId = 1L; + JwtTokenProvider.checkMemberToken(actor); + Long memberId = actor.getId(); reviewService.createLunchBoxReview(lunchBoxCreateDTO, memberId, files); @@ -39,10 +43,12 @@ public ResponseEntity createLunchBoxReview( @PostMapping(value = "/cafeteria", consumes = "multipart/form-data") public ResponseEntity createCafeteriaReview( @Valid @RequestPart("data") ReviewRequest.CafeteriaCreateDTO CafeteriaCreateDTO, - @RequestPart(value = "imageFiles") List files + @RequestPart(value = "imageFiles") List files, + @LoginActor Actor actor ) throws IOException { - Long memberId = 1L; + JwtTokenProvider.checkMemberToken(actor); + Long memberId = actor.getId(); reviewService.createCafeteriaReview(CafeteriaCreateDTO, memberId, files); @@ -63,4 +69,19 @@ public ResponseEntity createRestaurantReview( return ApiResponse.success(HttpStatus.CREATED); } + + @GetMapping("/members") + public ResponseEntity>> calendarListReview( + @RequestParam("year") String year, + @RequestParam("month") String month, + @LoginActor Actor actor + ) { + + JwtTokenProvider.checkMemberToken(actor); + Long memberId = actor.getId(); + + List result = reviewService.findCalendarList(memberId, year, month); + + return ApiResponse.success(result, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/review/dto/ReviewResponse.java b/src/main/java/com/livable/server/review/dto/ReviewResponse.java new file mode 100644 index 00000000..f17991a8 --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/ReviewResponse.java @@ -0,0 +1,23 @@ +package com.livable.server.review.dto; + +import lombok.Getter; + +import java.time.LocalDate; + +public class ReviewResponse { + + @Getter + public static class CalendarListDTO { + private Long reviewId; + private String type; + private String reviewImageUrl; + private LocalDate reviewDate; + + public CalendarListDTO(Long reviewId, String type, String reviewImageUrl, LocalDate reviewDate) { + this.reviewId = reviewId; + this.type = type; + this.reviewImageUrl = reviewImageUrl; + this.reviewDate = reviewDate; + } + } +} diff --git a/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java new file mode 100644 index 00000000..2af155a8 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java @@ -0,0 +1,68 @@ +package com.livable.server.review.repository; + +import com.livable.server.review.dto.ReviewResponse; +import org.springframework.stereotype.Repository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.List; + +@Repository +public class ReviewProjectionRepository { + private static final String FIND_ALL_REVIEWS_BY_YEAR_AND_MONTH_QUERY; + + static { + FIND_ALL_REVIEWS_BY_YEAR_AND_MONTH_QUERY = "select " + + "result.id as reviewId, " + + "result.type as type, " + + "result.url as reviewImageUrl, " + + "result.created_at as reviewDate " + + "from ( " + + "select r.id, date_format(r.created_at, \"%Y-%m-%d\") as created_at, " + + "(select ri3.url " + + "from review_image ri3 " + + "where ri3.id = min(ri.id)) as url, " + + "'cafeteria' as type " + + "from review r " + + "inner join cafeteria_review cr on cr.id = r.id " + + "left join review_image ri " + + "on ri.review_id = r.id " + + "group by r.id " + + "union " + + "select r.id, date_format(r.created_at, \"%Y-%m-%d\") as created_at, " + + "(select ri3.url " + + "from review_image ri3 " + + "where ri3.id = min(ri.id)) as url, " + + "'lunchbox' as type " + + "from review r " + + "inner join lunch_box_review lr on lr.id = r.id " + + "left join review_image ri " + + "on ri.review_id = r.id " + + "group by r.id " + + "union " + + "select r.id, date_format(r.created_at, \"%Y-%m-%d\") as created_at, " + + "(select ri3.url " + + "from review_image ri3 " + + "where ri3.id = min(ri.id)) as url, " + + "'restaurant' as type " + + "from review r " + + "inner join restaurant_review rr on rr.id = r.id " + + "left join review_image ri " + + "on ri.review_id = r.id " + + "group by r.id) as result " + + "where year(result.created_at) = :year and month(result.created_at) = :month " + + "order by result.created_at"; + } + + @PersistenceContext + private EntityManager entityManager; + + public List findCalendarListByYearAndMonth(String year, String month) { + Query query = entityManager.createNativeQuery(FIND_ALL_REVIEWS_BY_YEAR_AND_MONTH_QUERY, "ReviewAllList") + .setParameter("year", year) + .setParameter("month", month); + + return query.getResultList(); + } +} diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java index eeea30f1..aac36e46 100644 --- a/src/main/java/com/livable/server/review/service/ReviewService.java +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -7,7 +7,9 @@ import com.livable.server.restaurant.repository.RestaurantRepository; import com.livable.server.review.domain.ReviewErrorCode; import com.livable.server.review.dto.ReviewRequest; +import com.livable.server.review.dto.ReviewResponse; import com.livable.server.review.repository.ReviewImageRepository; +import com.livable.server.review.repository.ReviewProjectionRepository; import com.livable.server.review.repository.ReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,7 +17,6 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -30,6 +31,7 @@ public class ReviewService { private final MemberRepository memberRepository; private final ReviewImageRepository reviewImageRepository; private final RestaurantRepository restaurantRepository; + private final ReviewProjectionRepository reviewProjectionRepository; private final S3Uploader s3Uploader; @Transactional @@ -42,6 +44,8 @@ public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateD if (!images.isEmpty()) { // add point + // ํ•˜๋ฃจ์— ๋ฆฌ๋ทฐ ํ•œ๊ฐœ๋งŒ ์ธ์ง€ ์ฒดํฌ + // ํฌ์ธํŠธ 10์  ๋„ฃ๊ธฐ // register image List reviewImages = saveImageFiles(review, images); @@ -147,4 +151,11 @@ private List saveImageFiles(Review review, List images) { return reviewImages; } + + public List findCalendarList(Long memberId, String year, String month) { + + checkExistMemberById(memberId); + + return reviewProjectionRepository.findCalendarListByYearAndMonth(year, month); + } } \ No newline at end of file From ff6c44a0be401845f63f4485c8c2a024a6c84e2f Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:21:50 +0900 Subject: [PATCH 086/111] =?UTF-8?q?Reservation=20API=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=99=84=EB=A3=8C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: Reservation QueryParameter ๋ฆฌํŒฉํ† ๋ง (#175) --- .../core/exception/GlobalErrorCode.java | 15 +++++++++ .../exception/GlobalExceptionHandler.java | 4 +-- .../core/util/StringToLocalDateConverter.java | 6 ++++ .../controller/ReservationController.java | 9 +++--- .../domain/ReservationErrorCode.java | 16 ++++++++++ .../domain/ReservationRequest.java | 32 +++++++++++++++++++ .../service/ReservationService.java | 10 +++--- .../controller/ReservationControllerTest.java | 5 +-- .../service/ReservationServiceTest.java | 7 +++- 9 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/livable/server/core/exception/GlobalErrorCode.java create mode 100644 src/main/java/com/livable/server/reservation/domain/ReservationErrorCode.java create mode 100644 src/main/java/com/livable/server/reservation/domain/ReservationRequest.java diff --git a/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java b/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java new file mode 100644 index 00000000..8cdfe15b --- /dev/null +++ b/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java @@ -0,0 +1,15 @@ +package com.livable.server.core.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GlobalErrorCode implements ErrorCode { + + INVALID_TYPE(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java index 3f34db59..455ba006 100644 --- a/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java @@ -15,7 +15,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity methodMethodArgumentTypeMismatchExceptionHandle(MethodArgumentTypeMismatchException e) { - return ApiResponse.error(e.getMessage(), HttpStatus.BAD_REQUEST); + return ApiResponse.error(GlobalErrorCode.INVALID_TYPE.getMessage(), HttpStatus.BAD_REQUEST); } @ExceptionHandler(BindException.class) @@ -36,7 +36,7 @@ public ResponseEntity globalRuntimeExceptionHandle(GlobalRunt public ResponseEntity runtimeExceptionHandle(RuntimeException e) { log.error("runtimeExceptionHandle", e); - return ApiResponse.error(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + return ApiResponse.error(e.getCause().getCause().getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(Exception.class) diff --git a/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java index 028ebaf6..7c1ed921 100644 --- a/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java +++ b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java @@ -1,5 +1,7 @@ package com.livable.server.core.util; +import com.livable.server.core.exception.GlobalErrorCode; +import com.livable.server.core.exception.GlobalRuntimeException; import lombok.extern.slf4j.Slf4j; import org.springframework.core.convert.converter.Converter; @@ -10,6 +12,10 @@ public class StringToLocalDateConverter implements Converter @Override public LocalDate convert(String source) { + try { return LocalDate.parse(source); + } catch (RuntimeException e) { + throw new GlobalRuntimeException(GlobalErrorCode.INVALID_TYPE); + } } } diff --git a/src/main/java/com/livable/server/reservation/controller/ReservationController.java b/src/main/java/com/livable/server/reservation/controller/ReservationController.java index 2043f066..cf6095ea 100644 --- a/src/main/java/com/livable/server/reservation/controller/ReservationController.java +++ b/src/main/java/com/livable/server/reservation/controller/ReservationController.java @@ -4,6 +4,7 @@ import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; +import com.livable.server.reservation.domain.ReservationRequest; import com.livable.server.reservation.dto.ReservationResponse; import com.livable.server.reservation.service.ReservationService; import lombok.RequiredArgsConstructor; @@ -11,7 +12,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; import java.util.List; @RequiredArgsConstructor @@ -24,15 +24,14 @@ public class ReservationController { @GetMapping("/places/{commonPlaceId}") public ResponseEntity> findAvailableTimes( @PathVariable Long commonPlaceId, - @RequestParam("startDate") LocalDate startDate, - @RequestParam("endDate") LocalDate endDate, + @ModelAttribute ReservationRequest.DateQuery dateQuery, @LoginActor Actor actor - ) { + ) { JwtTokenProvider.checkMemberToken(actor); List result = - reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, startDate, endDate); + reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, dateQuery); return ApiResponse.success(result, HttpStatus.OK); } } diff --git a/src/main/java/com/livable/server/reservation/domain/ReservationErrorCode.java b/src/main/java/com/livable/server/reservation/domain/ReservationErrorCode.java new file mode 100644 index 00000000..535e6579 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/domain/ReservationErrorCode.java @@ -0,0 +1,16 @@ +package com.livable.server.reservation.domain; + +import com.livable.server.core.exception.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReservationErrorCode implements ErrorCode { + + INVALID_DATE_RANGE(HttpStatus.BAD_REQUEST, "๋‚ ์งœ ๋ฒ”์œ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + + private final HttpStatus httpStatus; + private final String message; + } diff --git a/src/main/java/com/livable/server/reservation/domain/ReservationRequest.java b/src/main/java/com/livable/server/reservation/domain/ReservationRequest.java new file mode 100644 index 00000000..204cd2b0 --- /dev/null +++ b/src/main/java/com/livable/server/reservation/domain/ReservationRequest.java @@ -0,0 +1,32 @@ +package com.livable.server.reservation.domain; + +import com.livable.server.core.exception.GlobalRuntimeException; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReservationRequest { + + + @Getter + public static class DateQuery { + + private final LocalDate startDate; + private final LocalDate endDate; + + public DateQuery(LocalDate startDate, LocalDate endDate) { + validateRange(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validateRange(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new GlobalRuntimeException(ReservationErrorCode.INVALID_DATE_RANGE); + } + } + } +} diff --git a/src/main/java/com/livable/server/reservation/service/ReservationService.java b/src/main/java/com/livable/server/reservation/service/ReservationService.java index 8e6a5173..ed8151c4 100644 --- a/src/main/java/com/livable/server/reservation/service/ReservationService.java +++ b/src/main/java/com/livable/server/reservation/service/ReservationService.java @@ -5,6 +5,7 @@ import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.domain.ReservationRequest; import com.livable.server.reservation.dto.AvailableReservationTimeProjection; import com.livable.server.reservation.dto.AvailableReservationTimeProjections; import com.livable.server.reservation.dto.ReservationResponse; @@ -26,8 +27,7 @@ public class ReservationService { public List findAvailableReservationTimes( Long memberId, Long commonPlaceId, - LocalDate startDate, - LocalDate endDate + ReservationRequest.DateQuery dateQuery ) { @@ -35,18 +35,18 @@ public List findAvailabl .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)); AvailableReservationTimeProjections availableReservationTimeProjections = - getAvailableReservationTimeProjections(member.getCompany().getId(), commonPlaceId, startDate, endDate); + getAvailableReservationTimeProjections(member.getCompany().getId(), commonPlaceId, dateQuery); return availableReservationTimeProjections.toDto(); } private AvailableReservationTimeProjections getAvailableReservationTimeProjections( - Long companyId, Long commonPlaceId, LocalDate startDate, LocalDate endDate + Long companyId, Long commonPlaceId, ReservationRequest.DateQuery dateQuery ) { List usedReservationIds = invitationReservationMapRepository.findAllReservationId(); List timeProjections = reservationRepository.findNotUsedReservationTimeByUsedReservationIds( - companyId, commonPlaceId, startDate, endDate, usedReservationIds + companyId, commonPlaceId, dateQuery.getStartDate(), dateQuery.getEndDate(), usedReservationIds ); return new AvailableReservationTimeProjections(timeProjections); diff --git a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java index 15212a61..46af7cf8 100644 --- a/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/com/livable/server/reservation/controller/ReservationControllerTest.java @@ -3,6 +3,7 @@ import com.livable.server.core.util.ActorType; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.TestConfig; +import com.livable.server.reservation.domain.ReservationRequest; import com.livable.server.reservation.dto.ReservationResponse; import com.livable.server.reservation.service.ReservationService; import org.junit.jupiter.api.DisplayName; @@ -57,8 +58,8 @@ void findAvailableTimesSuccessTest() throws Exception { .build() ) .collect(Collectors.toList()); - - given(reservationService.findAvailableReservationTimes(anyLong(), anyLong(), any(LocalDate.class), any(LocalDate.class))) + + given(reservationService.findAvailableReservationTimes(anyLong(), anyLong(), any(ReservationRequest.DateQuery.class))) .willReturn(result); // when diff --git a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java index 8971d4dd..4b0239cc 100644 --- a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java @@ -6,6 +6,7 @@ import com.livable.server.invitation.repository.InvitationReservationMapRepository; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.reservation.domain.ReservationRequest; import com.livable.server.reservation.dto.AvailableReservationTimeProjection; import com.livable.server.reservation.dto.AvailableReservationTimeProjections; import com.livable.server.reservation.dto.ReservationResponse; @@ -70,6 +71,8 @@ void findAvailableReservationTimesSuccessTest() { AvailableReservationTimeProjections projections = new AvailableReservationTimeProjections(queryResult); + ReservationRequest.DateQuery dateQuery = new ReservationRequest.DateQuery(LocalDate.now(), LocalDate.now().plusDays(1)); + given(invitationReservationMapRepository.findAllReservationId()).willReturn(List.of(1L, 2L, 3L)); given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); given(reservationRepository.findNotUsedReservationTimeByUsedReservationIds( @@ -97,10 +100,12 @@ void findAvailableReservationTimesFailTest() { // given given(memberRepository.findById(anyLong())).willReturn(Optional.empty()); + ReservationRequest.DateQuery dateQuery = + new ReservationRequest.DateQuery(LocalDate.now(), LocalDate.now().plusDays(1)); // when GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> - reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now(), LocalDate.now()) + reservationService.findAvailableReservationTimes(1L, 1L, dateQuery) ); // then From 83a245ba24658bbcaf786826c0deffc2473525db Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:29:49 +0900 Subject: [PATCH 087/111] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/reservation/service/ReservationServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java index 4b0239cc..df084c58 100644 --- a/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/livable/server/reservation/service/ReservationServiceTest.java @@ -83,7 +83,7 @@ void findAvailableReservationTimesSuccessTest() { // when List result = - reservationService.findAvailableReservationTimes(1L, 1L, LocalDate.now(), LocalDate.now().plusDays(1)); + reservationService.findAvailableReservationTimes(1L, 1L, dateQuery); // then then(invitationReservationMapRepository).should(times(1)).findAllReservationId(); From 62647fc15a936dede019b3c00994819f5b75ec40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Mon, 2 Oct 2023 15:31:11 +0900 Subject: [PATCH 088/111] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=EC=A0=90=EC=99=84=20=EB=8B=AC=EB=A0=A5=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„ (#180) * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ฟผ๋ฆฌ ์ž‘์„ฑ (#180) * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • (#180) * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์˜ค๋ธŒ์ ํŠธ ๊ตฌํ˜„ (#180) * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์˜ค๋ธŒ์ ํŠธ ๊ตฌํ˜„ (#180) * Feat: ๋ฆฌ๋ทฐ ์ƒ์„ธ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„ (#180) --- .../com/livable/server/entity/Review.java | 16 +++++ .../review/controller/ReviewController.java | 13 ++++ .../livable/server/review/dto/Projection.java | 26 ++++++++ .../server/review/dto/ReviewResponse.java | 29 +++++++++ .../ReviewProjectionRepository.java | 64 +++++++++++++++++++ .../server/review/service/ReviewService.java | 21 ++++++ 6 files changed, 169 insertions(+) diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java index ff35bcb3..eba4ef63 100644 --- a/src/main/java/com/livable/server/entity/Review.java +++ b/src/main/java/com/livable/server/entity/Review.java @@ -1,5 +1,6 @@ package com.livable.server.entity; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.ReviewResponse; import lombok.*; import lombok.experimental.SuperBuilder; @@ -23,6 +24,21 @@ } ) ) +@SqlResultSetMapping( + name = "AllReviewDetailListMapping", + classes = @ConstructorResult( + targetClass = Projection.AllReviewDetailDTO.class, + columns = { + @ColumnResult(name = "reviewTitle", type = String.class), + @ColumnResult(name = "reviewTaste", type = String.class), + @ColumnResult(name = "reviewDescription", type = String.class), + @ColumnResult(name = "reviewCreatedAt", type = String.class), + @ColumnResult(name = "location", type = String.class), + @ColumnResult(name = "images", type = String.class), + @ColumnResult(name = "reviewType", type = String.class) + } + ) +) @Getter @SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/livable/server/review/controller/ReviewController.java b/src/main/java/com/livable/server/review/controller/ReviewController.java index 90881cb3..1fd1ae62 100644 --- a/src/main/java/com/livable/server/review/controller/ReviewController.java +++ b/src/main/java/com/livable/server/review/controller/ReviewController.java @@ -84,4 +84,17 @@ public ResponseEntity>> calendarLis return ApiResponse.success(result, HttpStatus.OK); } + + @GetMapping("/detail/members") + public ResponseEntity>> findAllReviewDetail( + @RequestParam Integer year, + @RequestParam Integer month, + @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); + Long memberId = actor.getId(); + + List result = reviewService.findAllReviewDetailList(memberId, year, month); + return ApiResponse.success(result, HttpStatus.OK); + } } diff --git a/src/main/java/com/livable/server/review/dto/Projection.java b/src/main/java/com/livable/server/review/dto/Projection.java index ad39ad33..258dd839 100644 --- a/src/main/java/com/livable/server/review/dto/Projection.java +++ b/src/main/java/com/livable/server/review/dto/Projection.java @@ -4,6 +4,7 @@ import lombok.*; import java.time.LocalDateTime; +import java.util.Objects; public class Projection { @@ -48,4 +49,29 @@ public static class RestaurantReviewList { private String images; } + + @Getter + @Builder + @AllArgsConstructor + public static class AllReviewDetailDTO { + + private String reviewTitle; + private Evaluation reviewTaste; + private String reviewDescription; + private String reviewCreatedAt; + private String location; + private String images; + private String reviewType; + + public AllReviewDetailDTO(String reviewTitle, String reviewTaste, String reviewDescription, String reviewCreatedAt, String location, String images, String reviewType) { + + this.reviewTitle = reviewTitle; + this.reviewTaste = Objects.isNull(reviewTaste) ? null : Evaluation.valueOf(reviewTaste); + this.reviewDescription = reviewDescription; + this.reviewCreatedAt = reviewCreatedAt; + this.location = location; + this.images = images; + this.reviewType = reviewType; + } + } } diff --git a/src/main/java/com/livable/server/review/dto/ReviewResponse.java b/src/main/java/com/livable/server/review/dto/ReviewResponse.java index f17991a8..70d7ca71 100644 --- a/src/main/java/com/livable/server/review/dto/ReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/ReviewResponse.java @@ -1,8 +1,12 @@ package com.livable.server.review.dto; +import com.livable.server.core.util.ImageSeparator; +import com.livable.server.entity.Evaluation; +import lombok.Builder; import lombok.Getter; import java.time.LocalDate; +import java.util.List; public class ReviewResponse { @@ -20,4 +24,29 @@ public CalendarListDTO(Long reviewId, String type, String reviewImageUrl, LocalD this.reviewDate = reviewDate; } } + + @Getter + @Builder + public static class DetailListDTO { + + private String reviewTitle; + private Evaluation reviewTaste; + private String reviewDescription; + private String reviewCreatedAt; + private String location; + private List images; + private String reviewType; + + public static DetailListDTO valueOf(Projection.AllReviewDetailDTO detailDTO, ImageSeparator imageSeparator) { + return DetailListDTO.builder() + .reviewTitle(detailDTO.getReviewTitle()) + .reviewTaste(detailDTO.getReviewTaste()) + .reviewDescription(detailDTO.getReviewDescription()) + .reviewCreatedAt(detailDTO.getReviewCreatedAt()) + .location(detailDTO.getLocation()) + .images(imageSeparator.separateConcatenatedImages(detailDTO.getImages())) + .reviewType(detailDTO.getReviewType()) + .build(); + } + } } diff --git a/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java index 2af155a8..a8ec8721 100644 --- a/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java +++ b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java @@ -1,16 +1,21 @@ package com.livable.server.review.repository; +import com.livable.server.core.util.ImageSeparator; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.ReviewResponse; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; +import java.time.LocalDateTime; import java.util.List; @Repository public class ReviewProjectionRepository { + private static final String FIND_ALL_REVIEWS_BY_YEAR_AND_MONTH_QUERY; + private static final String FIND_ALL_REVIEW_DETAIL_BETWEEN_DATE_QUERY; static { FIND_ALL_REVIEWS_BY_YEAR_AND_MONTH_QUERY = "select " + @@ -53,6 +58,55 @@ public class ReviewProjectionRepository { "group by r.id) as result " + "where year(result.created_at) = :year and month(result.created_at) = :month " + "order by result.created_at"; + + FIND_ALL_REVIEW_DETAIL_BETWEEN_DATE_QUERY = "SELECT * " + + "FROM (" + + "SELECT " + + "review.selected_dishes as reviewTitle, " + + "restaurant_review.taste as reviewTaste, " + + "review.description as reviewDescription, " + + "date_format(review.created_at, \"%Y-%m-%d\") as reviewCreatedAt, " + + "restaurant.name as location, " + + "GROUP_CONCAT(review_image.url SEPARATOR :separator) AS images, " + + "'restaurant' AS reviewType " + + "FROM review " + + "LEFT JOIN review_image ON review_image.review_id = review.id " + + "INNER JOIN restaurant_review on restaurant_review.id = review.id " + + "INNER JOIN restaurant on restaurant.id = restaurant_review.restaurant_id " + + "WHERE review.member_id = :memberId " + + "GROUP BY review.id, review.member_id " + + "UNION " + + "SELECT " + + "review.selected_dishes as reviewTitle, " + + "cafeteria_review.taste as reviewTaste, " + + "review.description as reviewDescription, " + + "date_format(review.created_at, \"%Y-%m-%d\") as reviewCreatedAt, " + + "building.name as location, " + + "GROUP_CONCAT(review_image.url SEPARATOR :separator) AS images, " + + "'cafeteria' AS reviewType " + + "FROM review " + + "LEFT JOIN review_image ON review_image.review_id = review.id " + + "INNER JOIN cafeteria_review on cafeteria_review.id = review.id " + + "INNER JOIN building on building.id = cafeteria_review.building_id " + + "WHERE review.member_id = :memberId " + + "GROUP BY review.id, review.member_id " + + "UNION " + + "SELECT " + + "review.selected_dishes as reviewTitle, " + + "NULL as reviewTaste, " + + "review.description as reviewDescription, " + + "date_format(review.created_at, \"%Y-%m-%d\") as reviewCreatedAt, " + + "NULL as location, " + + "GROUP_CONCAT(review_image.url SEPARATOR :separator) AS images, " + + "'lunchBox' AS reviewType " + + "FROM review " + + "LEFT JOIN review_image ON review_image.review_id = review.id " + + "INNER JOIN lunch_box_review on lunch_box_review.id = review.id " + + "WHERE review.member_id = :memberId " + + "GROUP BY review.id, review.member_id " + + ") as data " + + "WHERE data.reviewCreatedAt BETWEEN :startDate AND :endDate " + + "ORDER BY data.reviewCreatedAt"; } @PersistenceContext @@ -65,4 +119,14 @@ public List findCalendarListByYearAndMonth(Strin return query.getResultList(); } + + public List findAllReviewDetailBetween(Long memberId, LocalDateTime startDate, LocalDateTime endDate) { + Query query = entityManager.createNativeQuery(FIND_ALL_REVIEW_DETAIL_BETWEEN_DATE_QUERY, "AllReviewDetailListMapping") + .setParameter("separator", ImageSeparator.IMAGE_SEPARATOR) + .setParameter("memberId", memberId) + .setParameter("startDate", startDate) + .setParameter("endDate", endDate); + + return query.getResultList(); + } } diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java index aac36e46..20ae00fb 100644 --- a/src/main/java/com/livable/server/review/service/ReviewService.java +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -1,11 +1,15 @@ package com.livable.server.review.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.core.util.ImageSeparator; import com.livable.server.core.util.S3Uploader; import com.livable.server.entity.*; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.point.domain.DateFactory; +import com.livable.server.point.domain.DateRange; import com.livable.server.restaurant.repository.RestaurantRepository; import com.livable.server.review.domain.ReviewErrorCode; +import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.ReviewRequest; import com.livable.server.review.dto.ReviewResponse; import com.livable.server.review.repository.ReviewImageRepository; @@ -17,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -33,6 +38,8 @@ public class ReviewService { private final RestaurantRepository restaurantRepository; private final ReviewProjectionRepository reviewProjectionRepository; private final S3Uploader s3Uploader; + private final DateFactory dateFactory; + private final ImageSeparator imageSeparator; @Transactional public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, Long memberId, List files) throws IOException { @@ -158,4 +165,18 @@ public List findCalendarList(Long memberId, Stri return reviewProjectionRepository.findCalendarListByYearAndMonth(year, month); } + + @Transactional(readOnly = true) + public List findAllReviewDetailList(Long memberId, Integer year, Integer month) { + + LocalDateTime requestTime = LocalDateTime.of(year, month, 1, 0, 0, 0); + + DateRange requestDateRange = dateFactory.getMonthRangeOf(requestTime); + List allReviewDetailDTOS = reviewProjectionRepository.findAllReviewDetailBetween( + memberId, requestDateRange.getStartDate(), requestDateRange.getEndDate()); + + return allReviewDetailDTOS.stream() + .map(detailDTO -> ReviewResponse.DetailListDTO.valueOf(detailDTO, imageSeparator)) + .collect(Collectors.toList()); + } } \ No newline at end of file From 594634ef9e34acdfda91e0a77331f4798a0a4f3c Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:37:54 +0900 Subject: [PATCH 089/111] =?UTF-8?q?=EB=A3=B0=EB=A0=9B=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat : ์˜ค๋Š˜ ๋‚ ์งœ ๊ฒ€์ฆ์šฉ ์–ด๋…ธํ…Œ์ด์…˜ ๊ตฌํ˜„ (#155) * Feat : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜์šฉ DTO ์ถ”๊ฐ€ (#155) * Feat : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜์šฉ Service ๊ตฌํ˜„ (#155) * Feat : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜์šฉ Repository ๊ตฌํ˜„ (#155) * Feat : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜์šฉ Controller ๊ตฌํ˜„ (#155) * Feat : Entity Update๋ฅผ ์œ„ํ•œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ (#155) * Feat : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜ ์ฝ”๋“œ ์ˆ˜์ • (#155) ๋‹น์ผ ์ด๋ฏธ ๋ฃฐ๋ › ์„ ํƒํ•œ ์œ ์ €์ผ ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธ * Test : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ (#155) --- .../livable/server/core/util/PresentDate.java | 19 +++ .../core/util/PresentDateValidator.java | 21 ++++ .../livable/server/entity/MenuChoiceLog.java | 4 + .../menu/controller/MenuController.java | 17 +++ .../server/menu/domain/MenuErrorCode.java | 6 +- .../menu/domain/MenuValidationMessage.java | 6 + .../livable/server/menu/dto/MenuRequest.java | 34 ++++++ .../repository/MenuChoiceLogRepository.java | 14 +++ .../server/menu/service/MenuService.java | 89 ++++++++++++-- .../menu/controller/MenuControllerTest.java | 112 ++++++++++++++++++ .../server/menu/service/MenuServiceTest.java | 110 +++++++++++++++++ 11 files changed, 423 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/livable/server/core/util/PresentDate.java create mode 100644 src/main/java/com/livable/server/core/util/PresentDateValidator.java create mode 100644 src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java create mode 100644 src/main/java/com/livable/server/menu/dto/MenuRequest.java create mode 100644 src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java diff --git a/src/main/java/com/livable/server/core/util/PresentDate.java b/src/main/java/com/livable/server/core/util/PresentDate.java new file mode 100644 index 00000000..a151d091 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/PresentDate.java @@ -0,0 +1,19 @@ +package com.livable.server.core.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Constraint(validatedBy = PresentDateValidator.class) +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PresentDate { + String message() default "Date๋Š” ์˜ค๋Š˜ ๋‚ ์งœ์™€ ๊ฐ™์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/livable/server/core/util/PresentDateValidator.java b/src/main/java/com/livable/server/core/util/PresentDateValidator.java new file mode 100644 index 00000000..35a2096c --- /dev/null +++ b/src/main/java/com/livable/server/core/util/PresentDateValidator.java @@ -0,0 +1,21 @@ +package com.livable.server.core.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class PresentDateValidator implements ConstraintValidator { + @Override + public boolean isValid(LocalDate date, ConstraintValidatorContext context) { + + if (date == null) { + return false; + } + + LocalDate currentDate = LocalDateTime.now().toLocalDate(); + + return date.isEqual(currentDate); + + } +} diff --git a/src/main/java/com/livable/server/entity/MenuChoiceLog.java b/src/main/java/com/livable/server/entity/MenuChoiceLog.java index 3405f35a..19ad9dc3 100644 --- a/src/main/java/com/livable/server/entity/MenuChoiceLog.java +++ b/src/main/java/com/livable/server/entity/MenuChoiceLog.java @@ -38,4 +38,8 @@ public class MenuChoiceLog extends BaseTimeEntity { @Column(nullable = false, name = "date") private LocalDate date; + + public void updateMenu(Menu changedMenu) { + this.menu = changedMenu; + } } diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index 1b868fbb..e0d5bb32 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -7,10 +7,12 @@ import com.livable.server.core.util.Actor; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.LoginActor; +import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.service.MenuService; import java.util.List; +import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -18,6 +20,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -49,4 +53,17 @@ public ResponseEntity>> getMostSelectedMenu(@P return ApiResponse.success(mostSelectedMenu, HttpStatus.OK); } + @PostMapping("/api/menus/choices") + public ResponseEntity createMenuChoiceLog(@Valid @RequestBody MenuChoiceLogDTO menuChoiceLogDTO, @LoginActor Actor actor) { + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + //์˜ค๋Š˜ ์ด๋ฏธ ๋ฃฐ๋ ›์„ ๋Œ๋ ธ๋‹ค๋ฉด ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜์˜ํ•˜์ง€ ์•Š๊ณ  201 return + menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO); + + return ApiResponse.success(HttpStatus.CREATED); + } + } diff --git a/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java index 0788e875..dda1a7bf 100644 --- a/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java +++ b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java @@ -10,7 +10,11 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum MenuErrorCode implements ErrorCode { - RETRIEVE_ROULETTE_MENU_FAILED(HttpStatus.BAD_REQUEST, "๋ฃฐ๋ › ๋ฉ”๋‰ด ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + RETRIEVE_ROULETTE_MENU_FAILED(HttpStatus.BAD_REQUEST, "๋ฃฐ๋ › ๋ฉ”๋‰ด ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + + MENU_NOT_EXIST(HttpStatus.BAD_REQUEST, "๋ฉ”๋‰ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + + BUILDING_NOT_VALID(HttpStatus.BAD_REQUEST, "๋นŒ๋”ฉ ์ •๋ณด๊ฐ€ ์ผ์น˜ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java b/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java new file mode 100644 index 00000000..254610eb --- /dev/null +++ b/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java @@ -0,0 +1,6 @@ +package com.livable.server.menu.domain; + +public interface MenuValidationMessage { + String NOT_NULL = "๊ฐ’์ด Null ์ผ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + +} diff --git a/src/main/java/com/livable/server/menu/dto/MenuRequest.java b/src/main/java/com/livable/server/menu/dto/MenuRequest.java new file mode 100644 index 00000000..401e3486 --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/MenuRequest.java @@ -0,0 +1,34 @@ +package com.livable.server.menu.dto; + + +import static com.livable.server.menu.domain.MenuValidationMessage.NOT_NULL; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.livable.server.core.util.PresentDate; +import java.time.LocalDate; +import javax.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MenuRequest { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class MenuChoiceLogDTO { + + @NotNull(message = NOT_NULL) + private Long menuId; + + @PresentDate + private LocalDate date; + + } + +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java new file mode 100644 index 00000000..9a9b5d9a --- /dev/null +++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java @@ -0,0 +1,14 @@ +package com.livable.server.menu.repository; + +import com.livable.server.entity.MenuChoiceLog; +import java.time.LocalDate; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MenuChoiceLogRepository extends JpaRepository { + + Optional findByMemberIdAndDate(Long memberId, LocalDate date); + +} diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java index 1ab21326..8ff17cbf 100644 --- a/src/main/java/com/livable/server/menu/service/MenuService.java +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -2,21 +2,33 @@ import com.livable.server.core.exception.ErrorCode; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Building; +import com.livable.server.entity.Company; +import com.livable.server.entity.Member; +import com.livable.server.entity.Menu; +import com.livable.server.entity.MenuChoiceLog; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.repository.MemberRepository; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenu; import com.livable.server.menu.dto.RouletteMenuProjection; +import com.livable.server.menu.repository.MenuChoiceLogRepository; import com.livable.server.menu.repository.MenuRepository; +import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -24,6 +36,8 @@ public class MenuService { private final MenuRepository menuRepository; + private final MenuChoiceLogRepository menuChoiceLogRepository; + private final MemberRepository memberRepository; public List getRouletteMenus() { @@ -79,15 +93,74 @@ public List getMostSelectedMenu(Long buildingId, Pageable p private List convertToDTO(List mostSelectedMenuProjections) { - List mostSelectedMenus = new ArrayList<>(); + List mostSelectedMenus = new ArrayList<>(); - for (int i = 0; i < mostSelectedMenuProjections.size(); i++) { - int rank = i + 1; - MostSelectedMenuDTO mostSelectedMenuDTO = MostSelectedMenuDTO.from(mostSelectedMenuProjections.get(i), rank); - mostSelectedMenus.add(mostSelectedMenuDTO); - } + for (int i = 0; i < mostSelectedMenuProjections.size(); i++) { + int rank = i + 1; + MostSelectedMenuDTO mostSelectedMenuDTO = MostSelectedMenuDTO.from(mostSelectedMenuProjections.get(i), rank); + mostSelectedMenus.add(mostSelectedMenuDTO); + } + + return mostSelectedMenus; + } + + @Transactional + public void createMenuChoiceLog(Long memberId, MenuChoiceLogDTO menuChoiceLogDTO) { + + Optional menuChoiceLogOptional = findMenuChoiceLogOfToday(memberId); + + MenuChoiceLog menuChoiceLog = getMenuchoiceLog(memberId, menuChoiceLogDTO); + + if(menuChoiceLogOptional.isPresent()) { + menuChoiceLog = menuChoiceLogOptional.get(); + updateMenuChoiceLog(menuChoiceLog, menuChoiceLogDTO); + } + + menuChoiceLogRepository.save(menuChoiceLog); + + } + + private void updateMenuChoiceLog(MenuChoiceLog menuChoiceLog, MenuChoiceLogDTO menuChoiceLogDTO) { + Long selectedId = menuChoiceLogDTO.getMenuId(); + + Menu selectedMenu = menuRepository.findById(selectedId) + .orElseThrow(() -> new GlobalRuntimeException(MenuErrorCode.MENU_NOT_EXIST)); + + menuChoiceLog.updateMenu(selectedMenu); + } + + private MenuChoiceLog getMenuchoiceLog(Long memberId, MenuChoiceLogDTO menuChoiceLogDTO) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GlobalRuntimeException( + MemberErrorCode.MEMBER_NOT_EXIST)); + + Company company = member.getCompany(); + + Building building = company.getBuilding(); + + Long menuId = menuChoiceLogDTO.getMenuId(); + + Menu menu = getMenu(menuId); + + LocalDate date = menuChoiceLogDTO.getDate(); + + return MenuChoiceLog.builder() + .member(member) + .building(building) + .menu(menu) + .date(date) + .build(); + } + + private Menu getMenu(Long menuId) { + return menuRepository.findById(menuId) + .orElseThrow(() -> new GlobalRuntimeException( + MenuErrorCode.MENU_NOT_EXIST) + ); + } + + public Optional findMenuChoiceLogOfToday(Long memberId) { - return mostSelectedMenus; + return menuChoiceLogRepository.findByMemberIdAndDate(memberId, LocalDate.now()); } - } diff --git a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java index f3c03b20..9b357e3d 100644 --- a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java +++ b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java @@ -1,21 +1,30 @@ package com.livable.server.menu.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.core.util.ActorType; import com.livable.server.core.util.JwtTokenProvider; import com.livable.server.core.util.TestConfig; +import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.service.MenuService; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.List; +import javax.validation.ConstraintViolationException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +35,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @Import(TestConfig.class) @WebMvcTest(MenuController.class) @@ -34,6 +44,9 @@ class MenuControllerTest { @Autowired MockMvc mockMvc; + @Autowired + ObjectMapper objectMapper; + @Autowired JwtTokenProvider tokenProvider; @@ -101,4 +114,103 @@ void getMostSelectedMenusSuccess() throws Exception { .andExpect(status().isOk()); } + + + @DisplayName("SUCCESS - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ : ์ •์ƒ") + @Test + void createMenuChoiceLogSuccess() throws Exception { + //given + Long memberId = 1L; + + String token = tokenProvider.createActorToken( + ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); + + MenuChoiceLogDTO menuChoiceLogDTO = new MenuChoiceLogDTO(1L, LocalDate.now()); + + doAnswer(invocation -> { + return null; + }).when(menuService).createMenuChoiceLog(anyLong(), any(MenuChoiceLogDTO.class)); + + + //when & then + mockMvc.perform( + MockMvcRequestBuilders.post("/api/menus/choices") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(menuChoiceLogDTO))) + .andExpect(status().isCreated()); + } + + @DisplayName("FAILED - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ : ์˜ค๋Š˜์ด ์•„๋‹Œ ๋‚ ์งœ ์š”์ฒญ") + @Test + void createMenuChoiceLogWithInvalidDate() throws Exception { + //given + Long memberId = 1L; + + String token = tokenProvider.createActorToken( + ActorType.MEMBER, memberId, new Date(new Date().getTime() + 10000000)); + + LocalDate pastDate = LocalDate.now().minusDays(1); + + MenuChoiceLogDTO invalidDto = new MenuChoiceLogDTO(1L, pastDate); + + doThrow(ConstraintViolationException.class) + .when(menuService) + .createMenuChoiceLog(anyLong(), any(MenuChoiceLogDTO.class)); + + //when & then + mockMvc.perform( + MockMvcRequestBuilders.post("/api/menus/choices") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidDto))) + .andExpect(status().isBadRequest()); + } + + @DisplayName("FAILED - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ : ์ž˜๋ชป๋œ ์š”์ฒญ - ์กด์žฌ ํ•˜์ง€ ์•Š๋Š” ์œ ์ €") + @Test + void createMenuChoiceLogInvalidMember() throws Exception { + //given + + String token = tokenProvider.createActorToken( + ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + MenuChoiceLogDTO invalidDto = new MenuChoiceLogDTO(1L, LocalDate.now()); + + doThrow(new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST)) + .when(menuService) + .createMenuChoiceLog(anyLong(), any(MenuChoiceLogDTO.class)); + + //when & then + mockMvc.perform( + MockMvcRequestBuilders.post("/api/menus/choices") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidDto))) + .andExpect(status().isBadRequest()); + } + + @DisplayName("FAILED - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ : ์ž˜๋ชป๋œ ์š”์ฒญ - ์กด์žฌ ํ•˜์ง€ ์•Š๋Š” ๋ฉ”๋‰ด") + @Test + void createMenuChoiceLogInvalidMenu() throws Exception { + //given + + String token = tokenProvider.createActorToken( + ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + MenuChoiceLogDTO invalidDto = new MenuChoiceLogDTO(1L, LocalDate.now()); + + doThrow(new GlobalRuntimeException(MenuErrorCode.MENU_NOT_EXIST)) + .when(menuService) + .createMenuChoiceLog(anyLong(), any(MenuChoiceLogDTO.class)); + + //when & then + mockMvc.perform( + MockMvcRequestBuilders.post("/api/menus/choices") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidDto))) + .andExpect(status().isBadRequest()); + } + } \ No newline at end of file diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java index 10eae888..7a130e62 100644 --- a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -1,22 +1,39 @@ package com.livable.server.menu.service; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Building; +import com.livable.server.entity.Company; +import com.livable.server.entity.Member; +import com.livable.server.entity.Menu; +import com.livable.server.entity.MenuChoiceLog; +import com.livable.server.member.domain.MemberErrorCode; +import com.livable.server.member.repository.MemberRepository; +import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenu; import com.livable.server.menu.dto.RouletteMenuProjection; +import com.livable.server.menu.repository.MenuChoiceLogRepository; import com.livable.server.menu.repository.MenuRepository; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +51,10 @@ class MenuServiceTest { @Mock MenuRepository menuRepository; + @Mock + MenuChoiceLogRepository menuChoiceLogRepository; + @Mock + MemberRepository memberRepository; @DisplayName("SUCCESS - ๋ฃฐ๋ ›์— ์‚ฌ์šฉ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๋ฉ”๋‰ด ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") @Test @@ -165,4 +186,93 @@ void getMostSelectedMenusFailedBadPagingLimit() { assertThrows(GlobalRuntimeException.class, () -> menuService.getMostSelectedMenu(1L, pageable)); } + + @DisplayName("SUCCESS - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ๋‹น์ผ ์ฒซ ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜") + @Test + void createMenuChoiceLog() { + //given + Long memberId = 1L; + MenuChoiceLogDTO menuChoiceLogDTO = new MenuChoiceLogDTO(1L, LocalDate.now()); + + Member mockMember = mock(Member.class); + Company mockCompany = mock(Company.class); + Building mockBuilding = mock(Building.class); + Menu mockMenu = mock(Menu.class); + MenuChoiceLog mockMenuChoiceLog = mock(MenuChoiceLog.class); + + when(memberRepository.findById(anyLong())).thenReturn(Optional.of(mockMember)); + when(menuRepository.findById(anyLong())).thenReturn(Optional.of(mockMenu)); + when(mockMember.getCompany()).thenReturn(mockCompany); + when(mockCompany.getBuilding()).thenReturn(mockBuilding); + when(menuChoiceLogRepository.save(any(MenuChoiceLog.class))).thenReturn(mockMenuChoiceLog); + + //when + menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO); + + //then + verify(menuChoiceLogRepository).save(any(MenuChoiceLog.class)); + } + + @DisplayName("SUCCESS - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ ๋ฐ˜์˜") + @Test + void updateMenuChoiceLog() { + //given + Long memberId = 1L; + MenuChoiceLogDTO menuChoiceLogDTO = new MenuChoiceLogDTO(1L, LocalDate.now()); + + Member mockMember = mock(Member.class); + Company mockCompany = mock(Company.class); + Building mockBuilding = mock(Building.class); + Menu mockMenu = mock(Menu.class); + MenuChoiceLog mockExistingMenuChoiceLog = mock(MenuChoiceLog.class); + + + when(memberRepository.findById(anyLong())).thenReturn(Optional.of(mockMember)); + when(menuRepository.findById(anyLong())).thenReturn(Optional.of(mockMenu)); + when(mockMember.getCompany()).thenReturn(mockCompany); + when(mockCompany.getBuilding()).thenReturn(mockBuilding); + when(menuChoiceLogRepository.findByMemberIdAndDate(anyLong(), any(LocalDate.class))).thenReturn(Optional.of(mockExistingMenuChoiceLog)); + + //when + menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO); + + //then + verify(menuChoiceLogRepository).findByMemberIdAndDate(anyLong(), any(LocalDate.class)); + verify(menuChoiceLogRepository).save(any(MenuChoiceLog.class)); + } + + @DisplayName("FAILED - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ์ž˜๋ชป๋œ ์š”์ฒญ - ์กด์žฌ ํ•˜์ง€ ์•Š๋Š” ์œ ์ €") + @Test + void createMenuChoiceLogInvalidMember() { + //given + Long memberId = 1L; + MenuChoiceLogDTO menuChoiceLogDTO = new MenuChoiceLogDTO(1L, LocalDate.now()); + + //when & then + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> + menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO)); + assertEquals(MemberErrorCode.MEMBER_NOT_EXIST, globalRuntimeException.getErrorCode()); + } + + @DisplayName("FAILED - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ์ž˜๋ชป๋œ ์š”์ฒญ - ์กด์žฌ ํ•˜์ง€ ์•Š๋Š” ๋ฉ”๋‰ด") + @Test + void createMenuChoiceLogInvalidMenu() { + //given + Long memberId = 1L; + MenuChoiceLogDTO menuChoiceLogDTO = new MenuChoiceLogDTO(1L, LocalDate.now()); + + Member mockMember = mock(Member.class); + Company mockCompany = mock(Company.class); + Building mockBuilding = mock(Building.class); + + when(mockMember.getCompany()).thenReturn(mockCompany); + when(mockCompany.getBuilding()).thenReturn(mockBuilding); + when(memberRepository.findById(memberId)).thenReturn(Optional.of(mockMember)); + + //when & then + GlobalRuntimeException globalRuntimeException = assertThrows(GlobalRuntimeException.class, () -> + menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO)); + assertEquals(MenuErrorCode.MENU_NOT_EXIST, globalRuntimeException.getErrorCode()); + } + } \ No newline at end of file From 7258000c1b47d98966120e8c1368892cf89bc030 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:39:50 +0900 Subject: [PATCH 090/111] =?UTF-8?q?=EB=94=94=EB=B2=84=EA=B9=85=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/controller/ReservationController.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/reservation/controller/ReservationController.java b/src/main/java/com/livable/server/reservation/controller/ReservationController.java index cf6095ea..edc46086 100644 --- a/src/main/java/com/livable/server/reservation/controller/ReservationController.java +++ b/src/main/java/com/livable/server/reservation/controller/ReservationController.java @@ -8,15 +8,18 @@ import com.livable.server.reservation.dto.ReservationResponse; import com.livable.server.reservation.service.ReservationService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; @RequiredArgsConstructor @RestController @RequestMapping("/api/reservation") +@Slf4j public class ReservationController { private final ReservationService reservationService; @@ -27,11 +30,15 @@ public ResponseEntity> findAvailableTimes( @ModelAttribute ReservationRequest.DateQuery dateQuery, @LoginActor Actor actor ) { - + log.info("Request Time: {}", LocalDateTime.now()); JwtTokenProvider.checkMemberToken(actor); List result = reservationService.findAvailableReservationTimes(actor.getId(), commonPlaceId, dateQuery); + if (result.size() != 0) { + log.info("time: {}", result.get(0).getAvailableTimes().get(0).toString()); + } + return ApiResponse.success(result, HttpStatus.OK); } } From c2dae46c03ae30be06d88475482b647e12bb319c Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:33:21 +0900 Subject: [PATCH 091/111] =?UTF-8?q?Modify:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=B0=ED=8F=AC=EC=8B=9C=20DB=20=EC=9E=AC=EC=8B=A4?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 49eb7679..ffe8146a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,6 +41,7 @@ jobs: echo "6. ํ™ˆ ๊ฒฝ๋กœ ์ด๋™ ํ›„ ํ”„๋กœ์ ํŠธ ์žฌ์‹คํ–‰" cd ~ nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > nohup.out 2> nohup.err < /dev/null & + sudo service mysql restart else echo "ํ”„๋กœ์ ํŠธ Build ์‹คํŒจ" exit 1 From 6fa8214a7a39108056ed51cc6bd442af00acceae Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:43:04 +0900 Subject: [PATCH 092/111] =?UTF-8?q?Modify:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20DB=20=EC=9E=AC=EC=8B=A4=ED=96=89=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ffe8146a..49eb7679 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,6 @@ jobs: echo "6. ํ™ˆ ๊ฒฝ๋กœ ์ด๋™ ํ›„ ํ”„๋กœ์ ํŠธ ์žฌ์‹คํ–‰" cd ~ nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > nohup.out 2> nohup.err < /dev/null & - sudo service mysql restart else echo "ํ”„๋กœ์ ํŠธ Build ์‹คํŒจ" exit 1 From 0bb8cd9c48fa3f1ac49d73a9aebd9404fb9cce3f Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:55:15 +0900 Subject: [PATCH 093/111] =?UTF-8?q?=EA=B0=80=EC=9E=A5=20=EB=A7=8E=EC=9D=B4?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=ED=95=9C=20=EB=A9=94=EB=89=B4=2010?= =?UTF-8?q?=EC=9C=84=EA=B9=8C=EC=A7=80=20=EC=9D=91=EB=8B=B5,=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=EB=A5=BC=20=ED=8C=90=EB=A7=A4=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=8B=9D=EB=8B=B9=20=EB=AA=A9=EB=A1=9D=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?EndPoint=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../livable/server/menu/controller/MenuController.java | 6 +++--- .../restaurant/controller/RestaurantController.java | 9 +++------ .../server/menu/controller/MenuControllerTest.java | 2 +- .../restaurant/controller/RestaurantControllerTest.java | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index e0d5bb32..1a38f5dc 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -19,9 +19,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -40,8 +40,8 @@ public ResponseEntity>> getRouletteMenus(@LoginAct return ApiResponse.success(rouletteMenuDTOs, HttpStatus.OK); } - @GetMapping("/api/menus/buildings/{buildingId}") - public ResponseEntity>> getMostSelectedMenu(@PathVariable("buildingId") Long buildingId, @LoginActor Actor actor) { + @GetMapping("/api/menus/choices") + public ResponseEntity>> getMostSelectedMenu(@RequestParam("buildingId") Long buildingId, @LoginActor Actor actor) { JwtTokenProvider.checkMemberToken(actor); diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 519e7246..0adccbfc 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -9,7 +9,7 @@ import com.livable.server.restaurant.dto.RestaurantResponse; import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; import com.livable.server.restaurant.service.RestaurantService; -import lombok.Getter; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,9 +18,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.*; - -import java.util.List; @RequiredArgsConstructor @RestController @@ -60,9 +57,9 @@ public ResponseEntity>> return ApiResponse.success(result, HttpStatus.OK); } - @GetMapping("/restaurants/menus/{menuId}") + @GetMapping("/restaurants") public ResponseEntity>> getRestaurantsByMenu( - @PathVariable("menuId") Long menuId, @LoginActor Actor actor + @RequestParam("menuId") Long menuId, @LoginActor Actor actor ) { JwtTokenProvider.checkMemberToken(actor); diff --git a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java index 9b357e3d..5e39b1b6 100644 --- a/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java +++ b/src/test/java/com/livable/server/menu/controller/MenuControllerTest.java @@ -108,7 +108,7 @@ void getMostSelectedMenusSuccess() throws Exception { //when & then mockMvc.perform( - get("/api/menus/buildings/{buildingId}", 1) + get("/api/menus/choices?buildingId=1") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java index 0a643364..2cf9dc18 100644 --- a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -91,7 +91,7 @@ void findRestaurantByMenuSuccess() throws Exception { .willReturn(result); // when & then - mockMvc.perform(get("/api/restaurants/menus/{menuId}", 1) + mockMvc.perform(get("/api/restaurants?menuId=1") .header("Authorization", "Bearer " + token) ) .andExpect(status().isOk()) @@ -112,7 +112,7 @@ void findRestaurantByMenuFailed() throws Exception { new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU)); // when & then - mockMvc.perform(get("/api/restaurants/menus/{menuId}", 1) + mockMvc.perform(get("/api/restaurants?menuId=1") .header("Authorization", "Bearer " + token) ) .andExpect(status().isBadRequest()) From 1c28b710a1b6ef73c6238e2ed911c0d7c30289c5 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 19:24:37 +0900 Subject: [PATCH 094/111] =?UTF-8?q?InvitationServiceTest=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=ED=86=B5=ED=95=A9=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/InvitationServiceTest.java | 47 +++++++++++ .../service/InvitationServiceTest.java | 78 ------------------- 2 files changed, 47 insertions(+), 78 deletions(-) delete mode 100644 src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java diff --git a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java index 606fc62c..16b1f9e5 100644 --- a/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java +++ b/src/test/java/com/livable/server/invitation/service/InvitationServiceTest.java @@ -13,8 +13,12 @@ import com.livable.server.invitation.service.data.InvitationBasicData; import com.livable.server.member.repository.MemberRepository; import com.livable.server.reservation.repository.ReservationRepository; +import com.livable.server.visitation.domain.VisitationErrorCode; +import com.livable.server.visitation.dto.VisitationResponse; +import com.livable.server.visitation.mock.MockInvitationDetailTimeDto; import com.livable.server.visitation.repository.ParkingLogRepository; import com.livable.server.visitation.repository.VisitorRepository; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,6 +40,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) class InvitationServiceTest { @@ -943,4 +949,45 @@ private List createReservations() { )); } + @DisplayName("InvitationService.findInvitationTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findInvitationTimeSuccessTest() { + + // Given + MockInvitationDetailTimeDto mockInvitationDetailTimeDto = new MockInvitationDetailTimeDto(); + Invitation invitation = Invitation.builder() + .startTime(mockInvitationDetailTimeDto.getStartTime()) + .endTime(mockInvitationDetailTimeDto.getEndTime()) + .startDate(mockInvitationDetailTimeDto.getStartDate()) + .endDate(mockInvitationDetailTimeDto.getEndDate()) + .build(); + + given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())) + .willReturn(Optional.of(mockInvitationDetailTimeDto)); + + // When + VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(1L); + + // Then + then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); + AssertionsForClassTypes.assertThat(invitationTime).usingRecursiveComparison().isEqualTo(invitation); + } + + @DisplayName("InvitationService.findInvitationTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") + @Test + void findInvitationTimeFailTest() { + + // Given + given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())).willReturn(Optional.empty()); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows( + GlobalRuntimeException.class, () -> invitationService.findInvitationTime(anyLong()) + ); + + // Then + AssertionsForClassTypes.assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); + then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); + } + } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java b/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java deleted file mode 100644 index b1e67432..00000000 --- a/src/test/java/com/livable/server/visitation/service/InvitationServiceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.livable.server.visitation.service; - -import com.livable.server.core.exception.GlobalRuntimeException; -import com.livable.server.entity.Invitation; -import com.livable.server.invitation.dto.InvitationDetailTimeDto; -import com.livable.server.invitation.service.InvitationService; -import com.livable.server.visitation.domain.VisitationErrorCode; -import com.livable.server.visitation.dto.VisitationResponse; -import com.livable.server.invitation.repository.InvitationRepository; -import com.livable.server.visitation.mock.MockInvitationDetailTimeDto; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Optional; - -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.times; - -@ExtendWith(MockitoExtension.class) -class InvitationServiceTest { - - @InjectMocks - InvitationService invitationService; - - @Mock - InvitationRepository invitationRepository; - - @DisplayName("InvitationService.findInvitationTime ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") - @Test - void findInvitationTimeSuccessTest() { - - // Given - MockInvitationDetailTimeDto mockInvitationDetailTimeDto = new MockInvitationDetailTimeDto(); - Invitation invitation = Invitation.builder() - .startTime(mockInvitationDetailTimeDto.getStartTime()) - .endTime(mockInvitationDetailTimeDto.getEndTime()) - .startDate(mockInvitationDetailTimeDto.getStartDate()) - .endDate(mockInvitationDetailTimeDto.getEndDate()) - .build(); - - given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())) - .willReturn(Optional.of(mockInvitationDetailTimeDto)); - - // When - VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(1L); - - // Then - then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); - assertThat(invitationTime).usingRecursiveComparison().isEqualTo(invitation); - } - - @DisplayName("InvitationService.findInvitationTime ์‹คํŒจ ํ…Œ์ŠคํŠธ") - @Test - void findInvitationTimeFailTest() { - - // Given - given(invitationRepository.findInvitationDetailTimeByVisitorId(anyLong())).willReturn(Optional.empty()); - - // When - GlobalRuntimeException globalRuntimeException = assertThrows( - GlobalRuntimeException.class, () -> invitationService.findInvitationTime(anyLong()) - ); - - // Then - assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.NOT_FOUND); - then(invitationRepository).should(times(1)).findInvitationDetailTimeByVisitorId(anyLong()); - } -} \ No newline at end of file From b9774fe1e3116b4f630c33d683ceec13d5441f23 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Mon, 2 Oct 2023 19:52:56 +0900 Subject: [PATCH 095/111] =?UTF-8?q?=EB=B0=A9=EB=AC=B8=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#190)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/visitation/domain/PlaceType.java | 7 ++++++ .../visitation/dto/VisitationResponse.java | 24 +++++++++++++++++-- .../VisitorCustomRepositoryImpl.java | 5 ++++ .../mock/MockDetailInformationDto.java | 3 +++ .../repository/VisitorRepositoryTest.java | 2 ++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/livable/server/visitation/domain/PlaceType.java diff --git a/src/main/java/com/livable/server/visitation/domain/PlaceType.java b/src/main/java/com/livable/server/visitation/domain/PlaceType.java new file mode 100644 index 00000000..50243fbd --- /dev/null +++ b/src/main/java/com/livable/server/visitation/domain/PlaceType.java @@ -0,0 +1,7 @@ +package com.livable.server.visitation.domain; + +public enum PlaceType { + + COMPANY, + COMMON_PLACE; +} diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java index b01f8894..6d8ae44f 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -1,6 +1,7 @@ package com.livable.server.visitation.dto; import com.livable.server.invitation.dto.InvitationDetailTimeDto; +import com.livable.server.visitation.domain.PlaceType; import lombok.*; import java.time.LocalDate; @@ -25,7 +26,6 @@ public static Base64QrCode of(String base64QrCode) { @Getter @NoArgsConstructor - @AllArgsConstructor public static class DetailInformationDto { private LocalDate invitationStartDate; @@ -41,13 +41,33 @@ public static class DetailInformationDto { private String buildingParkingCostInformation; private String buildingScale; -// private String placeImageUrl; + private PlaceType placeType; private String invitationTip; private String hostName; private String hostCompanyName; private String hostContact; private String hostBusinessCardImageUrl; + + public DetailInformationDto(LocalDate invitationStartDate, LocalTime invitationStartTime, LocalDate invitationEndDate, LocalTime invitationEndTime, String invitationBuildingName, String invitationOfficeName, String buildingRepresentativeImageUrl, String buildingName, String buildingAddress, String buildingParkingCostInformation, String buildingScale, String placeType, String invitationTip, String hostName, String hostCompanyName, String hostContact, String hostBusinessCardImageUrl) { + this.invitationStartDate = invitationStartDate; + this.invitationStartTime = invitationStartTime; + this.invitationEndDate = invitationEndDate; + this.invitationEndTime = invitationEndTime; + this.invitationBuildingName = invitationBuildingName; + this.invitationOfficeName = invitationOfficeName; + this.buildingRepresentativeImageUrl = buildingRepresentativeImageUrl; + this.buildingName = buildingName; + this.buildingAddress = buildingAddress; + this.buildingParkingCostInformation = buildingParkingCostInformation; + this.buildingScale = buildingScale; + this.placeType = PlaceType.valueOf(placeType); + this.invitationTip = invitationTip; + this.hostName = hostName; + this.hostCompanyName = hostCompanyName; + this.hostContact = hostContact; + this.hostBusinessCardImageUrl = hostBusinessCardImageUrl; + } } diff --git a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java index 732cbd28..5c8e4b12 100644 --- a/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/visitation/repository/VisitorCustomRepositoryImpl.java @@ -1,8 +1,10 @@ package com.livable.server.visitation.repository; import com.livable.server.entity.*; +import com.livable.server.visitation.domain.PlaceType; import com.livable.server.visitation.dto.VisitationResponse; import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -35,6 +37,9 @@ public Optional findVisitationDetailInf building.address, building.parkingCostInformation, building.scale, + new CaseBuilder().when(invitation.officeName.contains("์‚ฌ๋ฌด์‹ค")) + .then(PlaceType.COMPANY.name()) + .otherwise(PlaceType.COMMON_PLACE.name()), invitation.description, member.name, company.name, diff --git a/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java b/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java index 56260a83..8eb6e2d1 100644 --- a/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java +++ b/src/test/java/com/livable/server/visitation/mock/MockDetailInformationDto.java @@ -1,5 +1,6 @@ package com.livable.server.visitation.mock; +import com.livable.server.visitation.domain.PlaceType; import com.livable.server.visitation.dto.VisitationResponse; import java.time.LocalDate; @@ -23,6 +24,7 @@ public MockDetailInformationDto( String buildingAddress, String buildingParkingCostInformation, String buildingScale, + String placeType, String invitationTip, String hostName, String hostCompanyName, @@ -41,6 +43,7 @@ public MockDetailInformationDto( buildingAddress, buildingParkingCostInformation, buildingScale, + placeType, invitationTip, hostName, hostCompanyName, diff --git a/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java index 0f2e24f5..0dedd6d3 100644 --- a/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java +++ b/src/test/java/com/livable/server/visitation/repository/VisitorRepositoryTest.java @@ -2,6 +2,7 @@ import com.livable.server.core.config.QueryDslConfig; import com.livable.server.entity.*; +import com.livable.server.visitation.domain.PlaceType; import com.livable.server.visitation.dto.VisitationResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -113,6 +114,7 @@ void findVisitationDetailInformationByIdSuccessTest() { () -> assertThat(detailInformationDto.getHostName()).isEqualTo(member.getName()), () -> assertThat(detailInformationDto.getHostContact()).isEqualTo(member.getContact()), () -> assertThat(detailInformationDto.getBuildingAddress()).isEqualTo(building.getAddress()), + () -> assertThat(detailInformationDto.getPlaceType()).isEqualTo(PlaceType.COMPANY), () -> assertThat(detailInformationDto.getBuildingName()).isEqualTo(building.getName()), () -> assertThat(detailInformationDto.getBuildingParkingCostInformation()).isEqualTo(building.getParkingCostInformation()), () -> assertThat(detailInformationDto.getBuildingRepresentativeImageUrl()).isEqualTo(building.getRepresentativeImageUrl()) From 0f9120319ac75a764fc8aad2810e3afebe345a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Mon, 2 Oct 2023 20:21:47 +0900 Subject: [PATCH 096/111] =?UTF-8?q?=EB=AA=A9=ED=91=9C=20=EB=8B=AC=EC=84=B1?= =?UTF-8?q?=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A7=80=EA=B8=89=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (#191) * Test: ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (#191) --- .../point/controller/PointControllerTest.java | 20 +++ .../point/service/PointServiceTest.java | 136 ++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/src/test/java/com/livable/server/point/controller/PointControllerTest.java b/src/test/java/com/livable/server/point/controller/PointControllerTest.java index 59a8f4f5..d482ed46 100644 --- a/src/test/java/com/livable/server/point/controller/PointControllerTest.java +++ b/src/test/java/com/livable/server/point/controller/PointControllerTest.java @@ -63,4 +63,24 @@ void success_Test() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists()); } } + + @Nested + @DisplayName("๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + class GetAchievementPoint { + + @DisplayName("์„ฑ๊ณต") + @Test + void success_Test() throws Exception { + // Given + String uri = "/api/points/logs/members"; + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); + + // When + // Then + mockMvc.perform(MockMvcRequestBuilders.post(uri) + .header("Authorization", "Bearer " + token) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isCreated()); + } + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/point/service/PointServiceTest.java b/src/test/java/com/livable/server/point/service/PointServiceTest.java index e05adf36..194ee876 100644 --- a/src/test/java/com/livable/server/point/service/PointServiceTest.java +++ b/src/test/java/com/livable/server/point/service/PointServiceTest.java @@ -18,6 +18,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import java.time.LocalDate; import java.time.LocalDateTime; @@ -94,6 +96,7 @@ void failure_Test_existPointData() { } @Nested + @MockitoSettings(strictness = Strictness.LENIENT) @DisplayName("๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ ์ง€๊ธ‰ ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class GetAchievementPoint { @@ -111,6 +114,55 @@ void setUp() { .thenReturn(pureDate); } + @DisplayName("์‹คํŒจ - ํšŒ์› ์•„์ด๋””๋กœ๋ถ€ํ„ฐ ํฌ์ธํŠธ ํ…Œ์ด๋ธ”์ด ์กฐํšŒ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_GivenInvalidMemberId_ThenThrowsErrorWithMessage() { + // Given + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + // When + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.empty()); + + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> pointService.getAchievementPoint(memberId, requestTime)); + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals(PointErrorCode.POINT_NOT_EXIST.getMessage(), exception.getMessage()); + } + } + + @DisplayName("์‹คํŒจ - ํฌ์ธํŠธ ๋ฐ์ดํ„ฐ๋กœ๋ถ€ํ„ฐ ํฌ์ธํŠธ ๋กœ๊ทธ ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_GivenHaveNothingLogPoint_ThenThrowsErrorWithMessage() { + // Given + Long memberId = 1L; + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + + Point point = Point.builder().id(1L).balance(10).build(); + List mockList = List.of(); + + // When + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class)) + ).thenReturn(mockList); + + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> pointService.getAchievementPoint(memberId, requestTime)); + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals(PointErrorCode.POINT_NOT_EXIST_FOR_CURRENT_MONTH.getMessage(), exception.getMessage()); + } + } + @DisplayName("์„ฑ๊ณต - 7๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") @Test void success_Test_GivenSevenReviewPointLog_ThenPaidAchievementPoint() { @@ -143,6 +195,7 @@ void success_Test_GivenSevenReviewPointLog_ThenPaidAchievementPoint() { // When // Then pointService.getAchievementPoint(memberId, requestTime); + Assertions.assertEquals(110, point.getBalance()); } @DisplayName("์„ฑ๊ณต - 14๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰ํ•œ๋‹ค.") @@ -184,6 +237,7 @@ void success_Test_GivenFourTeenReviewPointLog_ThenPaidAchievementPoint() { // When // Then pointService.getAchievementPoint(memberId, requestTime); + Assertions.assertEquals(110, point.getBalance()); } @DisplayName("์‹คํŒจ - 3๊ฐœ์˜ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ๋กœ๊ทธ๊ฐ€ ์ฃผ์–ด์ง€๋Š” ๊ฒฝ์šฐ, ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๋ฉ”์‹œ์ง€ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") @@ -269,5 +323,87 @@ void failure_Test_GivenElevenReviewPointLog_ThenThrowsErrorWithMessage() { PointErrorCode.ACHIEVEMENT_POINT_NOT_MATCHED.getMessage(), exception.getMessage()); } } + + @DisplayName("์‹คํŒจ - ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํฌ์ธํŠธ๋ฅผ ์ง€๊ธ‰๋ฐ›์€ ์ดํ›„ ๋‹ค์‹œ ํฌ์ธํŠธ ์ง€๊ธ‰์„ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_WhenDuplicateRequest_ThenThrowError() { + // Given + Long memberId = 1L; + Point point = Point.builder().id(1L).balance(10).build(); + Review review = Review.builder().id(1L).build(); + + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).paidAt(paidTime).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(8L).point(point).review(review).code(PointCode.PA03).paidAt(paidTime).build() + ); + + // When + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> pointService.getAchievementPoint(memberId, requestTime)); + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals(PointErrorCode.ACHIEVEMENT_POINT_PAID_ALREADY.getMessage(), exception.getMessage()); + } + } + + @DisplayName("์‹คํŒจ - ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑ ํ–ˆ์ง€๋งŒ ๋ชฉํ‘œ ๋‹ฌ์„ฑ์ผ ์ดํ›„ ํฌ์ธํŠธ ์ง€๊ธ‰์„ ์š”์ฒญํ•œ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void failure_Test_GivenRequestNotAchievingDate_ThenThrowError() { + // Given + Long memberId = 1L; + Point point = Point.builder().id(1L).balance(10).build(); + Review review = Review.builder().id(1L).build(); + + LocalDateTime requestTime = LocalDateTime.of(2023, 1, 4, 0, 0, 0); + LocalDateTime paidTime = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + List MockList = List.of( + PointLog.builder().id(1L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(2L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build(), + PointLog.builder().id(3L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(4L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(5L).point(point).review(review).code(PointCode.PA02).paidAt(paidTime).build(), + PointLog.builder().id(6L).point(point).review(review).code(PointCode.PA00).paidAt(paidTime).build(), + PointLog.builder().id(7L).point(point).review(review).code(PointCode.PA01).paidAt(paidTime).build() + ); + + // When + Mockito.when(pointRepository.findByMemberId(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(point)); + + Mockito.when(pointLogRepository.findDateRangeOfPointLogByPointId( + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(LocalDateTime.class), + ArgumentMatchers.any(LocalDateTime.class) + )).thenReturn(MockList); + + Mockito.when(dateFactory.getPureDate(requestTime)) + .thenReturn(LocalDate.of(2023, 1, 4)); + + // Then + Assertions.assertThrows(GlobalRuntimeException.class, () -> pointService.getAchievementPoint(memberId, requestTime)); + try { + pointService.getAchievementPoint(memberId, requestTime); + } catch (GlobalRuntimeException exception) { + Assertions.assertEquals(PointErrorCode.ACHIEVEMENT_POINT_PAID_FAILED.getMessage(), exception.getMessage()); + } + } } } From 408fd32bd576dfbdc9ee529aa3ddd575dbcbd2d3 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Mon, 2 Oct 2023 20:32:46 +0900 Subject: [PATCH 097/111] =?UTF-8?q?=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ฐฉ๋ฌธ์ž ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ (#123) * Test: ๋ฐฉ๋ฌธ์ž ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#123) --- .../domain/InvitationValidationMessage.java | 4 + .../invitation/dto/InvitationRequest.java | 16 ++- .../controller/InvitationControllerTest.java | 136 +++++++++++++++++- 3 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java index e463a50a..05d62115 100644 --- a/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java +++ b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java @@ -4,4 +4,8 @@ public interface InvitationValidationMessage { String REQUIRED_FUTURE_DATE = "์„ ํƒ๋œ ์‹œ๊ฐ„์ด ํ˜„์žฌ๋ณด๋‹ค ๊ณผ๊ฑฐ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; String REQUIRED_VISITOR_COUNT = "๋ฐฉ๋ฌธ์ž๋Š” ์ตœ์†Œ 1๋ช… ์ด์ƒ 30๋ช… ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (๋ฉด์ ‘์€ 1๋ช…)"; String NOT_NULL = "๊ฐ’์ด Null ์ผ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + String VISITOR_NAME_MIN_SIZE = "๋ฐฉ๋ฌธ์ž ์ด๋ฆ„์€ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + String VISITOR_NAME_FORMAT = "๋ฐฉ๋ฌธ์ž ์ด๋ฆ„์€ ํ•œ๊ธ€๋งŒ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + String VISITOR_CONTACT_MIN_SIZE = "๋ฐฉ๋ฌธ์ž ์ „ํ™”๋ฒˆํ˜ธ๋Š” 10๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + String VISITOR_CONTACT_FORMAT = "๋ฐฉ๋ฌธ์ž ์ „ํ™”๋ฒˆํ˜ธ๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."; } diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java index 064812af..c32dec98 100644 --- a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java +++ b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java @@ -12,6 +12,7 @@ import javax.validation.Valid; import javax.validation.constraints.FutureOrPresent; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import java.time.LocalDateTime; import java.util.List; @@ -45,6 +46,7 @@ public static class CreateDTO { @FutureOrPresent(message = REQUIRED_FUTURE_DATE) private LocalDateTime endDate; + @Valid @Size(min = 1, max = 30, message = REQUIRED_VISITOR_COUNT) private List visitors; @@ -67,10 +69,15 @@ public Invitation toEntity(Member member) { @NoArgsConstructor @AllArgsConstructor public static class VisitorCreateDTO { - @NotNull + + @Pattern(regexp = "^[๊ฐ€-ํžฃ]*$", message = VISITOR_NAME_FORMAT) + @Size(min = 2, message = VISITOR_NAME_MIN_SIZE) + @NotNull(message = NOT_NULL) private String name; - @NotNull + @Pattern(regexp = "^[0-9]*$", message = VISITOR_CONTACT_FORMAT) + @Size(min = 10, message = VISITOR_CONTACT_MIN_SIZE) + @NotNull(message = NOT_NULL) private String contact; public Visitor toEntity(Invitation invitation) { @@ -109,9 +116,14 @@ public static class UpdateDTO { @NoArgsConstructor @AllArgsConstructor public static class VisitorForUpdateDTO { + + @Pattern(regexp = "^[๊ฐ€-ํžฃ]*$", message = VISITOR_NAME_FORMAT) + @Size(min = 2, message = VISITOR_NAME_MIN_SIZE) @NotNull(message = NOT_NULL) private String name; + @Pattern(regexp = "^[0-9]*$", message = VISITOR_CONTACT_FORMAT) + @Size(min = 10, message = VISITOR_CONTACT_MIN_SIZE) @NotNull(message = NOT_NULL) private String contact; diff --git a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java index f0384b3e..a5a7f213 100644 --- a/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java +++ b/src/test/java/com/livable/server/invitation/controller/InvitationControllerTest.java @@ -23,7 +23,9 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -122,8 +124,8 @@ void createInvitationFail_01() throws Exception { InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") - .startDate(LocalDateTime.of(2023, 9, 18, 10, 0, 0)) - .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .startDate(LocalDateTime.of(LocalDate.now().minusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") .commonPlaceId(1L) .visitors(List.of( @@ -154,8 +156,8 @@ void createInvitationFail_02() throws Exception { InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") - .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) - .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") .commonPlaceId(1L) .visitors(List.of()) @@ -180,14 +182,14 @@ void createInvitationFail_03() throws Exception { // Given List visitors = new ArrayList<>(); for (int i = 0; i < 31; i++) { - visitors.add(InvitationRequest.VisitorCreateDTO.builder().build()); + visitors.add(InvitationRequest.VisitorCreateDTO.builder().name("ํ™๊ธธ๋™").contact("01012341234").build()); } InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() .purpose("interview") .officeName("๊ณต์šฉ ๋ผ์šด์ง€") - .startDate(LocalDateTime.of(2030, 10, 30, 10, 0, 0)) - .endDate(LocalDateTime.of(2030, 10, 30, 10, 30, 0)) + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") .commonPlaceId(1L) .visitors(visitors) @@ -206,6 +208,126 @@ void createInvitationFail_03() throws Exception { .andExpect(jsonPath("$.message").value(InvitationValidationMessage.REQUIRED_VISITOR_COUNT)); } + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž ์ด๋ฆ„์ด ์˜์–ด์ธ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_04() throws Exception { + // Given + List visitors = new ArrayList<>(); + visitors.add(InvitationRequest.VisitorCreateDTO.builder().name("testName").contact("01012341234").build()); + + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("interview") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(visitors) + .build(); + + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.VISITOR_NAME_FORMAT)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž ์ด๋ฆ„์ด ํ•œ ๊ธ€์ž์ธ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_05() throws Exception { + // Given + List visitors = new ArrayList<>(); + visitors.add(InvitationRequest.VisitorCreateDTO.builder().name("๊น€").contact("01012341234").build()); + + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("interview") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(visitors) + .build(); + + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.VISITOR_NAME_MIN_SIZE)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž ์ „ํ™”๋ฒˆํ˜ธ์— ์ˆซ์ž ์ด์™ธ์˜ ๋ฌธ์ž๊ฐ€ ์„ž์ธ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_06() throws Exception { + // Given + List visitors = new ArrayList<>(); + visitors.add(InvitationRequest.VisitorCreateDTO.builder().name("ํ™๊ธธ๋™").contact("01012341234as").build()); + + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("interview") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(visitors) + .build(); + + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.VISITOR_CONTACT_FORMAT)); + } + + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ €์žฅ - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ (๋ฐฉ๋ฌธ์ž ์ „ํ™”๋ฒˆํ˜ธ ๊ธธ์ด๊ฐ€ 9์ž์ธ ๊ฒฝ์šฐ)") + @Test + void createInvitationFail_07() throws Exception { + // Given + List visitors = new ArrayList<>(); + visitors.add(InvitationRequest.VisitorCreateDTO.builder().name("ํ™๊ธธ๋™").contact("010123412").build()); + + InvitationRequest.CreateDTO dto = InvitationRequest.CreateDTO.builder() + .purpose("interview") + .officeName("๊ณต์šฉ ๋ผ์šด์ง€") + .startDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 0, 0))) + .endDate(LocalDateTime.of(LocalDate.now().plusDays(1L), LocalTime.of(10, 30, 0))) + .description("์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์•ž์— ์žˆ์–ด์š”.") + .commonPlaceId(1L) + .visitors(visitors) + .build(); + + String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 1000000)); + + // When & Then + mockMvc.perform( + post("/api/invitation") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(InvitationValidationMessage.VISITOR_CONTACT_MIN_SIZE)); + } + @DisplayName("[์‹คํŒจ] ์ดˆ๋Œ€์žฅ ์ƒ์„ธ ์กฐํšŒ - ์ดˆ๋Œ€์žฅ ์ฃผ์ธ๊ณผ ์š”์ฒญํ•œ ์‚ฌ๋žŒ์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ") @Test void getInvitationFail_01() throws Exception { From b47362491bff03d934b8f19a132e0eb84956d243 Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:57:29 +0900 Subject: [PATCH 098/111] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add: ๋ฉ”๋‰ด ๋ฆฌ์ŠคํŠธ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#174) * Feat: ๋ฉ”๋‰ด๋ฆฌ์ŠคํŠธ ์š”์ฒญ DTO ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#174) * Add: ํฌ์ธํŠธ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#174) * Feat: ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ์ด๋„˜ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#174) * Feat: ๋ฆฌ๋ทฐ๋ฉ”๋‰ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€ (#174) * Feat: ๋ฆฌ๋ทฐ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์‚ฌ์šฉ์ž ์•„์ด๋”” ๊ธฐ์ค€ ๋ฆฌ๋ทฐ ๋‚ ์งœ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#174) * Fix: ์ง์ ‘์ž…๋ ฅ ๋ฉ”๋‰ด ํ•„๋“œ ์ œ๊ฑฐ (#174) * Feat: ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ํฌ์ธํŠธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (#174) --------- Co-authored-by: edan_cafe --- .../menu/repository/MenuRepository.java | 7 ++ .../point/repository/PointLogRepository.java | 9 ++ .../server/review/domain/PointReview.java | 18 ++++ .../server/review/dto/MenuRequest.java | 13 +++ .../server/review/dto/ReviewRequest.java | 7 +- .../repository/ReviewMenuMapRepository.java | 7 ++ .../review/repository/ReviewRepository.java | 13 +++ .../server/review/service/ReviewService.java | 101 ++++++++++++++---- 8 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/livable/server/review/domain/PointReview.java create mode 100644 src/main/java/com/livable/server/review/dto/MenuRequest.java create mode 100644 src/main/java/com/livable/server/review/repository/ReviewMenuMapRepository.java diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index 15b0bf5d..ff7b06f8 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -30,4 +30,11 @@ public interface MenuRepository extends JpaRepository { "ORDER BY mcr.count DESC " ) List findMostSelectedMenuOrderByCount(@Param("buildingId") Long buildingId, Pageable pageable); + + @Query( + "SELECT m " + + "FROM Menu m " + + "WHERE m.id in :menuList" + ) + List findAllMenuByMenuId(@Param("menuList") List menuList); } diff --git a/src/main/java/com/livable/server/point/repository/PointLogRepository.java b/src/main/java/com/livable/server/point/repository/PointLogRepository.java index 57dbed80..393efba3 100644 --- a/src/main/java/com/livable/server/point/repository/PointLogRepository.java +++ b/src/main/java/com/livable/server/point/repository/PointLogRepository.java @@ -1,5 +1,6 @@ package com.livable.server.point.repository; +import com.livable.server.entity.Point; import com.livable.server.entity.PointLog; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -20,4 +21,12 @@ List findDateRangeOfPointLogByPointId( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate ); + + + @Query(value = "SELECT p " + + "FROM Point p " + + "WHERE p.member.id = :memberId" + + ) + Point findByMemberId(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/livable/server/review/domain/PointReview.java b/src/main/java/com/livable/server/review/domain/PointReview.java new file mode 100644 index 00000000..cde9ca47 --- /dev/null +++ b/src/main/java/com/livable/server/review/domain/PointReview.java @@ -0,0 +1,18 @@ +package com.livable.server.review.domain; + +import com.livable.server.entity.PointCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PointReview { + + RESTAURANT_POINT(PointCode.PA00, 10), + CAFETERIA_POINT(PointCode.PA01, 10), + LUNCHBOX_POINT(PointCode.PA02, 10); + + private final PointCode pointCode; + private final Integer amount; + +} diff --git a/src/main/java/com/livable/server/review/dto/MenuRequest.java b/src/main/java/com/livable/server/review/dto/MenuRequest.java new file mode 100644 index 00000000..ebabce8f --- /dev/null +++ b/src/main/java/com/livable/server/review/dto/MenuRequest.java @@ -0,0 +1,13 @@ +package com.livable.server.review.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MenuRequest { + + private Long menuId; + private String menuName; +} diff --git a/src/main/java/com/livable/server/review/dto/ReviewRequest.java b/src/main/java/com/livable/server/review/dto/ReviewRequest.java index f5c7ec1d..baf9301e 100644 --- a/src/main/java/com/livable/server/review/dto/ReviewRequest.java +++ b/src/main/java/com/livable/server/review/dto/ReviewRequest.java @@ -3,19 +3,17 @@ import com.livable.server.entity.*; import lombok.*; import lombok.extern.jackson.Jacksonized; -import org.hibernate.annotations.ColumnDefault; import javax.validation.constraints.NotNull; import java.util.List; -import static com.livable.server.entity.Evaluation.GOOD; - @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ReviewRequest { @Getter @Builder + @Jacksonized public static class LunchBoxCreateDTO { // JWT ํ† ํฐ ์˜ค๊ณ  => MemberId @NotNull(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.") @@ -65,8 +63,7 @@ public static class RestaurantCreateDTO { private Evaluation speed; private Evaluation service; - private List menus; - private List customMenus; + private List menus; public RestaurantReview toEntity(Member member, Restaurant restaurant, String selectedDishes) { diff --git a/src/main/java/com/livable/server/review/repository/ReviewMenuMapRepository.java b/src/main/java/com/livable/server/review/repository/ReviewMenuMapRepository.java new file mode 100644 index 00000000..074275a2 --- /dev/null +++ b/src/main/java/com/livable/server/review/repository/ReviewMenuMapRepository.java @@ -0,0 +1,7 @@ +package com.livable.server.review.repository; + +import com.livable.server.entity.ReviewMenuMap; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewMenuMapRepository extends JpaRepository { +} diff --git a/src/main/java/com/livable/server/review/repository/ReviewRepository.java b/src/main/java/com/livable/server/review/repository/ReviewRepository.java index 42289267..adf69ab8 100644 --- a/src/main/java/com/livable/server/review/repository/ReviewRepository.java +++ b/src/main/java/com/livable/server/review/repository/ReviewRepository.java @@ -2,6 +2,19 @@ import com.livable.server.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; public interface ReviewRepository extends JpaRepository { + + @Query(value = + "SELECT count(r) " + + "FROM Review r " + + "WHERE r.member.id = :memberId and DATE(r.createdAt) = current_date " + ) + Long findBymemberIdAndDate(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java index 20ae00fb..e8642a1f 100644 --- a/src/main/java/com/livable/server/review/service/ReviewService.java +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -5,22 +5,30 @@ import com.livable.server.core.util.S3Uploader; import com.livable.server.entity.*; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.menu.repository.MenuRepository; +import com.livable.server.point.repository.PointLogRepository; import com.livable.server.point.domain.DateFactory; import com.livable.server.point.domain.DateRange; import com.livable.server.restaurant.repository.RestaurantRepository; +import com.livable.server.review.domain.PointReview; import com.livable.server.review.domain.ReviewErrorCode; +import com.livable.server.review.dto.MenuRequest; import com.livable.server.review.dto.Projection; import com.livable.server.review.dto.ReviewRequest; import com.livable.server.review.dto.ReviewResponse; import com.livable.server.review.repository.ReviewImageRepository; +import com.livable.server.review.repository.ReviewMenuMapRepository; import com.livable.server.review.repository.ReviewProjectionRepository; import com.livable.server.review.repository.ReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -32,10 +40,13 @@ @Service public class ReviewService { + private final MenuRepository menuRepository; private final ReviewRepository reviewRepository; private final MemberRepository memberRepository; - private final ReviewImageRepository reviewImageRepository; + private final PointLogRepository pointLogRepository; private final RestaurantRepository restaurantRepository; + private final ReviewImageRepository reviewImageRepository; + private final ReviewMenuMapRepository reviewMenuMapRepository; private final ReviewProjectionRepository reviewProjectionRepository; private final S3Uploader s3Uploader; private final DateFactory dateFactory; @@ -50,9 +61,15 @@ public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateD List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { - // add point - // ํ•˜๋ฃจ์— ๋ฆฌ๋ทฐ ํ•œ๊ฐœ๋งŒ ์ธ์ง€ ์ฒดํฌ - // ํฌ์ธํŠธ 10์  ๋„ฃ๊ธฐ + // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + + if (reviewCount == 1) { + Point point = pointLogRepository.findByMemberId(memberId); + + // add point + paidPoints(point, PointReview.LUNCHBOX_POINT, review); + } // register image List reviewImages = saveImageFiles(review, images); @@ -71,7 +88,14 @@ public void createCafeteriaReview(ReviewRequest.CafeteriaCreateDTO cafeteriaCrea List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { - // add point + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + + if (reviewCount == 1) { + Point point = pointLogRepository.findByMemberId(memberId); + + // add point + paidPoints(point, PointReview.CAFETERIA_POINT, review); + } // register image List reviewImages = saveImageFiles(review, images); @@ -84,43 +108,67 @@ public void createCafeteriaReview(ReviewRequest.CafeteriaCreateDTO cafeteriaCrea public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantCreateDTO, Long memberId, List files) throws IOException { String selectedDishes = ""; StringBuffer sb = new StringBuffer(); + List menuList = new ArrayList<>(); + List reviewMenuMapList = new ArrayList<>(); - List menu = restaurantCreateDTO.getMenus(); + // request menu + List menu = restaurantCreateDTO.getMenus(); Long restaurantId = restaurantCreateDTO.getRestaurantId(); - List customMenu = restaurantCreateDTO.getCustomMenus(); Member member = findMemberById(memberId); Restaurant restaurant = findRestaurantById(restaurantId); - if (menu.isEmpty() && customMenu.isEmpty()) { + // menu valid + if (menu.isEmpty()) { throw new GlobalRuntimeException(ReviewErrorCode.MENUS_NOT_CHOICE); } - + // menu ๋ฆฌ์ŠคํŠธ ์ˆœํšŒ menu.forEach(el -> { - if (sb.length() > 0) { - sb.append(","); + if (el.getMenuId() > 0) { + menuList.add(el.getMenuId()); } - sb.append(el.getName()); - }); - customMenu.forEach(el -> { + // selected dishes ์šฉ if (sb.length() > 0) { sb.append(","); } - sb.append(el); + sb.append(el.getMenuName()); }); selectedDishes = sb.substring(0, sb.length()); - Review review = restaurantCreateDTO.toEntity(member, restaurant, selectedDishes); - reviewRepository.save(review); + List menus = menuRepository.findAllMenuByMenuId(menuList); + + menus.forEach(el -> { + reviewMenuMapList.add(ReviewMenuMap.builder() + .menu(el) + .review(review) + .build()); + }); + + reviewRepository.save(review); + reviewMenuMapRepository.saveAll(reviewMenuMapList); List images = s3Uploader.saveFile(files); - List reviewImages = saveImageFiles(review, images); - reviewImageRepository.saveAll(reviewImages); + if (!images.isEmpty()) { + + // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + + if (reviewCount == 1) { + Point point = pointLogRepository.findByMemberId(memberId); + + // add point + paidPoints(point, PointReview.RESTAURANT_POINT, review); + } + + List reviewImages = saveImageFiles(review, images); + reviewImageRepository.saveAll(reviewImages); + } + } private Restaurant findRestaurantById(Long restaurantId) { @@ -166,6 +214,21 @@ public List findCalendarList(Long memberId, Stri return reviewProjectionRepository.findCalendarListByYearAndMonth(year, month); } + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) + public void paidPoints(Point point, PointReview pointReview, Review review) { + + point.plusPoint(pointReview.getAmount()); + + PointLog pointLog = PointLog.builder() + .point(point) + .review(review) + .code(pointReview.getPointCode()) + .amount(pointReview.getAmount()) + .build(); + + pointLogRepository.save(pointLog); + } + @Transactional(readOnly = true) public List findAllReviewDetailList(Long memberId, Integer year, Integer month) { From 0197b0f6719aa6d714d9dd8cba445c638ecc35c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Mon, 2 Oct 2023 21:04:09 +0900 Subject: [PATCH 099/111] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=EC=A0=90=EC=99=84=20=EB=8B=AC=EB=A0=A5=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: ์‘๋‹ต์— reviewId๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ์ฟผ๋ฆฌ ๊ฐœ์„  (#194) * Refactor: ์‘๋‹ต์— reviewId๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ๋งคํ•‘ ์ •๋ณด ๊ฐœ์„  (#194) * Refactor: ์‘๋‹ต์— reviewId๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ๋งคํ•‘ ์˜ค๋ธŒ์ ํŠธ ๊ฐœ์„  (#194) * Refactor: ์‘๋‹ต์— reviewId๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ์‘๋‹ต ์˜ค๋ธŒ์ ํŠธ ๊ฐœ์„  (#194) --- src/main/java/com/livable/server/entity/Review.java | 1 + src/main/java/com/livable/server/review/dto/Projection.java | 4 +++- .../java/com/livable/server/review/dto/ReviewResponse.java | 2 ++ .../server/review/repository/ReviewProjectionRepository.java | 3 +++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java index eba4ef63..789b1175 100644 --- a/src/main/java/com/livable/server/entity/Review.java +++ b/src/main/java/com/livable/server/entity/Review.java @@ -29,6 +29,7 @@ classes = @ConstructorResult( targetClass = Projection.AllReviewDetailDTO.class, columns = { + @ColumnResult(name = "reviewId", type = Long.class), @ColumnResult(name = "reviewTitle", type = String.class), @ColumnResult(name = "reviewTaste", type = String.class), @ColumnResult(name = "reviewDescription", type = String.class), diff --git a/src/main/java/com/livable/server/review/dto/Projection.java b/src/main/java/com/livable/server/review/dto/Projection.java index 258dd839..d756e1e3 100644 --- a/src/main/java/com/livable/server/review/dto/Projection.java +++ b/src/main/java/com/livable/server/review/dto/Projection.java @@ -55,6 +55,7 @@ public static class RestaurantReviewList { @AllArgsConstructor public static class AllReviewDetailDTO { + private Long reviewId; private String reviewTitle; private Evaluation reviewTaste; private String reviewDescription; @@ -63,8 +64,9 @@ public static class AllReviewDetailDTO { private String images; private String reviewType; - public AllReviewDetailDTO(String reviewTitle, String reviewTaste, String reviewDescription, String reviewCreatedAt, String location, String images, String reviewType) { + public AllReviewDetailDTO(Long reviewId, String reviewTitle, String reviewTaste, String reviewDescription, String reviewCreatedAt, String location, String images, String reviewType) { + this.reviewId = reviewId; this.reviewTitle = reviewTitle; this.reviewTaste = Objects.isNull(reviewTaste) ? null : Evaluation.valueOf(reviewTaste); this.reviewDescription = reviewDescription; diff --git a/src/main/java/com/livable/server/review/dto/ReviewResponse.java b/src/main/java/com/livable/server/review/dto/ReviewResponse.java index 70d7ca71..2149826e 100644 --- a/src/main/java/com/livable/server/review/dto/ReviewResponse.java +++ b/src/main/java/com/livable/server/review/dto/ReviewResponse.java @@ -29,6 +29,7 @@ public CalendarListDTO(Long reviewId, String type, String reviewImageUrl, LocalD @Builder public static class DetailListDTO { + private Long reviewId; private String reviewTitle; private Evaluation reviewTaste; private String reviewDescription; @@ -39,6 +40,7 @@ public static class DetailListDTO { public static DetailListDTO valueOf(Projection.AllReviewDetailDTO detailDTO, ImageSeparator imageSeparator) { return DetailListDTO.builder() + .reviewId(detailDTO.getReviewId()) .reviewTitle(detailDTO.getReviewTitle()) .reviewTaste(detailDTO.getReviewTaste()) .reviewDescription(detailDTO.getReviewDescription()) diff --git a/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java index a8ec8721..74feb80b 100644 --- a/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java +++ b/src/main/java/com/livable/server/review/repository/ReviewProjectionRepository.java @@ -62,6 +62,7 @@ public class ReviewProjectionRepository { FIND_ALL_REVIEW_DETAIL_BETWEEN_DATE_QUERY = "SELECT * " + "FROM (" + "SELECT " + + "review.id as reviewId, " + "review.selected_dishes as reviewTitle, " + "restaurant_review.taste as reviewTaste, " + "review.description as reviewDescription, " + @@ -77,6 +78,7 @@ public class ReviewProjectionRepository { "GROUP BY review.id, review.member_id " + "UNION " + "SELECT " + + "review.id as reviewId, " + "review.selected_dishes as reviewTitle, " + "cafeteria_review.taste as reviewTaste, " + "review.description as reviewDescription, " + @@ -92,6 +94,7 @@ public class ReviewProjectionRepository { "GROUP BY review.id, review.member_id " + "UNION " + "SELECT " + + "review.id as reviewId, " + "review.selected_dishes as reviewTitle, " + "NULL as reviewTaste, " + "review.description as reviewDescription, " + From 4e05c8adb13d6bc1979f79887e84ebe269c4af70 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Tue, 3 Oct 2023 00:29:08 +0900 Subject: [PATCH 100/111] =?UTF-8?q?Refactor=20:=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20(#197)=20(#198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๊ธฐ์กด : ์ „์ผ ๊ธฐ์ค€ ์œผ๋กœ ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒ๋œ ๋ฉ”๋‰ด ์นด์šดํŠธ ๋ณ€๊ฒฝ : ๋‹น์ผ์„ ๊ธฐ์ค€์œผ๋กœ ์ „์ฃผ ์›”์š”์ผ - ์ผ์š”์ผ์— ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒ๋œ ๋ฉ”๋‰ด ์นด์šดํŠธ ๊ธฐ์ค€์ผ์„ ๊ณ„์‚ฐ ํ•˜๋Š” Enum Class์™€ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ --- .../menu/controller/MenuController.java | 1 + .../server/menu/domain/ReferenceDate.java | 20 ++++++++++ .../menu/repository/MenuRepository.java | 5 ++- .../server/menu/service/MenuService.java | 6 ++- .../server/menu/service/MenuServiceTest.java | 38 ++----------------- 5 files changed, 33 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/livable/server/menu/domain/ReferenceDate.java diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index 1a38f5dc..99c11197 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -48,6 +48,7 @@ public ResponseEntity>> getMostSelectedMenu(@R //์ถ”ํ›„ ๊ฐ€์ ธ ์˜ค๋Š” ๋ฉ”๋‰ด ์ˆซ์ž ๋ณ€๊ฒฝ์‹œ ๋ณ€๊ฒฝ Pageable pageable = PageRequest.of(0, MOST_SELECTED_MENU.getLimit()); + List mostSelectedMenu = menuService.getMostSelectedMenu(buildingId, pageable); return ApiResponse.success(mostSelectedMenu, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/menu/domain/ReferenceDate.java b/src/main/java/com/livable/server/menu/domain/ReferenceDate.java new file mode 100644 index 00000000..c6324ba5 --- /dev/null +++ b/src/main/java/com/livable/server/menu/domain/ReferenceDate.java @@ -0,0 +1,20 @@ +package com.livable.server.menu.domain; + +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReferenceDate { + + START_WITH_MONDAY(1); + + private final Integer dayOfWeek; + + public static LocalDate getReferenceDate(LocalDate localDate, ReferenceDate referenceDate) { + return localDate.minusDays( + localDate.getDayOfWeek().getValue() - referenceDate.getDayOfWeek()) + .minusWeeks(1); + } +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index ff7b06f8..223e9973 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -3,6 +3,7 @@ import com.livable.server.entity.Menu; import com.livable.server.menu.dto.MostSelectedMenuProjection; import com.livable.server.menu.dto.RouletteMenuProjection; +import java.time.LocalDate; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -25,11 +26,11 @@ public interface MenuRepository extends JpaRepository { "FROM MenuChoiceResult mcr " + "JOIN Menu m " + "ON m.id = mcr.menu.id " + - "WHERE mcr.building.id = :buildingId AND mcr.date = CURRENT_DATE " + + "WHERE mcr.building.id = :buildingId AND mcr.date = :referenceDate " + "GROUP BY mcr.date, mcr.menu.id, m.name, m.representativeImageUrl " + "ORDER BY mcr.count DESC " ) - List findMostSelectedMenuOrderByCount(@Param("buildingId") Long buildingId, Pageable pageable); + List findMostSelectedMenuOrderByCount(@Param("buildingId") Long buildingId, @Param("referenceDate") LocalDate referenceDate, Pageable pageable); @Query( "SELECT m " + diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java index 8ff17cbf..45fc573f 100644 --- a/src/main/java/com/livable/server/menu/service/MenuService.java +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -10,6 +10,7 @@ import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.domain.ReferenceDate; import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; @@ -86,7 +87,10 @@ private List convertToDTO(Map> menuG } public List getMostSelectedMenu(Long buildingId, Pageable pageable) { - List mostSelectedMenuProjections = menuRepository.findMostSelectedMenuOrderByCount(buildingId, pageable); + //๊ธฐ์ค€์ผ(์ง€๋‚œ์ฃผ ์›”~์ผ) + LocalDate referenceDate = ReferenceDate.getReferenceDate(LocalDate.now(), ReferenceDate.START_WITH_MONDAY); + + List mostSelectedMenuProjections = menuRepository.findMostSelectedMenuOrderByCount(buildingId, referenceDate, pageable); return convertToDTO(mostSelectedMenuProjections); } diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java index 7a130e62..a20e2a9f 100644 --- a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -22,6 +22,7 @@ import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; import com.livable.server.menu.domain.MenuErrorCode; +import com.livable.server.menu.domain.ReferenceDate; import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; @@ -116,8 +117,9 @@ void getMostSelectedMenusSuccess() { Long menuId = 1L; String menuName = "๋ฉ”๋‰ด1"; String menuUrl = "/dummyUrl"; - Integer pageLimit = 1; + int pageLimit = 1; Pageable pageable = PageRequest.of(0, pageLimit); + LocalDate referenceDate = ReferenceDate.getReferenceDate(LocalDate.now(), ReferenceDate.START_WITH_MONDAY); MostSelectedMenuProjection mostSelectedMenuProjection = new MostSelectedMenuProjection(count, @@ -137,7 +139,7 @@ void getMostSelectedMenusSuccess() { - given(menuRepository.findMostSelectedMenuOrderByCount(buildingId, pageable)) + given(menuRepository.findMostSelectedMenuOrderByCount(buildingId, referenceDate, pageable)) .willReturn(mostSelectedMenuProjections); //when @@ -155,38 +157,6 @@ void getMostSelectedMenusSuccess() { ); } - @DisplayName("FAIELD - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") - @Test - void getMostSelectedMenusFailedThrowException() { - - //given - - Pageable pageable = PageRequest.of(0, 1); - - given(menuRepository.findMostSelectedMenuOrderByCount(1L, pageable)) - .willThrow(GlobalRuntimeException.class); - - //when - assertThrows(GlobalRuntimeException.class, () -> - menuService.getMostSelectedMenu(1L, pageable)); - } - - @DisplayName("FAIELD - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด 10์œ„๊นŒ์ง€ ์‘๋‹ต ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ") - @Test - void getMostSelectedMenusFailedBadPagingLimit() { - - //given - - Pageable pageable = PageRequest.of(0, 1); - - given(menuRepository.findMostSelectedMenuOrderByCount(1L, pageable)) - .willThrow(GlobalRuntimeException.class); - - //when - assertThrows(GlobalRuntimeException.class, () -> - menuService.getMostSelectedMenu(1L, pageable)); - } - @DisplayName("SUCCESS - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ๋‹น์ผ ์ฒซ ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜") @Test void createMenuChoiceLog() { From fcdff2b25b1aa38b6fa1d6e27d284322e1931865 Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Tue, 3 Oct 2023 03:04:55 +0900 Subject: [PATCH 101/111] =?UTF-8?q?Hotfix:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9A=94=EC=B2=AD=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#199)=20(#200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: edan_cafe --- .../livable/server/review/controller/ReviewController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/livable/server/review/controller/ReviewController.java b/src/main/java/com/livable/server/review/controller/ReviewController.java index 1fd1ae62..b4328123 100644 --- a/src/main/java/com/livable/server/review/controller/ReviewController.java +++ b/src/main/java/com/livable/server/review/controller/ReviewController.java @@ -28,7 +28,7 @@ public class ReviewController { @PostMapping(value = "/lunch-box", consumes = "multipart/form-data") public ResponseEntity createLunchBoxReview( @Valid @RequestPart("data") ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, - @RequestPart(value = "imageFiles") List files, + @RequestPart(value = "imageFiles", required = false) List files, @LoginActor Actor actor ) throws IOException { @@ -43,7 +43,7 @@ public ResponseEntity createLunchBoxReview( @PostMapping(value = "/cafeteria", consumes = "multipart/form-data") public ResponseEntity createCafeteriaReview( @Valid @RequestPart("data") ReviewRequest.CafeteriaCreateDTO CafeteriaCreateDTO, - @RequestPart(value = "imageFiles") List files, + @RequestPart(value = "imageFiles", required = false) List files, @LoginActor Actor actor ) throws IOException { @@ -58,7 +58,7 @@ public ResponseEntity createCafeteriaReview( @PostMapping(value = "/restaurant", consumes = "multipart/form-data") public ResponseEntity createRestaurantReview( @Valid @RequestPart("data") ReviewRequest.RestaurantCreateDTO restaurantCreateDTO, - @RequestPart(value = "imageFiles") List files, + @RequestPart(value = "imageFiles", required = false) List files, @LoginActor Actor actor ) throws IOException { From 6619c74ddaaf9b5fae6eb98babd69dff0ea1fccf Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:56:08 +0900 Subject: [PATCH 102/111] =?UTF-8?q?modify:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=95=98=EB=A3=A8=20=EC=9E=91=EC=84=B1=20=ED=95=9C=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#201)=20(#202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: edan_cafe --- .../server/review/domain/ReviewErrorCode.java | 3 +- .../server/review/service/ReviewService.java | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java b/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java index 70727f01..4dcbafdf 100644 --- a/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java +++ b/src/main/java/com/livable/server/review/domain/ReviewErrorCode.java @@ -11,7 +11,8 @@ public enum ReviewErrorCode implements ErrorCode { MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์› ์ •๋ณด์ž…๋‹ˆ๋‹ค."), RESTAURANT_NOT_EXITST(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์Œ์‹์  ์ •๋ณด์ž…๋‹ˆ๋‹ค."), - MENUS_NOT_CHOICE(HttpStatus.BAD_REQUEST, "ํ•˜๋‚˜ ์ด์ƒ์˜ ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”."); + MENUS_NOT_CHOICE(HttpStatus.BAD_REQUEST, "ํ•˜๋‚˜ ์ด์ƒ์˜ ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”."), + ALREADY_HAVE_A_REVIEW(HttpStatus.BAD_REQUEST, "๋ฆฌ๋ทฐ๋Š” ํ•˜๋ฃจ์— ํ•œ ๊ฐœ๋งŒ ์ž‘์„ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java index e8642a1f..df879d94 100644 --- a/src/main/java/com/livable/server/review/service/ReviewService.java +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -56,13 +56,18 @@ public class ReviewService { public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateDTO, Long memberId, List files) throws IOException { Member member = findMemberById(memberId); Review review = lunchBoxCreateDTO.toEntity(member, LUNCH_BOX.getMessage()); - reviewRepository.save(review); + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + + if (reviewCount == 0) { + reviewRepository.save(review); + } else { + throw new GlobalRuntimeException(ReviewErrorCode.ALREADY_HAVE_A_REVIEW); + } List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? - Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); if (reviewCount == 1) { Point point = pointLogRepository.findByMemberId(memberId); @@ -83,12 +88,17 @@ public void createCafeteriaReview(ReviewRequest.CafeteriaCreateDTO cafeteriaCrea Member member = findMemberById(memberId); Building building = getBuildingByMember(member); Review review = cafeteriaCreateDTO.toEntity(member, building, CAFETERIA.getMessage()); - reviewRepository.save(review); + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + + if (reviewCount == 0) { + reviewRepository.save(review); + } else { + throw new GlobalRuntimeException(ReviewErrorCode.ALREADY_HAVE_A_REVIEW); + } List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { - Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); if (reviewCount == 1) { Point point = pointLogRepository.findByMemberId(memberId); @@ -118,6 +128,8 @@ public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantC Member member = findMemberById(memberId); Restaurant restaurant = findRestaurantById(restaurantId); + Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); + // menu valid if (menu.isEmpty()) { throw new GlobalRuntimeException(ReviewErrorCode.MENUS_NOT_CHOICE); @@ -148,7 +160,12 @@ public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantC .build()); }); - reviewRepository.save(review); + if (reviewCount == 0) { + reviewRepository.save(review); + } else { + throw new GlobalRuntimeException(ReviewErrorCode.ALREADY_HAVE_A_REVIEW); + } + reviewMenuMapRepository.saveAll(reviewMenuMapList); List images = s3Uploader.saveFile(files); @@ -156,7 +173,6 @@ public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantC if (!images.isEmpty()) { // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? - Long reviewCount = reviewRepository.findBymemberIdAndDate(memberId); if (reviewCount == 1) { Point point = pointLogRepository.findByMemberId(memberId); From c798c15fe0c51793bf61590a4e01bbfb499dad62 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:28:38 +0900 Subject: [PATCH 103/111] =?UTF-8?q?=EC=A3=BC=EC=B0=A8=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: ๋ฐฉ๋ฌธ์ž์˜ ์ฃผ์ฐจ๋ฒˆํ˜ธ ์กฐํšŒ API (#205) * Test: ๋ฐฉ๋ฌธ์ž์˜ ์ฐจ๋Ÿ‰ ๋“ฑ๋ก ์กฐํšŒ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ (#205) --- .../controller/VisitationController.java | 12 +++++++- .../visitation/dto/VisitationResponse.java | 16 ++++++++++ .../repository/ParkingLogRepository.java | 3 ++ .../visitation/service/ParkingLogService.java | 4 +++ .../service/VisitationFacadeService.java | 7 +++++ .../controller/VisitationControllerTest.java | 30 +++++++++++++++++++ .../service/ParkingLogServiceTest.java | 18 +++++++++++ .../service/VisitationFacadeServiceTest.java | 17 +++++++++++ 8 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index 97cc5b2d..a5b52057 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -56,6 +56,17 @@ public ResponseEntity> validateQrCode( return ApiResponse.success(HttpStatus.OK); } + @GetMapping("/parking") + public ResponseEntity> findCarNumber(@LoginActor Actor actor) { + + JwtTokenProvider.checkVisitorToken(actor); + + Long visitorId = actor.getId(); + VisitationResponse.CarNumber result = visitationFacadeService.findCarNumber(visitorId); + + return ApiResponse.success(result, HttpStatus.OK); + } + @PostMapping("/parking") public ResponseEntity> registerParking( @RequestBody @Valid VisitationRequest.RegisterParkingDto registerParkingDto, @LoginActor Actor actor @@ -68,5 +79,4 @@ public ResponseEntity> registerParking( return ApiResponse.success(HttpStatus.CREATED); } - } diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java index 6d8ae44f..15e5dd49 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationResponse.java @@ -24,6 +24,22 @@ public static Base64QrCode of(String base64QrCode) { } } + @Getter + @NoArgsConstructor + public static class CarNumber { + + private Long visitorId; + private String carNumber; + + public static CarNumber of(Long visitorId, String carNumber) { + CarNumber carNumberDto = new CarNumber(); + carNumberDto.visitorId = visitorId; + carNumberDto.carNumber = carNumber; + + return carNumberDto; + } + } + @Getter @NoArgsConstructor public static class DetailInformationDto { diff --git a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java index a2987afa..cde66e28 100644 --- a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java +++ b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java @@ -18,4 +18,7 @@ public interface ParkingLogRepository extends JpaRepository { @Modifying @Query("delete from ParkingLog p where p.visitor.id in :visitorIds") void deleteByVisitorIdsIn(@Param("visitorIds") List visitorIds); + + @Query("select p.carNumber from ParkingLog p where p.visitor.id in :visitorId") + Optional findCarNumberByVisitorId(@Param("visitorId") final Long visitorId); } diff --git a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java index 3e1af5b4..0e70bba4 100644 --- a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java +++ b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java @@ -18,6 +18,10 @@ public Optional findParkingLogByVisitorId(final Long visitorId) { return parkingLogRepository.findParkingLogByVisitorId(visitorId); } + public Optional findCarNumberByVisitorId(final Long visitorId) { + return parkingLogRepository.findCarNumberByVisitorId(visitorId); + } + public void registerParkingLog(final Visitor visitor, final String carNumber) { ParkingLog parkingLog = ParkingLog.create(visitor, carNumber); diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 0ceb20d0..a1a0ea50 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -50,4 +50,11 @@ private void validateDuplicationRegister(final Long visitorId) { throw new GlobalRuntimeException(VisitationErrorCode.ALREADY_REGISTER_PARKING); } } + + public VisitationResponse.CarNumber findCarNumber(Long visitorId) { + String carNumber = parkingLogService.findCarNumberByVisitorId(visitorId) + .orElse(null); + + return VisitationResponse.CarNumber.of(visitorId, carNumber); + } } diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index c26f8f9b..8ce4bade 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -151,6 +151,36 @@ void validateQrCodeFail() throws Exception { then(visitationFacadeService).should(times(1)).validateQrCode(anyString(), anyLong()); } + @DisplayName("[GET][/api/visitation/parking] - ์ฐจ๋Ÿ‰ ์กฐํšŒ ์„ฑ๊ณต") + @Test + void findCarNumberSuccess() throws Exception { + // given + String token = tokenProvider.createActorToken(ActorType.VISITOR, 1L, new Date(new Date().getTime() + 10000000)); + String carNumber = "12๊ฐ€1234"; + VisitationResponse.CarNumber result = VisitationResponse.CarNumber.of(1L, carNumber); + + + given(visitationFacadeService.findCarNumber(1L)).willReturn(result); + + // when + ResultActions resultActions = mockMvc.perform( + get("/api/visitation/parking") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.visitorId").isNumber()) + .andExpect(jsonPath("$.data.carNumber").isString()) + .andExpect(jsonPath("$.data.visitorId").value(1)) + .andExpect(jsonPath("$.data.carNumber").value("12๊ฐ€1234")); + + then(visitationFacadeService) + .should(times(1)) + .findCarNumber(1L); + } + @DisplayName("[POST][/api/visitation/parking] - ์ฐจ๋Ÿ‰ ๋“ฑ๋ก ์„ฑ๊ณต") @Test void registerParking() throws Exception { diff --git a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java index 70dd9ab8..c7d21546 100644 --- a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java @@ -79,4 +79,22 @@ void registerParkingLogSuccessTest() { parkingLogMockedStatic.close(); } + + @DisplayName("ParkingLogService.findCarNumberByVisitorId ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findCarNumberByVisitorIdSuccessTest() { + + String carNumber = "testCarNumber"; + Optional optionalCarNumber = Optional.of(carNumber); + + // Given + given(parkingLogRepository.findCarNumberByVisitorId(anyLong())).willReturn(optionalCarNumber); + + // When + Optional result = parkingLogService.findCarNumberByVisitorId(1L); + + // Then + then(parkingLogRepository).should(times(1)).findCarNumberByVisitorId(anyLong()); + assertThat(optionalCarNumber).isEqualTo(result); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 599b7e9d..cae1b884 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -142,4 +142,21 @@ void registerParkingFailTest() { then(parkingLogService).should(times(1)).findParkingLogByVisitorId(any()); assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.ALREADY_REGISTER_PARKING); } + + @DisplayName("VisitationFacadeService.findCarNumber ์„ฑ๊ณต ํ…Œ์ŠคํŠธ") + @Test + void findCarNumberSuccessTest() { + // Given + String carNumber = "12๊ฐ€1234"; + VisitationResponse.CarNumber dto = VisitationResponse.CarNumber.of(1L, carNumber); + given(parkingLogService.findCarNumberByVisitorId(any())).willReturn(Optional.of("12๊ฐ€1234")); + + // When + + VisitationResponse.CarNumber expectedCarNumber = visitationFacadeService.findCarNumber(1L); + + // Then + then(parkingLogService).should(times(1)).findCarNumberByVisitorId(any()); + assertThat(dto).usingRecursiveComparison().isEqualTo(expectedCarNumber); + } } \ No newline at end of file From bee637a57752acca7f4ab26b1a19b1f6ce6abcda Mon Sep 17 00:00:00 2001 From: Tae-il Kim <71359732+james-taeil@users.noreply.github.com> Date: Wed, 4 Oct 2023 22:48:57 +0900 Subject: [PATCH 104/111] =?UTF-8?q?Hotfix:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#207)=20?= =?UTF-8?q?(#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: edan_cafe --- .../server/review/service/ReviewService.java | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/livable/server/review/service/ReviewService.java b/src/main/java/com/livable/server/review/service/ReviewService.java index df879d94..b88048c0 100644 --- a/src/main/java/com/livable/server/review/service/ReviewService.java +++ b/src/main/java/com/livable/server/review/service/ReviewService.java @@ -67,14 +67,10 @@ public void createLunchBoxReview(ReviewRequest.LunchBoxCreateDTO lunchBoxCreateD List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { - // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? + Point point = pointLogRepository.findByMemberId(memberId); - if (reviewCount == 1) { - Point point = pointLogRepository.findByMemberId(memberId); - - // add point - paidPoints(point, PointReview.LUNCHBOX_POINT, review); - } + // add point + paidPoints(point, PointReview.LUNCHBOX_POINT, review); // register image List reviewImages = saveImageFiles(review, images); @@ -99,13 +95,10 @@ public void createCafeteriaReview(ReviewRequest.CafeteriaCreateDTO cafeteriaCrea List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { + Point point = pointLogRepository.findByMemberId(memberId); - if (reviewCount == 1) { - Point point = pointLogRepository.findByMemberId(memberId); - - // add point - paidPoints(point, PointReview.CAFETERIA_POINT, review); - } + // add point + paidPoints(point, PointReview.CAFETERIA_POINT, review); // register image List reviewImages = saveImageFiles(review, images); @@ -132,6 +125,7 @@ public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantC // menu valid if (menu.isEmpty()) { + throw new GlobalRuntimeException(ReviewErrorCode.MENUS_NOT_CHOICE); } @@ -171,20 +165,14 @@ public void createRestaurantReview(ReviewRequest.RestaurantCreateDTO restaurantC List images = s3Uploader.saveFile(files); if (!images.isEmpty()) { + Point point = pointLogRepository.findByMemberId(memberId); - // ๋‚ ์งœ ๋น„๊ต, ์˜ค๋Š˜ ๋ฆฌ๋ทฐ ์ป๋Š”์ง€? - - if (reviewCount == 1) { - Point point = pointLogRepository.findByMemberId(memberId); - - // add point - paidPoints(point, PointReview.RESTAURANT_POINT, review); - } + // add point + paidPoints(point, PointReview.RESTAURANT_POINT, review); List reviewImages = saveImageFiles(review, images); reviewImageRepository.saveAll(reviewImages); } - } private Restaurant findRestaurantById(Long restaurantId) { From 0a1df99d941c3f0960803a0cfa7e24d89fa5df93 Mon Sep 17 00:00:00 2001 From: Taeyun Choi <109710879+cxxxtxxyxx@users.noreply.github.com> Date: Thu, 5 Oct 2023 02:41:13 +0900 Subject: [PATCH 105/111] =?UTF-8?q?Transaction=20AOP=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../livable/server/reservation/service/ReservationService.java | 2 ++ .../server/visitation/service/VisitationFacadeService.java | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/com/livable/server/reservation/service/ReservationService.java b/src/main/java/com/livable/server/reservation/service/ReservationService.java index ed8151c4..931cd0a8 100644 --- a/src/main/java/com/livable/server/reservation/service/ReservationService.java +++ b/src/main/java/com/livable/server/reservation/service/ReservationService.java @@ -12,12 +12,14 @@ import com.livable.server.reservation.repository.ReservationRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.*; @RequiredArgsConstructor @Service +@Transactional(readOnly = true) public class ReservationService { private final ReservationRepository reservationRepository; diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index a1a0ea50..42a243a7 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -33,6 +33,7 @@ public VisitationResponse.Base64QrCode createQrCode(final Long visitorId) { return VisitationResponse.Base64QrCode.of(base64QrCode); } + @Transactional public void validateQrCode(final String qr, final Long visitorId) { visitationService.validateQrCode(qr); visitorService.doEntrance(visitorId); From b5b66075cc57c9cef43c74bb35e73f0c071a5051 Mon Sep 17 00:00:00 2001 From: HoonSub Kim Date: Fri, 6 Oct 2023 20:12:29 +0900 Subject: [PATCH 106/111] =?UTF-8?q?=EB=A6=AC=EB=93=9C=EB=AF=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=99=84=EB=A3=8C=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: Readme 1์ฐจ ์ž‘์„ฑ ์™„๋ฃŒ (#204) * Docs: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ * Docs: ํ”„๋กœ์ ํŠธ ์‹คํ–‰ ์„ค์ •, ๋ช…๋ น์–ด ์ถ”๊ฐ€ * Docs: ๋งก์€ ์—ญํ•  ์ถ”๊ฐ€ (#203) * Docs: ๋งก์€ ์—ญํ•  ์ถ”๊ฐ€ (#203) * Docs: ๋งก์€ ์—ญํ•  ์ถ”๊ฐ€ * Docs: ๋งก์€ ์—ญํ•  ์ถ”๊ฐ€ * Update README.md * Docs: ๋งก์€ ์—ญํ•  ์ถ”๊ฐ€ (#203) * Docs: ์—ญํ•  ํ‘œ ์ •๋ ฌ ์ถ”๊ฐ€ --- README.md | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59069fb5..87923da9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,173 @@ -# server -์˜คํ”ผ์Šค ํ˜์‹ ์„ ์œ„ํ•œ ํ†ตํ•ฉ ํ”Œ๋žซํผ โ€˜์˜คํ”ผ์Šค๋„ˆโ€™์˜ ์œ ์ € ๊ฐ€์ž…์ž์ˆ˜์™€ WAU๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ +image + + +

๐Ÿข ์˜คํ”ผ์Šค ํ˜์‹ ์„ ์œ„ํ•œ ํ†ตํ•ฉ ํ”Œ๋žซํผ, ์˜คํ”ผ์Šค๋„ˆ

+ +> **๊ฐœ๋ฐœ ๊ธฐ๊ฐ„** : 2023.09.11(์›”) ~ 2023.10.06(๋ชฉ)
+> **๋ฐฐํฌ ์ฃผ์†Œ** : [์˜คํ”ผ์Šค๋„ˆ](https://officener.vercel.app)
+> **๋ฐฑ์—”๋“œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ** : [๋ฐฑ์—”๋“œ](https://github.com/livable-final/server)
+> **ํ”„๋ก ํŠธ ์œ ์ € ๋ ˆํฌ์ง€ํ† ๋ฆฌ** : [ํ”„๋ก ํŠธ](https://github.com/livable-final/client)
+ + +



+ +

ํ”„๋กœ์ ํŠธ ๋ชฉ์ 

+ +- ๊ธฐ์กด ์˜คํ”ผ์Šค๋„ˆ ์„œ๋น„์Šค๋Š” ๊ด€๋ฆฌ์ž, ๊ด€๋ฆฌ ๋ฉค๋ฒ„ ์ด์™ธ์˜ ์ผ๋ฐ˜ ์œ ์ €์˜ ๊ฐ€์ž…๊ณผ ์ด์šฉ ๋™๊ธฐ๊ฐ€ ๋ถ€์กฑ +- ์ด์šฉ์ž๊ฐ€ ๋งค์ผ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋งŒํ•œ ์ปจํ…์ธ ์™€ ๊ธฐ๋Šฅ์˜ ๋ถ€์žฌ +- "์œ ์ € ๊ฐ€์ž…์ž ์ˆ˜"์™€ "WAU" ์ƒ์Šน์„ ๋ชฉ์ ์œผ๋กœ ์‹œ์ž‘๋œ ๊ธฐ์—… ์—ฐ๊ณ„ ํ”„๋กœ์ ํŠธ + +



+ +

์‚ฌ์šฉํ•œ ๊ธฐ์ˆ ์Šคํƒ

+ +

+ + + + + +

+ +

+ + + +

+ +

+ + + +

+ +

+ + + + + +

+ + +



+ + +

๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜

+ +![image](https://github.com/khsrla9806/livable-server/assets/70641477/383dee4a-7032-4ef0-b147-d315a4bb5672) + + +



+ + +

ERD

+ +![image](https://github.com/khsrla9806/livable-server/assets/70641477/4ae505f2-139f-406c-b358-b1f11d1982f6) + + +



+ + +

Jacoco ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€

+ +> ๋ฐฑ์—”๋“œํŒ€ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ 40% ์ด์ƒ ๋‹ฌ์„ฑ + +![image](https://github.com/khsrla9806/livable-server/assets/70641477/cf7ea67a-e881-4a0a-a26c-4db876acb0ea) + + + +



+ + +

ํ”„๋กœ์ ํŠธ ์‹คํ–‰ํ•˜๊ธฐ

+ +### application.yml +``` yaml +# Spring, DB propertiesg setting +spring: + datasource: + url: #DB Address + driver-class-name: #DB Driver + username: #DB Username + password: #DB Password + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + default_batch_fetch_size: 100 + +# S3 propertiesg setting +cloud: + aws: + s3: + bucket: #S3 bucketName + credentials: + accessKey: #S3 accessKey + secretKey: #S3 secretKey + region: + static: ap-northeast-2 + stack: + auto: false + +# JWT properties setting +jwt: + secret: #JWT key +``` + +### build and test +```bash +$ ./gradlew clean build +``` + +### run +```bash +$ java -jar ./build/libs/server-0.0.1-SNAPSHOT.jar +``` + + +



+ + +

๋ฐฑ์—”๋“œ ํŒ€์›

+ + + + + + + + + + +
+
์ •ํ˜„์ˆ˜

+
+
๊น€ํ›ˆ์„ญ

+
+
์ตœํƒœ์œค

+
+
๊น€ํƒœ์ผ

+
+
๋ฐฐ์ข…์œค

+
+ +



+ +

ํŒ€์› ์—ญํ• 

+ +
+ + | ์ด๋ฆ„ | ์—ญํ•  | + | :-----------------: | -------------------------------- | + | ์ •ํ˜„์ˆ˜
`ํŒ€์žฅ` | - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„
- API ๋ช…์„ธ์„œ ์„ค๊ณ„
- S3 ์—…๋กœ๋“œ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
- ํฌ์ธํŠธ API ๊ตฌํ˜„
- ์‹๋‹น ๋ฆฌ๋ทฐ API ๊ตฌํ˜„
- ๋‹ค์ค‘ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
| + | ๊น€ํ›ˆ์„ญ | - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„
- API ๋ช…์„ธ์„œ ์„ค๊ณ„
- EC2 ์„œ๋ฒ„ํ™˜๊ฒฝ ๊ตฌ์„ฑ
- ์ž๋™ํ™” ๋ฐฐํฌํ™˜๊ฒฝ ๊ตฌ์„ฑ
- SSL ์ ์šฉ (HTTPS)
- ์ดˆ๋Œ€์žฅ CRUD ๊ตฌํ˜„
- Kakao ์•Œ๋ฆผํ†ก ์ ์šฉ
| + | ์ตœํƒœ์œค | - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„
- API ๋ช…์„ธ์„œ ์„ค๊ณ„
- ์ž๋™ํ™” ๋ฐฐํฌํ™˜๊ฒฝ ๊ตฌ์„ฑ
- ๋ฐฉ๋ฌธ์ฆ API ๊ตฌํ˜„
| + | ๊น€ํƒœ์ผ | - ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ์„ค๊ณ„
- API ๋ช…์„ธ์„œ ์„ค๊ณ„
- ์‹๋‹น ๋ฆฌ๋ทฐ API ๊ตฌํ˜„
| + | ๋ฐฐ์ข…์œค | - ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ์„ค๊ณ„
- API ๋ช…์„ธ์„œ ์„ค๊ณ„
- ํ™ˆ ํ™”๋ฉด API ๊ตฌํ˜„
- ์˜ค๋Š˜ ์ ์‹ฌ ๋ญ๋จน์ง€ ๋ฃฐ๋ › API ๊ตฌํ˜„
- Spring Quartz ์Šค์ผ€์ฅด๋Ÿฌ๋ฅผ ์ด์šฉํ•œ ์ง‘๊ณ„ ๊ธฐ๋Šฅ ๊ตฌํ˜„
| + +
+ + From 05f745d20fc5a345f900d4733202749c2fa9f0e6 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Sat, 7 Oct 2023 02:28:03 +0900 Subject: [PATCH 107/111] =?UTF-8?q?=EB=A3=B0=EB=A0=9B=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=EC=A7=91=EA=B3=84=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor : Spring Quartz ์˜์กด์„ฑ ์ถ”๊ฐ€ (#156) ์Šค์ผ€์ฅด๋ง์„ ์œ„ํ•œ Spring Quartz ์˜์กด์„ฑ, Schema ์ถ”๊ฐ€ * Refactor : Cron ์Šค์ผ€์ฅด์šฉ Enum ์ถ”๊ฐ€ (#156) Daily, Weekly ์ถ”๊ฐ€ * Refactor : ๋ฉ”๋‰ด ์„ ํƒ ์ง‘๊ณ„ ๋กœ์ง ์ˆ˜์ • (#156) ๋ฉ”๋‰ด ์„ ํƒ ์ง‘๊ณ„ ๋กœ์ง ์ˆ˜์ •์œผ๋กœ ์ธํ•œ ์ฝ”๋“œ ๋ณ€๊ฒฝ ๊ธฐ์กด : ๋งค์ฃผ 1ํšŒ ํ•ฉ์‚ฐ ์ง‘๊ณ„ ๋ณ€๊ฒฝ : ๋งค์ผ ์ง‘๊ณ„ ํ›„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉ ํ•˜์—ฌ ์ฃผ 1ํšŒ ํ•ฉ์‚ฐ ์ง‘๊ณ„ * Test : ๋ฉ”๋‰ด ์„ ํƒ ์ง‘๊ณ„ ํ…Œ์ŠคํŠธ ์ˆ˜์ • (#156) ์ฝ”๋“œ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์ˆ˜์ • * Refactor : ๋ฉ”๋‰ด ์„ ํƒ ์ง‘๊ณ„ ์Šค์ผ€์ฅด๋Ÿฌ ๊ตฌํ˜„ (#156) Spring Quartz Lib๋ฅผ ์ด์šฉํ•œ ์Šค์ผ€์ฅด๋Ÿฌ ๊ตฌํ˜„ ๋งค์ผ ์„ ํƒ๋œ ๋ฉ”๋‰ด ์ง‘๊ณ„ ์ง‘๊ณ„๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์ฃผ 1ํšŒ ํ†ตํ•ฉ ์ง‘๊ณ„ ์Šค์ผ€์ฅด๋ง MisFire(์Šค์ผ€์ฅด๋Ÿฌ JOB ์‹คํŒจ)์‹œ ๋Œ€์‘ํ•˜๊ธฐ ์œ„ํ•ด Quartz Lib๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ * ReFactor : Early Return (#156) * Test : Test ์ฝ”๋“œ ์ˆ˜์ • (#156) --- build.gradle | 2 + .../menuchoiceresult/domain/CronSchedule.java | 14 ++++ .../job/MenuChoiceResultDailyJob.java | 38 +++++++++ .../job/MenuChoiceResultWeeklyJob.java | 37 +++++++++ .../listener/MenuChoiceJobListener.java | 35 ++++++++ .../listener/MenuChoiceTriggerListener.java | 42 ++++++++++ .../service/MenuChoiceQuartzService.java | 81 +++++++++++++++++++ .../server/entity/MenuChoiceWeeklyResult.java | 52 ++++++++++++ .../menu/controller/MenuController.java | 1 - .../domain/MenuChoiceResultDateRange.java | 32 ++++++++ .../server/menu/domain/ReferenceDate.java | 20 ----- .../server/menu/dto/MenuChoiceProjection.java | 18 +++++ .../repository/MenuChoiceLogRepository.java | 13 ++- .../MenuChoiceResultRepository.java | 18 +++++ .../MenuChoiceWeeklyResultRepository.java | 8 ++ .../menu/repository/MenuRepository.java | 18 ++--- .../menu/service/MenuChoiceResultService.java | 79 ++++++++++++++++++ .../server/menu/service/MenuService.java | 14 +++- .../service/MenuChoiceResultServiceTest.java | 49 +++++++++++ .../server/menu/service/MenuServiceTest.java | 16 ++-- ...ava => MenuChoiceResultDateRangeTest.java} | 2 +- 21 files changed, 547 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java create mode 100644 src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java create mode 100644 src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java create mode 100644 src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java delete mode 100644 src/main/java/com/livable/server/menu/domain/ReferenceDate.java create mode 100644 src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java create mode 100644 src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java create mode 100644 src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java create mode 100644 src/main/java/com/livable/server/menu/service/MenuChoiceResultService.java create mode 100644 src/test/java/com/livable/server/menu/service/MenuChoiceResultServiceTest.java rename src/test/java/com/livable/server/point/domain/{DateRangeTest.java => MenuChoiceResultDateRangeTest.java} (96%) diff --git a/build.gradle b/build.gradle index 1820af90..962eccb8 100644 --- a/build.gradle +++ b/build.gradle @@ -109,6 +109,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' + implementation 'org.springframework.boot:spring-boot-starter-quartz' + } test { diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java new file mode 100644 index 00000000..422f74cb --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java @@ -0,0 +1,14 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CronSchedule { + + EVERY_DAY_OF_ZERO("0 0 0 1/1 * ? *"), + EVERY_SUNDAY_OF_END("0 45 23 ? * SUN *"); + + private final String croneSchedule; +} diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java new file mode 100644 index 00000000..87c3dce8 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java @@ -0,0 +1,38 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.job; + +import com.livable.server.menu.service.MenuChoiceResultService; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +@Slf4j +public class MenuChoiceResultDailyJob implements Job { + + private final MenuChoiceResultService menuChoiceResultService; + + @Override + public void execute(JobExecutionContext context) { + + //Date of Job executed + Long milliseconds = context.getFireTime().getTime(); + + LocalDate referenceDate = millisecondsToDate(milliseconds); + + menuChoiceResultService.createDailyMenuChoiceResult(referenceDate); + + } + + private LocalDate millisecondsToDate(Long milliseconds) { + Instant instant = Instant.ofEpochMilli(milliseconds); + + ZoneId zoneId = ZoneId.systemDefault(); + return instant.atZone(zoneId).toLocalDate(); + } + + +} diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java new file mode 100644 index 00000000..28627a9a --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java @@ -0,0 +1,37 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.job; + +import com.livable.server.menu.service.MenuChoiceResultService; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +@Slf4j +public class MenuChoiceResultWeeklyJob implements Job { + + private final MenuChoiceResultService menuChoiceResultService; + + @Override + public void execute(JobExecutionContext context) { + + //Date of Job executed + Long milliseconds = context.getFireTime().getTime(); + + LocalDate referenceDate = millisecondsToDate(milliseconds); + + menuChoiceResultService.createWeeklyMenuChoiceResult(referenceDate); + + } + private LocalDate millisecondsToDate(Long milliseconds) { + Instant instant = Instant.ofEpochMilli(milliseconds); + + ZoneId zoneId = ZoneId.systemDefault(); + return instant.atZone(zoneId).toLocalDate(); + } + + +} diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java new file mode 100644 index 00000000..61fead2f --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java @@ -0,0 +1,35 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.listener; + +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class MenuChoiceJobListener implements org.quartz.JobListener { + + @Override + public String getName() { + return MenuChoiceJobListener.class.getName(); + } + + @Override + public void jobToBeExecuted(JobExecutionContext context) { + JobKey jobKey = context.getJobDetail().getKey(); + log.info("Job Executed : JobKey : {}",jobKey); + } + + @Override + public void jobExecutionVetoed(JobExecutionContext context) { + JobKey jobKey = context.getJobDetail().getKey(); + log.info("Job ExecutionVetoed : JobKey : {}",jobKey); + } + + @Override + public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { + JobKey jobKey = context.getJobDetail().getKey(); + log.info("Job Was Executed : JobKey : {}",jobKey); + } +} diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java new file mode 100644 index 00000000..0848aec6 --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java @@ -0,0 +1,42 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.listener; + +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.quartz.JobKey; +import org.quartz.Trigger; +import org.quartz.Trigger.CompletedExecutionInstruction; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class MenuChoiceTriggerListener implements org.quartz.TriggerListener { + + @Override + public String getName() { + return MenuChoiceTriggerListener.class.getName(); + } + + @Override + public void triggerFired(Trigger trigger, JobExecutionContext context) { + JobKey jobKey = trigger.getJobKey(); + log.info("Trigger Fired at {} : JobKey : {}", trigger.getStartTime(),jobKey); + } + + @Override + public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { + return false; + } + + @Override + public void triggerMisfired(Trigger trigger) { + JobKey jobKey = trigger.getJobKey(); + log.info("Trigger MisFired at {} : JobKey : {}", trigger.getStartTime(),jobKey); + } + + @Override + public void triggerComplete(Trigger trigger, JobExecutionContext context, + CompletedExecutionInstruction triggerInstructionCode) { + JobKey jobKey = trigger.getJobKey(); + log.info("Trigger Complete at {} : JobKey : {}", trigger.getStartTime(),jobKey); + } +} diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java new file mode 100644 index 00000000..bbd5220d --- /dev/null +++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java @@ -0,0 +1,81 @@ +package com.livable.server.core.util.scheduler.menuchoiceresult.service; + +import static org.quartz.CronScheduleBuilder.cronSchedule; + +import com.livable.server.core.util.scheduler.menuchoiceresult.domain.CronSchedule; +import com.livable.server.core.util.scheduler.menuchoiceresult.job.MenuChoiceResultWeeklyJob; +import com.livable.server.core.util.scheduler.menuchoiceresult.listener.MenuChoiceJobListener; +import com.livable.server.core.util.scheduler.menuchoiceresult.listener.MenuChoiceTriggerListener; +import com.livable.server.core.util.scheduler.menuchoiceresult.job.MenuChoiceResultDailyJob; +import java.util.TimeZone; +import java.util.UUID; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.ListenerManager; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MenuChoiceQuartzService { + + private final SchedulerFactoryBean schedulerFactory; + + private static final String GROUP_NAME = "Menu Choice Result"; + + @PostConstruct + public void scheduled() { + + Scheduler scheduler = schedulerFactory.getScheduler(); + + JobDetail menuChoiceResultDailyJob = menuChoiceResultJobDetail(MenuChoiceResultDailyJob.class); + CronTrigger menuChoiceResultDailyCronTrigger = menuChoiceResultCronTrigger(menuChoiceResultDailyJob, CronSchedule.EVERY_DAY_OF_ZERO.getCroneSchedule(), "๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒ๋œ ๋ฉ”๋‰ด ์ง‘๊ณ„ Daily Trigger"); + + JobDetail menuChoiceResultWeeklyJob = menuChoiceResultJobDetail(MenuChoiceResultWeeklyJob.class); + CronTrigger menuChoiceResultWeeklyCronTrigger = menuChoiceResultCronTrigger(menuChoiceResultWeeklyJob, CronSchedule.EVERY_SUNDAY_OF_END.getCroneSchedule(), "๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒ๋œ ๋ฉ”๋‰ด ์ง‘๊ณ„ WeeklyTrigger"); + + try { + //Listener Setting + ListenerManager listenerManager = scheduler.getListenerManager(); + listenerManager.addJobListener(new MenuChoiceJobListener()); + listenerManager.addTriggerListener(new MenuChoiceTriggerListener()); + + //Add Job & Trigger to schedule + scheduler.scheduleJob(menuChoiceResultDailyJob, menuChoiceResultDailyCronTrigger); + scheduler.scheduleJob(menuChoiceResultWeeklyJob, menuChoiceResultWeeklyCronTrigger); + + } catch (SchedulerException e) { + log.error("Menu Choice Result Scheduler Exception Occurred : {}", menuChoiceResultDailyJob.getKey()); + } + } + + public JobDetail menuChoiceResultJobDetail(Class clazz) { + + return JobBuilder.newJob(clazz) + .withIdentity(UUID.randomUUID().toString(), GROUP_NAME) + .withDescription(clazz.getSimpleName()) + .build(); + } + + public CronTrigger menuChoiceResultCronTrigger(JobDetail jobDetail, String schedule, String description) { + return TriggerBuilder.newTrigger() + .forJob(jobDetail) + .withIdentity(jobDetail.getKey().getName(), GROUP_NAME) + .withDescription(description) + .withSchedule(cronSchedule(schedule) + .inTimeZone(TimeZone.getDefault()) + .withMisfireHandlingInstructionFireAndProceed() + ) + .build(); + } + +} diff --git a/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java b/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java new file mode 100644 index 00000000..33376fe2 --- /dev/null +++ b/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java @@ -0,0 +1,52 @@ +package com.livable.server.entity; + +import java.time.LocalDate; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "menu_choice_weekly_result", + uniqueConstraints = + @UniqueConstraint( + name = "MENU_BUILDING_DATE_UNIQUE_IDX", + columnNames = {"building_id", "menu_id", "date"} + ) +) +@Entity +public class MenuChoiceWeeklyResult extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false, name = "building_id") + @ManyToOne(fetch = FetchType.LAZY) + private Building building; + + @JoinColumn(nullable = false, name = "menu_id") + @ManyToOne(fetch = FetchType.LAZY) + private Menu menu; + + @Column(nullable = false, name = "date") + private LocalDate date; + + @Column(nullable = false) + private Integer count; +} diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java index 99c11197..1a38f5dc 100644 --- a/src/main/java/com/livable/server/menu/controller/MenuController.java +++ b/src/main/java/com/livable/server/menu/controller/MenuController.java @@ -48,7 +48,6 @@ public ResponseEntity>> getMostSelectedMenu(@R //์ถ”ํ›„ ๊ฐ€์ ธ ์˜ค๋Š” ๋ฉ”๋‰ด ์ˆซ์ž ๋ณ€๊ฒฝ์‹œ ๋ณ€๊ฒฝ Pageable pageable = PageRequest.of(0, MOST_SELECTED_MENU.getLimit()); - List mostSelectedMenu = menuService.getMostSelectedMenu(buildingId, pageable); return ApiResponse.success(mostSelectedMenu, HttpStatus.OK); diff --git a/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java b/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java new file mode 100644 index 00000000..380fc387 --- /dev/null +++ b/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java @@ -0,0 +1,32 @@ +package com.livable.server.menu.domain; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MenuChoiceResultDateRange { + + private LocalDate startDate; + private LocalDate endDate; + + public static MenuChoiceResultDateRange getDateRange(LocalDate referenceDate) { + + LocalDate startDate; + LocalDate endDate; + + startDate = referenceDate.minusDays( + referenceDate.getDayOfWeek().getValue() - 1L) + .minusWeeks(1); + + endDate = startDate.plusDays(6); + + return MenuChoiceResultDateRange.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + } + +} diff --git a/src/main/java/com/livable/server/menu/domain/ReferenceDate.java b/src/main/java/com/livable/server/menu/domain/ReferenceDate.java deleted file mode 100644 index c6324ba5..00000000 --- a/src/main/java/com/livable/server/menu/domain/ReferenceDate.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.livable.server.menu.domain; - -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum ReferenceDate { - - START_WITH_MONDAY(1); - - private final Integer dayOfWeek; - - public static LocalDate getReferenceDate(LocalDate localDate, ReferenceDate referenceDate) { - return localDate.minusDays( - localDate.getDayOfWeek().getValue() - referenceDate.getDayOfWeek()) - .minusWeeks(1); - } -} diff --git a/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java b/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java new file mode 100644 index 00000000..9e6f88d4 --- /dev/null +++ b/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java @@ -0,0 +1,18 @@ +package com.livable.server.menu.dto; + +import com.livable.server.entity.Building; +import com.livable.server.entity.Menu; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MenuChoiceProjection { + + Building building; + Menu menu; + LocalDate date; + Long count; + +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java index 9a9b5d9a..74f18d41 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java @@ -1,14 +1,25 @@ package com.livable.server.menu.repository; import com.livable.server.entity.MenuChoiceLog; +import com.livable.server.menu.dto.MenuChoiceProjection; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface MenuChoiceLogRepository extends JpaRepository { - Optional findByMemberIdAndDate(Long memberId, LocalDate date); + Optional findByMemberIdAndDate(Long memberId, LocalDate date); + + @Query("SELECT new com.livable.server.menu.dto.MenuChoiceProjection(mcl.building, mcl.menu, mcl.date, COUNT(mcl.menu.id)) " + + "FROM MenuChoiceLog mcl " + + "WHERE mcl.date = :referenceDate " + + "GROUP BY mcl.building, mcl.menu, mcl.date " + + "ORDER BY count(mcl.menu.id) DESC") + List findMenuChoiceLog(@Param("referenceDate") LocalDate referenceDate); } diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java new file mode 100644 index 00000000..0ace18ce --- /dev/null +++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java @@ -0,0 +1,18 @@ +package com.livable.server.menu.repository; + +import com.livable.server.entity.MenuChoiceResult; +import com.livable.server.menu.dto.MenuChoiceProjection; +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MenuChoiceResultRepository extends JpaRepository { + @Query("SELECT new com.livable.server.menu.dto.MenuChoiceProjection(mcr.building, mcr.menu, mcr.date, sum(mcr.count)) " + + "FROM MenuChoiceResult mcr " + + "WHERE mcr.date between :startDate AND :endDate " + + "GROUP BY mcr.building, mcr.menu, mcr.date " + + "ORDER BY sum(mcr.count) DESC") + List findMenuChoiceResult(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java new file mode 100644 index 00000000..74074286 --- /dev/null +++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java @@ -0,0 +1,8 @@ +package com.livable.server.menu.repository; + +import com.livable.server.entity.MenuChoiceWeeklyResult; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MenuChoiceWeeklyResultRepository extends JpaRepository { + +} diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java index 223e9973..9c813d52 100644 --- a/src/main/java/com/livable/server/menu/repository/MenuRepository.java +++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java @@ -22,20 +22,20 @@ public interface MenuRepository extends JpaRepository { List findRouletteMenus(); @Query( - "SELECT new com.livable.server.menu.dto.MostSelectedMenuProjection(mcr.count, mcr.date, mcr.menu.id, m.name, m.representativeImageUrl) " + - "FROM MenuChoiceResult mcr " + + "SELECT new com.livable.server.menu.dto.MostSelectedMenuProjection(mcwr.count, mcwr.date, mcwr.menu.id, m.name, m.representativeImageUrl) " + + "FROM MenuChoiceWeeklyResult mcwr " + "JOIN Menu m " + - "ON m.id = mcr.menu.id " + - "WHERE mcr.building.id = :buildingId AND mcr.date = :referenceDate " + - "GROUP BY mcr.date, mcr.menu.id, m.name, m.representativeImageUrl " + - "ORDER BY mcr.count DESC " + "ON m.id = mcwr.menu.id " + + "WHERE mcwr.building.id = :buildingId AND mcwr.date = :referenceDate " + + "GROUP BY mcwr.date, mcwr.menu.id, m.name, m.representativeImageUrl " + + "ORDER BY mcwr.count DESC " ) List findMostSelectedMenuOrderByCount(@Param("buildingId") Long buildingId, @Param("referenceDate") LocalDate referenceDate, Pageable pageable); @Query( - "SELECT m " + - "FROM Menu m " + - "WHERE m.id in :menuList" + "SELECT m " + + "FROM Menu m " + + "WHERE m.id in :menuList" ) List findAllMenuByMenuId(@Param("menuList") List menuList); } diff --git a/src/main/java/com/livable/server/menu/service/MenuChoiceResultService.java b/src/main/java/com/livable/server/menu/service/MenuChoiceResultService.java new file mode 100644 index 00000000..3932dbbd --- /dev/null +++ b/src/main/java/com/livable/server/menu/service/MenuChoiceResultService.java @@ -0,0 +1,79 @@ +package com.livable.server.menu.service; + +import com.livable.server.entity.MenuChoiceResult; +import com.livable.server.entity.MenuChoiceWeeklyResult; +import com.livable.server.menu.domain.MenuChoiceResultDateRange; +import com.livable.server.menu.domain.MenuPaging; +import com.livable.server.menu.dto.MenuChoiceProjection; +import com.livable.server.menu.repository.MenuChoiceLogRepository; +import com.livable.server.menu.repository.MenuChoiceResultRepository; +import com.livable.server.menu.repository.MenuChoiceWeeklyResultRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class MenuChoiceResultService { + + private final MenuChoiceLogRepository menuChoiceLogRepository; + private final MenuChoiceResultRepository menuChoiceResultRepository; + private final MenuChoiceWeeklyResultRepository menuChoiceWeeklyResultRepository; + + @Transactional + public void createDailyMenuChoiceResult(LocalDate referenceDate) { + + List menuChoiceResults = new ArrayList<>(); + List menuChoiceLogs = menuChoiceLogRepository.findMenuChoiceLog(referenceDate); + + for (MenuChoiceProjection menuChoiceProjection : menuChoiceLogs) { + + MenuChoiceResult menuChoiceResult = MenuChoiceResult.builder() + .menu(menuChoiceProjection.getMenu()) + .building(menuChoiceProjection.getBuilding()) + .date(menuChoiceProjection.getDate()) + .count((menuChoiceProjection.getCount()).intValue()) + .build(); + + menuChoiceResults.add(menuChoiceResult); + } + + menuChoiceResultRepository.saveAll(menuChoiceResults); + + } + + @Transactional + public void createWeeklyMenuChoiceResult(LocalDate referenceDate) { + + MenuChoiceResultDateRange dateRange = MenuChoiceResultDateRange.getDateRange(referenceDate); + + LocalDate startDate = dateRange.getStartDate(); + LocalDate endDate = dateRange.getEndDate(); + + List menuChoiceProjections = menuChoiceResultRepository.findMenuChoiceResult(startDate, endDate); + List menuChoiceWeeklyResults = new ArrayList<>(); + + for (MenuChoiceProjection menuChoiceProjection : menuChoiceProjections) { + + MenuChoiceWeeklyResult menuChoiceWeeklyResult = MenuChoiceWeeklyResult.builder() + .menu(menuChoiceProjection.getMenu()) + .building(menuChoiceProjection.getBuilding()) + .date(menuChoiceProjection.getDate()) + .count((menuChoiceProjection.getCount()).intValue()) + .build(); + + menuChoiceWeeklyResults.add(menuChoiceWeeklyResult); + if (menuChoiceWeeklyResults.size() > MenuPaging.MOST_SELECTED_MENU.getLimit()) { + break; + } + + } + + menuChoiceWeeklyResultRepository.saveAll(menuChoiceWeeklyResults); + + } + +} diff --git a/src/main/java/com/livable/server/menu/service/MenuService.java b/src/main/java/com/livable/server/menu/service/MenuService.java index 45fc573f..159a81dd 100644 --- a/src/main/java/com/livable/server/menu/service/MenuService.java +++ b/src/main/java/com/livable/server/menu/service/MenuService.java @@ -9,8 +9,8 @@ import com.livable.server.entity.MenuChoiceLog; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.menu.domain.MenuChoiceResultDateRange; import com.livable.server.menu.domain.MenuErrorCode; -import com.livable.server.menu.domain.ReferenceDate; import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; @@ -87,16 +87,22 @@ private List convertToDTO(Map> menuG } public List getMostSelectedMenu(Long buildingId, Pageable pageable) { - //๊ธฐ์ค€์ผ(์ง€๋‚œ์ฃผ ์›”~์ผ) - LocalDate referenceDate = ReferenceDate.getReferenceDate(LocalDate.now(), ReferenceDate.START_WITH_MONDAY); + //๊ธฐ์ค€์ผ(์˜ค๋Š˜, ์ง€๋‚œ์ฃผ ์›”-์ผ) + LocalDate referenceDate = LocalDate.now(); - List mostSelectedMenuProjections = menuRepository.findMostSelectedMenuOrderByCount(buildingId, referenceDate, pageable); + MenuChoiceResultDateRange menuChoiceResultDateRange = MenuChoiceResultDateRange.getDateRange(referenceDate); + + List mostSelectedMenuProjections = menuRepository.findMostSelectedMenuOrderByCount(buildingId, menuChoiceResultDateRange.getEndDate(), pageable); return convertToDTO(mostSelectedMenuProjections); } private List convertToDTO(List mostSelectedMenuProjections) { + if (mostSelectedMenuProjections.isEmpty()) { + return List.of(); + } + List mostSelectedMenus = new ArrayList<>(); for (int i = 0; i < mostSelectedMenuProjections.size(); i++) { diff --git a/src/test/java/com/livable/server/menu/service/MenuChoiceResultServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuChoiceResultServiceTest.java new file mode 100644 index 00000000..4d405b96 --- /dev/null +++ b/src/test/java/com/livable/server/menu/service/MenuChoiceResultServiceTest.java @@ -0,0 +1,49 @@ +package com.livable.server.menu.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.livable.server.menu.dto.MenuChoiceProjection; +import com.livable.server.menu.repository.MenuChoiceLogRepository; +import com.livable.server.menu.repository.MenuChoiceResultRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MenuChoiceResultServiceTest { + + @InjectMocks + MenuChoiceResultService menuChoiceResultService; + + @Mock + MenuChoiceLogRepository menuChoiceLogRepository; + + @Mock + MenuChoiceResultRepository menuChoiceResultRepository; + + @DisplayName("SUCCESS - ๊ฐ€์žฅ ๋งŽ์ด ์„ ํƒํ•œ ๋ฉ”๋‰ด ํ†ต๊ณ„ ์ง‘๊ณ„ ์„ฑ๊ณต") + @Test + void MenuChoiceResultSuccess() { + // given + List menuChoiceLogs = new ArrayList<>(); + given(menuChoiceLogRepository.findMenuChoiceLog(any(LocalDate.class))).willReturn(menuChoiceLogs); + + // when + menuChoiceResultService.createDailyMenuChoiceResult(LocalDate.now()); + + // then + verify(menuChoiceResultRepository, times(1)).saveAll(anyList()); + } + +} diff --git a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java index a20e2a9f..c599cdff 100644 --- a/src/test/java/com/livable/server/menu/service/MenuServiceTest.java +++ b/src/test/java/com/livable/server/menu/service/MenuServiceTest.java @@ -5,7 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -21,8 +23,8 @@ import com.livable.server.entity.MenuChoiceLog; import com.livable.server.member.domain.MemberErrorCode; import com.livable.server.member.repository.MemberRepository; +import com.livable.server.menu.domain.MenuChoiceResultDateRange; import com.livable.server.menu.domain.MenuErrorCode; -import com.livable.server.menu.domain.ReferenceDate; import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO; import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO; import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO; @@ -31,6 +33,7 @@ import com.livable.server.menu.dto.RouletteMenuProjection; import com.livable.server.menu.repository.MenuChoiceLogRepository; import com.livable.server.menu.repository.MenuRepository; +import java.time.DayOfWeek; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -38,9 +41,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -118,8 +123,8 @@ void getMostSelectedMenusSuccess() { String menuName = "๋ฉ”๋‰ด1"; String menuUrl = "/dummyUrl"; int pageLimit = 1; - Pageable pageable = PageRequest.of(0, pageLimit); - LocalDate referenceDate = ReferenceDate.getReferenceDate(LocalDate.now(), ReferenceDate.START_WITH_MONDAY); + Pageable pageable = PageRequest.of(5, pageLimit); + MenuChoiceResultDateRange referenceDate = MenuChoiceResultDateRange.getDateRange(date); MostSelectedMenuProjection mostSelectedMenuProjection = new MostSelectedMenuProjection(count, @@ -137,9 +142,7 @@ void getMostSelectedMenusSuccess() { MostSelectedMenuDTO expected = new MostSelectedMenuDTO(date, count, pageLimit, menuId, menuName, menuUrl); - - - given(menuRepository.findMostSelectedMenuOrderByCount(buildingId, referenceDate, pageable)) + given(menuRepository.findMostSelectedMenuOrderByCount(buildingId, referenceDate.getEndDate(), pageable)) .willReturn(mostSelectedMenuProjections); //when @@ -155,6 +158,7 @@ void getMostSelectedMenusSuccess() { () -> assertEquals(expected.getMenuImage(), actual.get(0).getMenuImage()), () -> assertEquals(expected.getDate(), actual.get(0).getDate()) ); + } @DisplayName("SUCCESS - ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ์ €์žฅ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ : ๋‹น์ผ ์ฒซ ๋ฃฐ๋ › ์„ ํƒ ๊ฒฐ๊ณผ ๋ฐ˜์˜") diff --git a/src/test/java/com/livable/server/point/domain/DateRangeTest.java b/src/test/java/com/livable/server/point/domain/MenuChoiceResultDateRangeTest.java similarity index 96% rename from src/test/java/com/livable/server/point/domain/DateRangeTest.java rename to src/test/java/com/livable/server/point/domain/MenuChoiceResultDateRangeTest.java index 79fe54d1..4fee4b0d 100644 --- a/src/test/java/com/livable/server/point/domain/DateRangeTest.java +++ b/src/test/java/com/livable/server/point/domain/MenuChoiceResultDateRangeTest.java @@ -6,7 +6,7 @@ import java.time.LocalDateTime; -class DateRangeTest { +class MenuChoiceResultDateRangeTest { @DisplayName("์ƒ์„ฑ์ž ํ…Œ์ŠคํŠธ - ์„ฑ๊ณต") @Test From a0f22e3aae496fd303fdb9975ff1c39728845137 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Sat, 7 Oct 2023 02:29:46 +0900 Subject: [PATCH 108/111] =?UTF-8?q?=EB=A3=B0=EB=A0=9B=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=8B=9D=EB=8B=B9=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat : ๋นŒ๋”ฉ ๊ทผ์ฒ˜ ์‹๋‹น ๋ชฉ๋ก ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ (#213) ์˜คํƒˆ์ž ์ˆ˜์ • ๋ฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ * Typo : typo ์ˆ˜์ • (#213) * Feat : ๋นŒ๋”ฉ ๊ทผ์ฒ˜ ์‹๋‹น ๋ชฉ๋ก ์„œ๋น„์Šค ๊ตฌํ˜„ (#213) ์˜คํƒˆ์ž ์ˆ˜์ • ๋ฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ * Test : Test ์ฝ”๋“œ ์ˆ˜์ • (#213) * Refactor : ์žฌ ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ (#213) Pageable ๊ฐ์ฒด ์ƒ์„ฑ์„ ์œ„ํ•ด ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ * Feat : ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ถ”๊ฐ€ (#213) --- .../controller/RestaurantController.java | 28 +++++++--- .../restaurant/dto/RestaurantResponse.java | 8 +-- .../RestaurantCustomRepository.java | 3 +- .../RestaurantCustomRepositoryImpl.java | 7 +-- ...aurantGroupByMenuProjectionRepository.java | 35 +++++++++++++ .../restaurant/service/RestaurantService.java | 52 ++++++++++++------- .../controller/RestaurantControllerTest.java | 9 ++-- .../mock/MockRestaurantByMenuDto.java | 7 --- .../restaurant/mock/MockRestaurantDto.java | 7 +++ .../service/RestaurantServiceTest.java | 4 +- 10 files changed, 113 insertions(+), 47 deletions(-) delete mode 100644 src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java create mode 100644 src/test/java/com/livable/server/restaurant/mock/MockRestaurantDto.java diff --git a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java index 0adccbfc..a64035ba 100644 --- a/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java +++ b/src/main/java/com/livable/server/restaurant/controller/RestaurantController.java @@ -7,7 +7,8 @@ import com.livable.server.core.util.LoginActor; import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.dto.RestaurantResponse; -import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; +import com.livable.server.restaurant.dto.RestaurantResponse.ListMenuDTO; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsDto; import com.livable.server.restaurant.service.RestaurantService; import java.util.List; import lombok.RequiredArgsConstructor; @@ -43,7 +44,7 @@ public ResponseEntity> findRestaurantByCategory( @GetMapping("restaurant/{restaurantId}/menus") - public ResponseEntity>> sellMenuByRestaurant ( + public ResponseEntity>> sellMenuByRestaurant ( @PathVariable Long restaurantId, @LoginActor Actor actor ) { @@ -52,13 +53,13 @@ public ResponseEntity>> Long memberId = actor.getId(); - List result = restaurantService.findMenuList(memberId, restaurantId); + List result = restaurantService.findMenuList(memberId, restaurantId); return ApiResponse.success(result, HttpStatus.OK); } @GetMapping("/restaurants") - public ResponseEntity>> getRestaurantsByMenu( + public ResponseEntity>> getRestaurantsByMenu( @RequestParam("menuId") Long menuId, @LoginActor Actor actor ) { @@ -66,9 +67,23 @@ public ResponseEntity>> getRestaurantsByMenu( Long memberId = actor.getId(); - List restaurantsByMenuDtos = restaurantService.findRestaurantByMenuId(menuId, memberId); + List restaurantsDtos = restaurantService.findRestaurantByMenuId(menuId, memberId); - return ApiResponse.success(restaurantsByMenuDtos, HttpStatus.OK); + return ApiResponse.success(restaurantsDtos, HttpStatus.OK); + } + + @GetMapping("/restaurants/near") + public ResponseEntity>> getRestaurantsByBuilding( + @RequestParam("buildingId") Long buildingId, @LoginActor Actor actor + ) { + + JwtTokenProvider.checkMemberToken(actor); + + Long memberId = actor.getId(); + + List restaurantsDtos = restaurantService.findRestaurantByBuildingId(buildingId, memberId); + + return ApiResponse.success(restaurantsDtos, HttpStatus.OK); } @GetMapping("/restaurants/search") @@ -84,4 +99,5 @@ public ResponseEntity>> se return ApiResponse.success(result, HttpStatus.OK); } + } diff --git a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java index 4588b70f..1cb08a11 100644 --- a/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java +++ b/src/main/java/com/livable/server/restaurant/dto/RestaurantResponse.java @@ -29,7 +29,7 @@ public static class NearRestaurantDto { @Getter @NoArgsConstructor @AllArgsConstructor - public static class listMenuDTO { + public static class ListMenuDTO { private Long menuId; private String menuName; } @@ -38,7 +38,7 @@ public static class listMenuDTO { @Builder @NoArgsConstructor @AllArgsConstructor - public static class RestaurantsByMenuDto { + public static class RestaurantsDto { Long restaurantId; String restaurantName; @@ -50,8 +50,8 @@ public static class RestaurantsByMenuDto { Integer estimatedTime; String review; - public static RestaurantsByMenuDto from(RestaurantByMenuProjection restaurantByMenuProjection) { - return RestaurantsByMenuDto.builder() + public static RestaurantsDto from(RestaurantByMenuProjection restaurantByMenuProjection) { + return RestaurantsDto.builder() .restaurantId(restaurantByMenuProjection.getRestaurantId()) .restaurantName(restaurantByMenuProjection.getRestaurantName()) .tastePercentage(restaurantByMenuProjection.getTastePercentage()) diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java index 8cbd5f4f..c7ceef75 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepository.java @@ -2,6 +2,7 @@ import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.ListMenuDTO; import org.springframework.data.domain.Pageable; import java.util.List; @@ -14,7 +15,7 @@ List findRestaurantByBuildingIdAndRestaura Pageable pageable ); - List findMenuList(Long restaurantId); + List findMenuList(Long restaurantId); List findRestaurantByKeyword(Long buildingId, String keyword); } diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java index a9ff4802..c5eb7095 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantCustomRepositoryImpl.java @@ -2,6 +2,7 @@ import com.livable.server.entity.*; import com.livable.server.restaurant.dto.RestaurantResponse; +import com.livable.server.restaurant.dto.RestaurantResponse.ListMenuDTO; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; @@ -80,13 +81,13 @@ public List findRestaurantByBuildingIdAndR } @Override - public List findMenuList(Long restaurantId) { + public List findMenuList(Long restaurantId) { final QMenu menu = QMenu.menu; final QRestaurantMenuMap restaurantMenuMap = QRestaurantMenuMap.restaurantMenuMap; - JPAQuery query = queryFactory - .select(Projections.constructor(RestaurantResponse.listMenuDTO.class, + JPAQuery query = queryFactory + .select(Projections.constructor(ListMenuDTO.class, menu.id, menu.name )) diff --git a/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java b/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java index 503eab1b..190bc0f3 100644 --- a/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java +++ b/src/main/java/com/livable/server/restaurant/repository/RestaurantGroupByMenuProjectionRepository.java @@ -5,12 +5,14 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository public class RestaurantGroupByMenuProjectionRepository { private static final String FIND_RESTAURANT_BY_MENU_ID_QUERY; + private static final String FIND_RESTAURANT_BY_BUILDING_ID_QUERY; static { FIND_RESTAURANT_BY_MENU_ID_QUERY = "SELECT res.id as restaurantId, res.name as restaurantName, res.thumbnail_image_url as restaurantThumbnailUrl, res.address as address, " + @@ -35,6 +37,30 @@ public class RestaurantGroupByMenuProjectionRepository { "LEFT JOIN restaurant_review rsv ON res.id = rsv.restaurant_id " + "WHERE rmm.menu_id = :menuId AND m.id = :memberId " + "GROUP BY res.id, res.name, res.thumbnail_image_url, res.address, brm.in_building, brm.distance"; + + FIND_RESTAURANT_BY_BUILDING_ID_QUERY = "SELECT res.id as restaurantId, res.name as restaurantName, res.thumbnail_image_url as restaurantThumbnailUrl, res.address as address, " + + "brm.in_building as inBuilding, brm.distance as distance, " + + "MAX(" + + "(SELECT r.description " + + " FROM review r " + + " WHERE r.id = rsv.id " + + " ORDER BY r.created_at DESC " + + " LIMIT 1)) as review, " + + "(SELECT ROUND(SUM(CASE WHEN rsv2.taste = 'good' THEN 1 ELSE 0 END) / COUNT(rsv2.id) * 100, 0) " + + "FROM restaurant_review rsv2 " + + "WHERE rsv2.restaurant_id = res.id) as tastePercentage " + + "FROM member m " + + "JOIN company c " + + "ON m.company_id = c.id " + + "JOIN building_restaurant_map brm " + + "ON c.building_id = brm.building_id " + + "JOIN restaurant res ON brm.restaurant_id = res.id " + + "JOIN restaurant_menu_map rmm " + + "ON res.id = rmm.restaurant_id " + + "LEFT JOIN restaurant_review rsv ON res.id = rsv.restaurant_id " + + "WHERE brm.building_id = :buildingId AND m.id = :memberId " + + "GROUP BY res.id, res.name, res.thumbnail_image_url, res.address, brm.in_building, brm.distance " + + "Limit :page"; } @PersistenceContext @@ -49,5 +75,14 @@ public List findRestaurantByMenuId(Long menuId, Long return (List) query.getResultList(); } + public List findRestaurantByBuildingId(Long buildingId, Long memberId, Pageable pageable) { + + Query query = entityManager.createNativeQuery(FIND_RESTAURANT_BY_BUILDING_ID_QUERY, "RestaurantsByMenuMapping") + .setParameter("buildingId", buildingId) + .setParameter("memberId", memberId) + .setParameter("page", pageable.getPageSize()); + + return (List) query.getResultList(); + } } diff --git a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java index 9790f0f1..c989a87a 100644 --- a/src/main/java/com/livable/server/restaurant/service/RestaurantService.java +++ b/src/main/java/com/livable/server/restaurant/service/RestaurantService.java @@ -1,8 +1,6 @@ package com.livable.server.restaurant.service; import com.livable.server.core.exception.GlobalRuntimeException; -import com.livable.server.entity.Building; -import com.livable.server.entity.Company; import com.livable.server.entity.Member; import com.livable.server.entity.RestaurantCategory; import com.livable.server.member.repository.MemberRepository; @@ -10,22 +8,22 @@ import com.livable.server.restaurant.domain.RestaurantErrorCode; import com.livable.server.restaurant.dto.RestaurantByMenuProjection; import com.livable.server.restaurant.dto.RestaurantResponse; -import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; +import com.livable.server.restaurant.dto.RestaurantResponse.ListMenuDTO; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsDto; import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; import com.livable.server.restaurant.repository.RestaurantGroupByMenuProjectionRepository; import com.livable.server.restaurant.repository.RestaurantRepository; import com.livable.server.review.domain.ReviewErrorCode; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - @RequiredArgsConstructor @Service @Transactional(readOnly = true) @@ -45,11 +43,7 @@ public List findNearRestaurantByVisitorIdA Long buildingId = visitorRepository.findBuildingIdById(visitorId) .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); - Integer nearRestaurantCount = - buildingRestaurantMapRepository.countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( - buildingId, - category - ); + Integer nearRestaurantCount =getNearRestaurantCount(buildingId, category); if (nearRestaurantCount == 0) { return List.of(); @@ -60,8 +54,14 @@ public List findNearRestaurantByVisitorIdA ); } + private Integer getNearRestaurantCount(Long buildingId, RestaurantCategory category) { + return buildingRestaurantMapRepository.countBuildingRestaurantMapByBuildingIdAndRestaurant_RestaurantCategory( + buildingId, + category + ); + } - public List findMenuList(Long memberId, Long restaurantId) { + public List findMenuList(Long memberId, Long restaurantId) { checkExistMemberById(memberId); return restaurantRepository.findMenuList(restaurantId); @@ -73,7 +73,7 @@ private Member checkExistMemberById(Long memberId) { return memberOptional.orElseThrow(() -> new GlobalRuntimeException(ReviewErrorCode.MEMBER_NOT_EXIST)); } - public List findRestaurantByMenuId(Long menuId, Long memberId) { + public List findRestaurantByMenuId(Long menuId, Long memberId) { List restaurantByMenuProjections = restaurantGroupByMenuProjectionRepository.findRestaurantByMenuId(menuId, memberId); if (restaurantByMenuProjections.isEmpty()) { @@ -83,16 +83,29 @@ public List findRestaurantByMenuId(Long menuId, Long membe return getRestaurantsByMenu(restaurantByMenuProjections); } - private List getRestaurantsByMenu( + public List findRestaurantByBuildingId(Long buildingId, Long memberId) { + + Integer nearRestaurantCount = getNearRestaurantCount(buildingId, RestaurantCategory.RESTAURANT); + + List restaurantByMenuProjections = restaurantGroupByMenuProjectionRepository.findRestaurantByBuildingId(buildingId, memberId, randomGenerator.getRandom(nearRestaurantCount)); + + if (restaurantByMenuProjections.isEmpty()) { + throw new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_RESTAURANT_BY_MENU); + } + + return getRestaurantsByMenu(restaurantByMenuProjections); + } + + private List getRestaurantsByMenu( List restaurantByMenuProjections) { - List restaurantsByMenuDtos = new ArrayList<>(); + List restaurantsDtos = new ArrayList<>(); for (RestaurantByMenuProjection restaurantByMenuProjection : restaurantByMenuProjections) { - restaurantsByMenuDtos.add(RestaurantsByMenuDto.from(restaurantByMenuProjection)); + restaurantsDtos.add(RestaurantsDto.from(restaurantByMenuProjection)); } - return restaurantsByMenuDtos; + return restaurantsDtos; } public List findRestaurantByKeyword(Long memberId, String keyword) { @@ -107,4 +120,5 @@ private Long getBuildingByMember(Long memberId) { return memberRepository.findBuildingInfoByMemberId(memberId).get().getBuildingId(); } -} \ No newline at end of file + +} diff --git a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java index 2cf9dc18..5f11181b 100644 --- a/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java +++ b/src/test/java/com/livable/server/restaurant/controller/RestaurantControllerTest.java @@ -13,9 +13,9 @@ import com.livable.server.entity.RestaurantCategory; import com.livable.server.restaurant.domain.RestaurantErrorCode; import com.livable.server.restaurant.dto.RestaurantResponse; -import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsDto; import com.livable.server.restaurant.mock.MockNearRestaurantDto; -import com.livable.server.restaurant.mock.MockRestaurantByMenuDto; +import com.livable.server.restaurant.mock.MockRestaurantDto; import com.livable.server.restaurant.service.RestaurantService; import java.util.Date; import java.util.List; @@ -31,7 +31,6 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @Import(TestConfig.class) @WebMvcTest(RestaurantController.class) @@ -82,9 +81,9 @@ void findRestaurantByMenuSuccess() throws Exception { // given String token = tokenProvider.createActorToken(ActorType.MEMBER, 1L, new Date(new Date().getTime() + 10000000)); - List result = + List result = IntStream.range(1, 10) - .mapToObj(idx -> new MockRestaurantByMenuDto()) + .mapToObj(idx -> new MockRestaurantDto()) .collect(Collectors.toList()); given(restaurantService.findRestaurantByMenuId(anyLong(), anyLong())) diff --git a/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java b/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java deleted file mode 100644 index bc8fcd9a..00000000 --- a/src/test/java/com/livable/server/restaurant/mock/MockRestaurantByMenuDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.livable.server.restaurant.mock; - -import com.livable.server.restaurant.dto.RestaurantResponse; - -public class MockRestaurantByMenuDto extends RestaurantResponse.RestaurantsByMenuDto { - -} diff --git a/src/test/java/com/livable/server/restaurant/mock/MockRestaurantDto.java b/src/test/java/com/livable/server/restaurant/mock/MockRestaurantDto.java new file mode 100644 index 00000000..2928465f --- /dev/null +++ b/src/test/java/com/livable/server/restaurant/mock/MockRestaurantDto.java @@ -0,0 +1,7 @@ +package com.livable.server.restaurant.mock; + +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsDto; + +public class MockRestaurantDto extends RestaurantsDto { + +} diff --git a/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java index c831a493..b4e3a41b 100644 --- a/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java +++ b/src/test/java/com/livable/server/restaurant/service/RestaurantServiceTest.java @@ -16,7 +16,7 @@ import com.livable.server.restaurant.domain.RandomGenerator; import com.livable.server.restaurant.dto.RestaurantByMenuProjection; import com.livable.server.restaurant.dto.RestaurantResponse; -import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsByMenuDto; +import com.livable.server.restaurant.dto.RestaurantResponse.RestaurantsDto; import com.livable.server.restaurant.repository.BuildingRestaurantMapRepository; import com.livable.server.restaurant.repository.RestaurantGroupByMenuProjectionRepository; import com.livable.server.restaurant.repository.RestaurantRepository; @@ -143,7 +143,7 @@ void findRestaurantByMenuSuccess() { .willReturn(projections); // when - List actual = + List actual = restaurantService.findRestaurantByMenuId(anyLong(), anyLong()); // then From ee8cf29ab3247438e8c4d5b1025a31acc3938636 Mon Sep 17 00:00:00 2001 From: JY-B <61049995+jy-b@users.noreply.github.com> Date: Sat, 7 Oct 2023 02:41:04 +0900 Subject: [PATCH 109/111] DOCS : Modify Application.yml add properties of Spring quartz --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 87923da9..1532fb24 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,13 @@ spring: hibernate: default_batch_fetch_size: 100 + quartz: + auto-startup: true + job-store-type: jdbc + jdbc: + initialize-schema: never + overwrite-existing-jobs: false + # S3 propertiesg setting cloud: aws: @@ -115,6 +122,8 @@ cloud: # JWT properties setting jwt: secret: #JWT key + +# ``` ### build and test From a2361341d9b04af354c1a2139041ca0304978e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 19 Oct 2023 23:12:11 +0900 Subject: [PATCH 110/111] =?UTF-8?q?Swagger=203.0.0=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: swagger ๊ด€๋ จ ์˜์กด์„ฑ ์ถ”๊ฐ€ (#216) * Chore: swagger ๊ด€๋ จ ์„ค์ • ์ถ”๊ฐ€ (#216) * Feat: swagger ์„ค์ • ํด๋ž˜์Šค ๊ตฌํ˜„ (#216) --- build.gradle | 2 + .../server/core/config/SwaggerConfig.java | 37 +++++++++++++++++++ src/main/resources/application.yml | 3 ++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/com/livable/server/core/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 962eccb8..4af6188e 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' implementation 'org.springframework.boot:spring-boot-starter-quartz' + implementation "io.springfox:springfox-boot-starter:3.0.0" + implementation "io.springfox:springfox-swagger-ui:3.0.0" } test { diff --git a/src/main/java/com/livable/server/core/config/SwaggerConfig.java b/src/main/java/com/livable/server/core/config/SwaggerConfig.java new file mode 100644 index 00000000..70ecd9fd --- /dev/null +++ b/src/main/java/com/livable/server/core/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.livable.server.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerConfig { + + private static final String SERVICE_NAME = "์˜คํ”ผ์Šค๋„ˆ - ์‹์Šค์„ผ์Šค"; + private static final String API_VERSION = "V1"; + private static final String API_DESCRIPTION = "์˜คํ”ผ์Šค๋„ˆ"; + private static final String API_URL = "https://livableserver.site/"; + + @Bean + public Docket api() { + return new Docket(DocumentationType.OAS_30) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.livable.server")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder().title(SERVICE_NAME) + .version(API_VERSION) + .description(API_DESCRIPTION) + .termsOfServiceUrl(API_URL) + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 27a4dea6..6096153a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,9 @@ spring: properties: hibernate: default_batch_fetch_size: 100 + mvc: + path match: + matching-strategy: ant_path_matcher logging: pattern: From 8f33b0ca725294480a76684a76fad0c8668b10b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 19 Oct 2023 23:22:17 +0900 Subject: [PATCH 111/111] =?UTF-8?q?Swagger=203.0.0=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C=20(#219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 912c7561..39b025d4 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -6,6 +6,9 @@ spring: hibernate: show_sql: true format_sql: true + mvc: + path match: + matching-strategy: ant_path_matcher logging: pattern: