From 53a20063a84ab90164e6f3ab745cb62a70983530 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Thu, 3 Aug 2023 21:38:17 +0900 Subject: [PATCH 01/14] git init --- .gitignore | 37 +++ build.gradle | 35 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 92 +++++++ settings.gradle | 1 + .../example/jpaboard/JpaboardApplication.java | 13 + src/main/resources/application.properties | 1 + .../jpaboard/JpaboardApplicationTests.java | 13 + 10 files changed, 447 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 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/example/jpaboard/JpaboardApplication.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/example/jpaboard/JpaboardApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c2065bc26 --- /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 000000000..ad81a12de --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.2' + id 'io.spring.dependency-management' version '1.1.2' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} 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 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 '} + 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 000000000..93e3f59f1 --- /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 000000000..1ec9f6ed0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jpaboard' diff --git a/src/main/java/com/example/jpaboard/JpaboardApplication.java b/src/main/java/com/example/jpaboard/JpaboardApplication.java new file mode 100644 index 000000000..b9ea80234 --- /dev/null +++ b/src/main/java/com/example/jpaboard/JpaboardApplication.java @@ -0,0 +1,13 @@ +package com.example.jpaboard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JpaboardApplication { + + public static void main(String[] args) { + SpringApplication.run(JpaboardApplication.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/com/example/jpaboard/JpaboardApplicationTests.java b/src/test/java/com/example/jpaboard/JpaboardApplicationTests.java new file mode 100644 index 000000000..458cb1d45 --- /dev/null +++ b/src/test/java/com/example/jpaboard/JpaboardApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.jpaboard; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JpaboardApplicationTests { + + @Test + void contextLoads() { + } + +} From ae776a973f0f7ad9fb9eca791db5f0922834421b Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Thu, 3 Aug 2023 21:44:32 +0900 Subject: [PATCH 02/14] git init packages --- .../post/controller/PostController.java | 5 +++++ .../example/jpaboard/post/domain/Post.java | 5 +++++ .../jpaboard/post/service/PostService.java | 5 +++++ .../example/jpaboard/user/domain/Users.java | 21 +++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 src/main/java/com/example/jpaboard/post/controller/PostController.java create mode 100644 src/main/java/com/example/jpaboard/post/domain/Post.java create mode 100644 src/main/java/com/example/jpaboard/post/service/PostService.java create mode 100644 src/main/java/com/example/jpaboard/user/domain/Users.java diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java new file mode 100644 index 000000000..afbe72707 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -0,0 +1,5 @@ +package com.example.jpaboard.post.controller; + +public class PostController { + +} diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java new file mode 100644 index 000000000..c7895c7c6 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -0,0 +1,5 @@ +package com.example.jpaboard.post.domain; + +public class Post { + +} diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java new file mode 100644 index 000000000..5f372def9 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -0,0 +1,5 @@ +package com.example.jpaboard.post.service; + +public class PostService { + +} diff --git a/src/main/java/com/example/jpaboard/user/domain/Users.java b/src/main/java/com/example/jpaboard/user/domain/Users.java new file mode 100644 index 000000000..b7683ec4d --- /dev/null +++ b/src/main/java/com/example/jpaboard/user/domain/Users.java @@ -0,0 +1,21 @@ +package com.example.jpaboard.user.domain; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +public class Users { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Name name; + + private int age; + + private String hobby; + + private String created; + +} From 140f34373aa2d995b59a776e8da5377c5d512843 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Fri, 4 Aug 2023 00:33:56 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat=20:=20=C3=A3=C2=85Member=20=C3=AB?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../example/jpaboard/JpaboardApplication.java | 2 + .../jpaboard/config/AuditorAwareConfig.java | 39 +++++++++++++++++ .../example/jpaboard/global/BaseEntity.java | 32 ++++++++++++++ .../example/jpaboard/member/domain/Age.java | 25 +++++++++++ .../jpaboard/member/domain/Member.java | 36 ++++++++++++++++ .../member/service/MemberRepository.java | 8 ++++ .../member/service/MemberService.java | 37 ++++++++++++++++ .../service/dto/CreateMemberRequest.java | 7 ++++ .../service/dto/CreateMemberResponse.java | 15 +++++++ .../service/dto/FindMemberResponse.java | 23 ++++++++++ .../service/dto/FindMemberResponses.java | 15 +++++++ .../member/service/mapper/MemberMapper.java | 21 ++++++++++ .../example/jpaboard/user/domain/Users.java | 21 ---------- src/main/resources/application.properties | 1 - src/main/resources/application.yml | 25 +++++++++++ .../jpaboard/member/domain/AgeTest.java | 6 +++ .../member/service/MemberServiceTest.java | 42 +++++++++++++++++++ 18 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java create mode 100644 src/main/java/com/example/jpaboard/global/BaseEntity.java create mode 100644 src/main/java/com/example/jpaboard/member/domain/Age.java create mode 100644 src/main/java/com/example/jpaboard/member/domain/Member.java create mode 100644 src/main/java/com/example/jpaboard/member/service/MemberRepository.java create mode 100644 src/main/java/com/example/jpaboard/member/service/MemberService.java create mode 100644 src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java create mode 100644 src/main/java/com/example/jpaboard/member/service/dto/CreateMemberResponse.java create mode 100644 src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java create mode 100644 src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponses.java create mode 100644 src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java delete mode 100644 src/main/java/com/example/jpaboard/user/domain/Users.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/example/jpaboard/member/domain/AgeTest.java create mode 100644 src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java diff --git a/build.gradle b/build.gradle index ad81a12de..2514c500c 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,8 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + runtimeOnly 'com.h2database:h2' } tasks.named('test') { diff --git a/src/main/java/com/example/jpaboard/JpaboardApplication.java b/src/main/java/com/example/jpaboard/JpaboardApplication.java index b9ea80234..3b53903a0 100644 --- a/src/main/java/com/example/jpaboard/JpaboardApplication.java +++ b/src/main/java/com/example/jpaboard/JpaboardApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class JpaboardApplication { diff --git a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java new file mode 100644 index 000000000..f16b033a9 --- /dev/null +++ b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java @@ -0,0 +1,39 @@ +package com.example.jpaboard.config; + +import com.example.jpaboard.member.domain.Member; +import jakarta.annotation.Resource; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +/* +@EnableJpaAuditing +@Configuration +public class AuditorAwareConfig { + + private final Member memberInfo; + + @Autowired + public AuditorAwareConfig(Member memberInfo) { + this.memberInfo = memberInfo; + } + + @Bean + public AuditorAware auditorAware() { + return new AuditorAware<>() { + + @Override + public Optional getCurrentAuditor() { + Long userId = memberInfo.getId(); + return Optional.of(userId); + } + + }; + } + +} + + */ diff --git a/src/main/java/com/example/jpaboard/global/BaseEntity.java b/src/main/java/com/example/jpaboard/global/BaseEntity.java new file mode 100644 index 000000000..980338c03 --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/BaseEntity.java @@ -0,0 +1,32 @@ +package com.example.jpaboard.global; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; + +/* + +@Getter +@MappedSuperclass +@EntityListeners(value = {AuditingEntityListener.class}) +public class BaseEntity { + + @CreatedDate + @DateTimeFormat(pattern = "yyyy-MM-dd/HH:mm:ss") + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @CreatedBy + @DateTimeFormat(pattern = "yyyy-MM-dd/HH:mm:ss") + @Column(name = "created_by") + private Long createdBy; + +} + + */ diff --git a/src/main/java/com/example/jpaboard/member/domain/Age.java b/src/main/java/com/example/jpaboard/member/domain/Age.java new file mode 100644 index 000000000..57e8efbd6 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/domain/Age.java @@ -0,0 +1,25 @@ +package com.example.jpaboard.member.domain; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class Age { + + private static final int AGE_MIN = 0; + + private int age; + + protected Age() { } + + public Age(int age) { + validateAge(age); + this.age = age; + } + + private void validateAge(int age) { + if (age < AGE_MIN ) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/src/main/java/com/example/jpaboard/member/domain/Member.java b/src/main/java/com/example/jpaboard/member/domain/Member.java new file mode 100644 index 000000000..8bf5cd6a2 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -0,0 +1,36 @@ +package com.example.jpaboard.member.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "members") +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Embedded + private Age age; + + private String hobby; + + protected Member() { } + + public Member(String name, Age age, String hobby) { + this.name = name; + this.age = age; + this.hobby = hobby; + } +} diff --git a/src/main/java/com/example/jpaboard/member/service/MemberRepository.java b/src/main/java/com/example/jpaboard/member/service/MemberRepository.java new file mode 100644 index 000000000..1fdb84409 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/MemberRepository.java @@ -0,0 +1,8 @@ +package com.example.jpaboard.member.service; + +import com.example.jpaboard.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/jpaboard/member/service/MemberService.java b/src/main/java/com/example/jpaboard/member/service/MemberService.java new file mode 100644 index 000000000..6b9786932 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/MemberService.java @@ -0,0 +1,37 @@ +package com.example.jpaboard.member.service; + +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.dto.CreateMemberRequest; +import com.example.jpaboard.member.service.dto.CreateMemberResponse; +import com.example.jpaboard.member.service.dto.FindMemberResponse; +import com.example.jpaboard.member.service.mapper.MemberMapper; +import org.springframework.stereotype.Service; + +@Service +public class MemberService { + + private final MemberRepository memberRepository; + private final MemberMapper memberMapper; + + public MemberService(MemberRepository memberRepository, MemberMapper memberMapper) { + this.memberRepository = memberRepository; + this.memberMapper = memberMapper; + } + + /* + + CreateMemberResponse createMember(CreateMemberRequest createMemberRequest) { + Member member = memberMapper.to(createMemberRequest); + + return new CreateMemberResponse(memberRepository.save(member)); + } + + */ + + FindMemberResponse findById(Long id) { + Member member = memberRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않은 고객입니다.")); + + return new FindMemberResponse(member); + } + +} diff --git a/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java b/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java new file mode 100644 index 000000000..0fb9683af --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java @@ -0,0 +1,7 @@ +package com.example.jpaboard.member.service.dto; + +import com.example.jpaboard.member.domain.Age; + +public record CreateMemberRequest(String name, Age age, String hobby) { + +} diff --git a/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberResponse.java b/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberResponse.java new file mode 100644 index 000000000..0eca90d46 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberResponse.java @@ -0,0 +1,15 @@ +package com.example.jpaboard.member.service.dto; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; + +public record CreateMemberResponse(String name, Age age, String hobby) { + + public CreateMemberResponse(Member member) { + this(member.getName(), + member.getAge(), + member.getHobby()); + } + +} + diff --git a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java new file mode 100644 index 000000000..6553786de --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java @@ -0,0 +1,23 @@ +package com.example.jpaboard.member.service.dto; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import lombok.Getter; + +@Getter +public class FindMemberResponse { + + private final String name; + private final Age age; + private final String hobby; + + public FindMemberResponse(String name, Age age, String hobby) { + this.name = name; + this.age = age; + this.hobby = hobby; + } + public FindMemberResponse (Member member) { + this(member.getName(), member.getAge(), member.getHobby()); + } + +} diff --git a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponses.java b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponses.java new file mode 100644 index 000000000..ebeabb89b --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponses.java @@ -0,0 +1,15 @@ +package com.example.jpaboard.member.service.dto; + +import com.example.jpaboard.member.domain.Member; +import java.util.List; +import java.util.stream.Collectors; + +public record FindMemberResponses(List findMemberResponses) { + + public FindMemberResponses of(List members) { + List responses = members.stream().map(FindMemberResponse::new) + .collect(Collectors.toList()); + return new FindMemberResponses(responses); + } + +} diff --git a/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java new file mode 100644 index 000000000..e09459dba --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java @@ -0,0 +1,21 @@ +package com.example.jpaboard.member.service.mapper; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.dto.CreateMemberRequest; +import org.springframework.stereotype.Component; + +@Component +public class MemberMapper { + + private MemberMapper() { } + + public Member to(CreateMemberRequest createMemberRequest) { + String name = createMemberRequest.name(); + Age age = createMemberRequest.age(); + String hobby = createMemberRequest.hobby(); + + return new Member(name, age, hobby); + } + +} diff --git a/src/main/java/com/example/jpaboard/user/domain/Users.java b/src/main/java/com/example/jpaboard/user/domain/Users.java deleted file mode 100644 index b7683ec4d..000000000 --- a/src/main/java/com/example/jpaboard/user/domain/Users.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.jpaboard.user.domain; - -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; - -public class Users { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private Name name; - - private int age; - - private String hobby; - - private String created; - -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b1378917..000000000 --- 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 000000000..3e0299020 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,25 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + + jpa: + open-in-view: false + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate.format_sql: true + main: + allow-bean-definition-overriding : true + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true + + diff --git a/src/test/java/com/example/jpaboard/member/domain/AgeTest.java b/src/test/java/com/example/jpaboard/member/domain/AgeTest.java new file mode 100644 index 000000000..e9d6fcdc1 --- /dev/null +++ b/src/test/java/com/example/jpaboard/member/domain/AgeTest.java @@ -0,0 +1,6 @@ +package com.example.jpaboard.member.domain; + + +class AgeTest { + +} diff --git a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java new file mode 100644 index 000000000..cc945eb15 --- /dev/null +++ b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java @@ -0,0 +1,42 @@ +package com.example.jpaboard.member.service; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.dto.FindMemberResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +class MemberServiceTest { + + @Autowired + MemberRepository memberRepository; + + @Autowired + MemberService memberService; + Long id; + + @BeforeEach + void setUp() { + Member member = new Member("김별", new Age(26), "산책"); + memberRepository.save(member); + id = member.getId(); + } + + @Test + void findById_Member_Equals() { + //when + FindMemberResponse response = memberService.findById(id); + + //then + assertThat(response.getName()).isEqualTo("김별"); + assertThat(response.getHobby()).isEqualTo("산책"); + assertThat(response.getAge()).usingRecursiveComparison().isEqualTo(new Age(26)); + } + +} From fabdb67e0df56be9c4e056694057fa141cba8608 Mon Sep 17 00:00:00 2001 From: young970 Date: Fri, 4 Aug 2023 00:45:15 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20post=20crud=20service=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../Users.java => member/domain/Member.java} | 4 +- .../example/jpaboard/post/domain/Post.java | 40 ++++++++++++++++++ .../jpaboard/post/domain/PostRepository.java | 7 ++++ .../exception/EntityNotFoundException.java | 7 ++++ .../jpaboard/post/service/PostService.java | 41 +++++++++++++++++++ .../post/service/dto/PostResponse.java | 9 ++++ .../post/service/dto/PostResponses.java | 18 ++++++++ .../post/service/dto/SaveRequest.java | 4 ++ .../post/service/dto/SaveResponse.java | 9 ++++ .../post/service/dto/UpdateRequest.java | 4 ++ .../post/service/mapper/PostMapper.java | 19 +++++++++ src/main/resources/application.yml | 13 ++++++ 13 files changed, 174 insertions(+), 3 deletions(-) rename src/main/java/com/example/jpaboard/{user/domain/Users.java => member/domain/Member.java} (82%) create mode 100644 src/main/java/com/example/jpaboard/post/domain/PostRepository.java create mode 100644 src/main/java/com/example/jpaboard/post/exception/EntityNotFoundException.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java create mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index ad81a12de..0563327c0 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/example/jpaboard/user/domain/Users.java b/src/main/java/com/example/jpaboard/member/domain/Member.java similarity index 82% rename from src/main/java/com/example/jpaboard/user/domain/Users.java rename to src/main/java/com/example/jpaboard/member/domain/Member.java index b7683ec4d..b969548b8 100644 --- a/src/main/java/com/example/jpaboard/user/domain/Users.java +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -1,10 +1,10 @@ -package com.example.jpaboard.user.domain; +package com.example.jpaboard.member.domain; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -public class Users { +public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index c7895c7c6..e744f3260 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -1,5 +1,45 @@ package com.example.jpaboard.post.domain; +import com.example.jpaboard.member.domain.Member; +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "posts") public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Lob + @Column(nullable = false) + private String content; + + @ManyToOne(cascade = CascadeType.PERSIST) + private Member member; + + public Post() { + } + + public Post(String title, String content, Member member) { + this.title = title; + this.content = content; + this.member = member; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setContent(String content) { + this.content = content; + } + public void setMember(Member member) { + this.member = member; + } } diff --git a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java new file mode 100644 index 000000000..a29e0fba0 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java @@ -0,0 +1,7 @@ +package com.example.jpaboard.post.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/jpaboard/post/exception/EntityNotFoundException.java b/src/main/java/com/example/jpaboard/post/exception/EntityNotFoundException.java new file mode 100644 index 000000000..df76ea59a --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/exception/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.jpaboard.post.exception; + +public class EntityNotFoundException extends RuntimeException{ + public EntityNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index 5f372def9..0d9f76c9a 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -1,5 +1,46 @@ package com.example.jpaboard.post.service; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.post.domain.Post; +import com.example.jpaboard.post.domain.PostRepository; +import com.example.jpaboard.post.exception.EntityNotFoundException; +import com.example.jpaboard.post.service.dto.*; +import com.example.jpaboard.post.service.mapper.PostMapper; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + public class PostService { + private final PostRepository postRepository; + private final PostMapper mapper; + + public PostService(PostRepository postRepository, PostMapper mapper) { + this.postRepository = postRepository; + this.mapper = mapper; + } + + public PostResponses findAllBy(Pageable pageable) { + return PostResponses.of(postRepository.findAll(pageable)); + } + + public PostResponse findById(Long postId){ + Post findPost = postRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException("해당 post가 존재하지 않습니다.")); + return new PostResponse(findPost); + } + + @Transactional + public PostResponse savePost(SaveRequest request){ + Member findMember = memberService.findById(request.memberId());// 나중에 별님이랑 얘기할거 + return new PostResponse(postRepository.save(mapper.to(request, findMember))); + } + + @Transactional + public PostResponse updatePost(UpdateRequest request){ + Post findPost = postRepository.findById(request.postId()) + .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); + + findPost.setTitle(request.title()); + findPost.setContent(request.content()); + return new PostResponse(findPost); + } } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java new file mode 100644 index 000000000..25f33bbdd --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java @@ -0,0 +1,9 @@ +package com.example.jpaboard.post.service.dto; + +import com.example.jpaboard.post.domain.Post; + +public record PostResponse(String title, String content, String memberName) { + public PostResponse(Post post) { + this(post.getTitle(), post.getContent(), post.getMember().getName()); + } +} diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java new file mode 100644 index 000000000..26049f699 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java @@ -0,0 +1,18 @@ +package com.example.jpaboard.post.service.dto; + +import com.example.jpaboard.post.domain.Post; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +public record PostResponses(List postResponse) { + + public static PostResponses of(Page posts){ + List responses = posts.stream() + .map(PostResponse::new) + .collect(Collectors.toList()); + return new PostResponses(responses); + } + +} diff --git a/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java new file mode 100644 index 000000000..f364d4f57 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.service.dto; + +public record SaveRequest(Long memberId, String title, String content) { +} diff --git a/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java b/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java new file mode 100644 index 000000000..3fb9a6af5 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java @@ -0,0 +1,9 @@ +package com.example.jpaboard.post.service.dto; + +import com.example.jpaboard.post.domain.Post; + +public record SaveResponse(Long postId, String title, String content) { + public SaveResponse(Post post) { + this(post.getId(), post.getTitle(), post.getTitle()); + } +} diff --git a/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java new file mode 100644 index 000000000..c01712c03 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.service.dto; + +public record UpdateRequest(Long postId, String title, String content, Long memberId) { +} diff --git a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java new file mode 100644 index 000000000..b9447d191 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java @@ -0,0 +1,19 @@ +package com.example.jpaboard.post.service.mapper; + +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.post.domain.Post; +import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.SaveResponse; +import com.example.jpaboard.post.service.dto.UpdateRequest; +import org.springframework.stereotype.Component; + +@Component +public class PostMapper { + private PostMapper() { } + + public Post to(SaveRequest request, Member member){ + return new Post(request.title(), + request.content(), + member); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..8ca0e9aa0 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:~/order;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + jpa: + open-in-view: false + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate.format_sql: true \ No newline at end of file From 72e7ea85e9bff43b3450940750879385f2e458fe Mon Sep 17 00:00:00 2001 From: young970 Date: Fri, 4 Aug 2023 22:17:38 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20PostController,=20PostServiceTest?= =?UTF-8?q?=20=EC=9D=BC=EB=B6=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/jpaboard/global/ApiResponse.java | 14 +++ .../jpaboard/global/SliceResponse.java | 14 +++ .../example/jpaboard/global/SuccessCode.java | 30 +++++ .../jpaboard/member/domain/Member.java | 57 +++++++++ .../member/service/MemberService.java | 14 +-- .../service/dto/FindMemberResponse.java | 6 +- .../post/controller/PostController.java | 56 +++++++++ .../controller/dto/FindAllApiRequest.java | 4 + .../post/controller/dto/SaveApiRequest.java | 4 + .../post/controller/dto/UpdateApiRequest.java | 4 + .../post/controller/mapper/PostApiMapper.java | 31 +++++ .../example/jpaboard/post/domain/Post.java | 21 +++- .../jpaboard/post/domain/PostRepository.java | 11 ++ .../PermissionDeniedEditException.java | 7 ++ .../jpaboard/post/service/PostService.java | 35 ++++-- .../post/service/dto/FindAllRequest.java | 4 + .../post/service/dto/PostResponse.java | 4 +- .../post/service/dto/PostResponses.java | 4 +- .../post/service/dto/SaveResponse.java | 9 -- .../post/service/dto/UpdateRequest.java | 2 +- .../post/service/mapper/PostMapper.java | 6 +- src/main/resources/application.yml | 14 +-- .../jpaboard/member/domain/AgeTest.java | 14 +++ .../member/service/MemberServiceTest.java | 5 +- .../post/controller/PostControllerTest.java | 64 ++++++++++ .../post/service/PostServiceTest.java | 109 ++++++++++++++++++ src/test/resources/application.yml | 23 ++++ 27 files changed, 509 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/example/jpaboard/global/ApiResponse.java create mode 100644 src/main/java/com/example/jpaboard/global/SliceResponse.java create mode 100644 src/main/java/com/example/jpaboard/global/SuccessCode.java create mode 100644 src/main/java/com/example/jpaboard/member/domain/Member.java create mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java create mode 100644 src/main/java/com/example/jpaboard/post/exception/PermissionDeniedEditException.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java create mode 100644 src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java create mode 100644 src/test/java/com/example/jpaboard/post/service/PostServiceTest.java create mode 100644 src/test/resources/application.yml diff --git a/src/main/java/com/example/jpaboard/global/ApiResponse.java b/src/main/java/com/example/jpaboard/global/ApiResponse.java new file mode 100644 index 000000000..35decbc7a --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/ApiResponse.java @@ -0,0 +1,14 @@ +package com.example.jpaboard.global; + +public class ApiResponse { + + private T result; + private int resultCode; + private String resultMsg; + + public ApiResponse(final T result, SuccessCode successCode) { + this.result = result; + this.resultCode = successCode.getStatus(); + this.resultMsg = successCode.getMessage(); + } +} diff --git a/src/main/java/com/example/jpaboard/global/SliceResponse.java b/src/main/java/com/example/jpaboard/global/SliceResponse.java new file mode 100644 index 000000000..69388228b --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/SliceResponse.java @@ -0,0 +1,14 @@ +package com.example.jpaboard.global; + +import com.example.jpaboard.post.service.dto.PostResponse; +import org.springframework.data.domain.Slice; + +public class SliceResponse extends ApiResponse{ + + private Slice data; + + public SliceResponse(Slice data, SuccessCode successCode) { + super(data, successCode); + } + +} diff --git a/src/main/java/com/example/jpaboard/global/SuccessCode.java b/src/main/java/com/example/jpaboard/global/SuccessCode.java new file mode 100644 index 000000000..11132b4f2 --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/SuccessCode.java @@ -0,0 +1,30 @@ +package com.example.jpaboard.global; + +public enum SuccessCode { + + SUCCESS(200, "200", "REQUEST SUCCESS"), + SELECT_SUCCESS(200, "200", "SELECT SUCCESS"), + DELETE_SUCCESS(200, "200", "DELETE SUCCESS"), + INSERT_SUCCESS(201, "201", "INSERT SUCCESS"), + UPDATE_SUCCESS(204, "204", "UPDATE SUCCESS"), + ; + + private final int status; + private final String code; + private final String message; + + SuccessCode(final int status, final String code, final String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getMessage() { + return message; + } + +} diff --git a/src/main/java/com/example/jpaboard/member/domain/Member.java b/src/main/java/com/example/jpaboard/member/domain/Member.java new file mode 100644 index 000000000..0fdb6b511 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -0,0 +1,57 @@ +package com.example.jpaboard.member.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "members") +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Embedded + private Age age; + + private String hobby; + + protected Member() { } + + public Member(String name, Age age, String hobby) { + this.name = name; + this.age = age; + this.hobby = hobby; + } + + public Member(Long id, String name, Age age, String hobby) { + this.id = id; + this.name = name; + this.age = age; + this.hobby = hobby; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Age getAge() { + return age; + } + + public String getHobby() { + return hobby; + } +} diff --git a/src/main/java/com/example/jpaboard/member/service/MemberService.java b/src/main/java/com/example/jpaboard/member/service/MemberService.java index 6b9786932..c95c53fa9 100644 --- a/src/main/java/com/example/jpaboard/member/service/MemberService.java +++ b/src/main/java/com/example/jpaboard/member/service/MemberService.java @@ -1,8 +1,6 @@ package com.example.jpaboard.member.service; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.dto.CreateMemberRequest; -import com.example.jpaboard.member.service.dto.CreateMemberResponse; import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.member.service.mapper.MemberMapper; import org.springframework.stereotype.Service; @@ -18,17 +16,7 @@ public MemberService(MemberRepository memberRepository, MemberMapper memberMappe this.memberMapper = memberMapper; } - /* - - CreateMemberResponse createMember(CreateMemberRequest createMemberRequest) { - Member member = memberMapper.to(createMemberRequest); - - return new CreateMemberResponse(memberRepository.save(member)); - } - - */ - - FindMemberResponse findById(Long id) { + public FindMemberResponse findById(Long id) { Member member = memberRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않은 고객입니다.")); return new FindMemberResponse(member); diff --git a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java index 6553786de..3b45b30cc 100644 --- a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java +++ b/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java @@ -7,17 +7,19 @@ @Getter public class FindMemberResponse { + private final Long id; private final String name; private final Age age; private final String hobby; - public FindMemberResponse(String name, Age age, String hobby) { + public FindMemberResponse(Long id, String name, Age age, String hobby) { + this.id = id; this.name = name; this.age = age; this.hobby = hobby; } public FindMemberResponse (Member member) { - this(member.getName(), member.getAge(), member.getHobby()); + this(member.getId(), member.getName(), member.getAge(), member.getHobby()); } } diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index afbe72707..f34dabc0c 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -1,5 +1,61 @@ package com.example.jpaboard.post.controller; +import com.example.jpaboard.global.ApiResponse; +import com.example.jpaboard.global.SliceResponse; +import com.example.jpaboard.global.SuccessCode; +import com.example.jpaboard.post.controller.dto.FindAllApiRequest; +import com.example.jpaboard.post.controller.dto.SaveApiRequest; +import com.example.jpaboard.post.controller.dto.UpdateApiRequest; +import com.example.jpaboard.post.controller.mapper.PostApiMapper; +import com.example.jpaboard.post.service.PostService; +import com.example.jpaboard.post.service.dto.*; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +@RestController("/posts") public class PostController { + private final PostService postService; + private final PostApiMapper postApiMapper; + + public PostController(PostService postService, PostApiMapper postApiMapper) { + this.postService = postService; + this.postApiMapper = postApiMapper; + } + + @GetMapping + SliceResponse findAllBy(@ModelAttribute FindAllApiRequest findAllApiRequest, Pageable pageable) { + FindAllRequest findAllRequest = postApiMapper.toFindAllRequest(findAllApiRequest); + + return new SliceResponse<>(postService.findPostAllByFilter(findAllRequest, pageable), SuccessCode.SELECT_SUCCESS); + } + + @PatchMapping("/{id}") + ApiResponse updatePost (@PathVariable Long postId, @RequestBody UpdateApiRequest updateApiRequest) { + UpdateRequest updateRequest = postApiMapper.toUpdateRequest(updateApiRequest); + return new ApiResponse<>(postService.updatePost(postId, updateRequest),SuccessCode.UPDATE_SUCCESS); + } + + @GetMapping("/{id}") + ApiResponse findById (@PathVariable Long id) { + return new ApiResponse<>(postService.findById(id), SuccessCode.SELECT_SUCCESS); + } + + @PostMapping + ResponseEntity savePost (@RequestBody SaveApiRequest saveApiRequest ) { + SaveRequest saveRequest = postApiMapper.toSaveRequest(saveApiRequest); + PostResponse saveResponse =postService.savePost(saveRequest); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(saveResponse.postId()) + .toUri(); + + return ResponseEntity.created(location).body(saveResponse); + } + } diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java new file mode 100644 index 000000000..393ca8e68 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.controller.dto; + +public record FindAllApiRequest (String title , + String content ) { } diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java new file mode 100644 index 000000000..f55776687 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.controller.dto; + +public record SaveApiRequest(Long memberId, String title, String content) { +} diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java new file mode 100644 index 000000000..90b7e8008 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.controller.dto; + +public record UpdateApiRequest(String title, String content, Long memberId) { +} diff --git a/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java new file mode 100644 index 000000000..30e01fa97 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java @@ -0,0 +1,31 @@ +package com.example.jpaboard.post.controller.mapper; + +import com.example.jpaboard.post.controller.dto.FindAllApiRequest; +import com.example.jpaboard.post.controller.dto.SaveApiRequest; +import com.example.jpaboard.post.controller.dto.UpdateApiRequest; +import com.example.jpaboard.post.service.dto.FindAllRequest; +import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.UpdateRequest; +import org.springframework.stereotype.Component; + +@Component +public class PostApiMapper { + + public FindAllRequest toFindAllRequest(FindAllApiRequest findAllApiRequest) { + + return new FindAllRequest(findAllApiRequest.title(), findAllApiRequest.content()); + } + + public UpdateRequest toUpdateRequest(UpdateApiRequest updateApiRequest) { + return new UpdateRequest(updateApiRequest.title(), + updateApiRequest.content(), + updateApiRequest.memberId()); + } + + public SaveRequest toSaveRequest(SaveApiRequest saveApiRequest) { + return new SaveRequest( saveApiRequest.memberId(), + saveApiRequest.title(), + saveApiRequest.content()); + } + +} diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index e744f3260..376abf16a 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -2,9 +2,7 @@ import com.example.jpaboard.member.domain.Member; import jakarta.persistence.*; -import lombok.Getter; -@Getter @Entity @Table(name = "posts") public class Post { @@ -22,8 +20,7 @@ public class Post { @ManyToOne(cascade = CascadeType.PERSIST) private Member member; - public Post() { - } + protected Post() { } public Post(String title, String content, Member member) { this.title = title; @@ -31,6 +28,22 @@ public Post(String title, String content, Member member) { this.member = member; } + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public Member getMember() { + return member; + } + public void setTitle(String title) { this.title = title; } diff --git a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java index a29e0fba0..02fd7d065 100644 --- a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java +++ b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java @@ -1,7 +1,18 @@ package com.example.jpaboard.post.domain; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.domain.Pageable; public interface PostRepository extends JpaRepository { + @Query("SELECT p FROM Post p " + + "WHERE (p.title LIKE %:title%) "+ + "AND (p.content LIKE %:contents%)") + Slice findPostAllByFilter(@Param("title") String title, + @Param("content") String contents, + Pageable pageable); + } diff --git a/src/main/java/com/example/jpaboard/post/exception/PermissionDeniedEditException.java b/src/main/java/com/example/jpaboard/post/exception/PermissionDeniedEditException.java new file mode 100644 index 000000000..780536209 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/exception/PermissionDeniedEditException.java @@ -0,0 +1,7 @@ +package com.example.jpaboard.post.exception; + +public class PermissionDeniedEditException extends RuntimeException{ + public PermissionDeniedEditException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index 0d9f76c9a..da5c58594 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -1,25 +1,37 @@ package com.example.jpaboard.post.service; -import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.MemberService; +import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; import com.example.jpaboard.post.exception.EntityNotFoundException; +import com.example.jpaboard.post.exception.PermissionDeniedEditException; import com.example.jpaboard.post.service.dto.*; import com.example.jpaboard.post.service.mapper.PostMapper; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service public class PostService { private final PostRepository postRepository; + private final MemberService memberService; private final PostMapper mapper; - public PostService(PostRepository postRepository, PostMapper mapper) { + public PostService(PostRepository postRepository, MemberService memberService, PostMapper mapper) { this.postRepository = postRepository; + this.memberService = memberService; this.mapper = mapper; } - public PostResponses findAllBy(Pageable pageable) { - return PostResponses.of(postRepository.findAll(pageable)); + public Slice findPostAllByFilter(FindAllRequest findAllRequest, Pageable pageable) { + + Slice results = postRepository.findPostAllByFilter(findAllRequest.title(), findAllRequest.content(), pageable); + return results.map(PostResponse::new); } public PostResponse findById(Long postId){ @@ -29,18 +41,27 @@ public PostResponse findById(Long postId){ @Transactional public PostResponse savePost(SaveRequest request){ - Member findMember = memberService.findById(request.memberId());// 나중에 별님이랑 얘기할거 + FindMemberResponse findMember = memberService.findById(request.memberId()); + return new PostResponse(postRepository.save(mapper.to(request, findMember))); } @Transactional - public PostResponse updatePost(UpdateRequest request){ - Post findPost = postRepository.findById(request.postId()) + public PostResponse updatePost(Long id, UpdateRequest request){ + Post findPost = postRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); + Long postOwnerId = findPost.getMember().getId(); + + if (isNotOwner(request.memberId(), postOwnerId)) + throw new PermissionDeniedEditException("해당 게시글을 수정할 권한이 없습니다."); findPost.setTitle(request.title()); findPost.setContent(request.content()); return new PostResponse(findPost); } + private static boolean isNotOwner(Long memberId, Long postOwnerId) { + return !Objects.equals(postOwnerId, memberId); + } + } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java new file mode 100644 index 000000000..bdd5a455f --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.service.dto; + +public record FindAllRequest(String title , + String content) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java index 25f33bbdd..7923d00f2 100644 --- a/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java @@ -2,8 +2,8 @@ import com.example.jpaboard.post.domain.Post; -public record PostResponse(String title, String content, String memberName) { +public record PostResponse(Long postId, String title, String content, String memberName) { public PostResponse(Post post) { - this(post.getTitle(), post.getContent(), post.getMember().getName()); + this(post.getId(), post.getTitle(), post.getContent(), post.getMember().getName()); } } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java index 26049f699..c5d8b4f48 100644 --- a/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java @@ -1,14 +1,14 @@ package com.example.jpaboard.post.service.dto; import com.example.jpaboard.post.domain.Post; -import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; import java.util.List; import java.util.stream.Collectors; public record PostResponses(List postResponse) { - public static PostResponses of(Page posts){ + public static PostResponses of(Slice posts){ List responses = posts.stream() .map(PostResponse::new) .collect(Collectors.toList()); diff --git a/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java b/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java deleted file mode 100644 index 3fb9a6af5..000000000 --- a/src/main/java/com/example/jpaboard/post/service/dto/SaveResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.jpaboard.post.service.dto; - -import com.example.jpaboard.post.domain.Post; - -public record SaveResponse(Long postId, String title, String content) { - public SaveResponse(Post post) { - this(post.getId(), post.getTitle(), post.getTitle()); - } -} diff --git a/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java index c01712c03..1561dd017 100644 --- a/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java +++ b/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java @@ -1,4 +1,4 @@ package com.example.jpaboard.post.service.dto; -public record UpdateRequest(Long postId, String title, String content, Long memberId) { +public record UpdateRequest(String title, String content, Long memberId) { } diff --git a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java index b9447d191..86e1d32c2 100644 --- a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java +++ b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java @@ -1,17 +1,17 @@ package com.example.jpaboard.post.service.mapper; import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.service.dto.SaveRequest; -import com.example.jpaboard.post.service.dto.SaveResponse; -import com.example.jpaboard.post.service.dto.UpdateRequest; import org.springframework.stereotype.Component; @Component public class PostMapper { private PostMapper() { } - public Post to(SaveRequest request, Member member){ + public Post to(SaveRequest request, FindMemberResponse memberResponse){ + Member member = new Member(memberResponse.getId(), memberResponse.getName(), memberResponse.getAge(), memberResponse.getHobby()); return new Post(request.title(), request.content(), member); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6227a63a9..044dc3a5a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,25 +1,16 @@ spring: datasource: driver-class-name: org.h2.Driver -<<<<<<< HEAD - url: jdbc:h2:~/order;DB_CLOSE_ON_EXIT=FALSE - username: sa - password: -======= - url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE + url: jdbc:h2:tcp://localhost/~/product;DB_CLOSE_ON_EXIT=FALSE username: sa password: ->>>>>>> main jpa: open-in-view: false hibernate: ddl-auto: create-drop show-sql: true properties: -<<<<<<< HEAD - hibernate.format_sql: true -======= hibernate.format_sql: true main: allow-bean-definition-overriding : true @@ -30,6 +21,3 @@ server: charset: UTF-8 enabled: true force: true - - ->>>>>>> main diff --git a/src/test/java/com/example/jpaboard/member/domain/AgeTest.java b/src/test/java/com/example/jpaboard/member/domain/AgeTest.java index e9d6fcdc1..3a50ca0be 100644 --- a/src/test/java/com/example/jpaboard/member/domain/AgeTest.java +++ b/src/test/java/com/example/jpaboard/member/domain/AgeTest.java @@ -1,6 +1,20 @@ package com.example.jpaboard.member.domain; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class AgeTest { + static final int MINUS_AGE = -10; + + @Test + @DisplayName("나이에 대해서 음수를 입력한 경우 예외를 던진다.") + void member_MinusAge_throwsException() { + //when_then + assertThatThrownBy(() -> new Age(MINUS_AGE)) + .isInstanceOf(IllegalArgumentException.class); + } + } diff --git a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java index cc945eb15..3eafe2563 100644 --- a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java +++ b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java @@ -7,11 +7,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; - +@ActiveProfiles("test") @SpringBootTest +@Transactional class MemberServiceTest { @Autowired diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java new file mode 100644 index 000000000..486ffda60 --- /dev/null +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -0,0 +1,64 @@ +package com.example.jpaboard.post.controller; + +import com.example.jpaboard.post.controller.mapper.PostApiMapper; +import com.example.jpaboard.post.service.PostService; +import com.example.jpaboard.post.service.dto.FindAllRequest; +import com.example.jpaboard.post.service.dto.PostResponse; +import com.example.jpaboard.post.service.dto.PostResponses; +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.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import static org.junit.jupiter.api.Assertions.*; + +@WebMvcTest(PostController.class) +class PostControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + PostService postService; + + @MockBean + private PostController postController; + + @MockBean + private PostApiMapper postApiMapper; + +// @Test +// void findAllBy() { +// //given +// String title = "엄청 맛있는 맛집을 발견했어요"; +// String content = "프로그래머스 주변 어쩌구저쩌구 타코집이 맛있습니다."; +// Pageable pageable = PageRequest.of(0, 10); +// +// FindAllRequest findAllRequest = new FindAllRequest(title,content); +// Slice results = +// +// +// given(postService.findPostAllByFilter(findAllRequest,pageable)).willReturn(); + + // +// +// } + + @Test + void updatePost() { + } + + @Test + void findById() { + } + + @Test + void savePost() { + } +} \ No newline at end of file diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java new file mode 100644 index 000000000..1d9699858 --- /dev/null +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -0,0 +1,109 @@ +package com.example.jpaboard.post.service; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.MemberRepository; +import com.example.jpaboard.post.domain.Post; +import com.example.jpaboard.post.domain.PostRepository; +import com.example.jpaboard.post.service.dto.PostResponse; +import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.UpdateRequest; +import org.assertj.core.api.Assertions; +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.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("test") +@SpringBootTest +@Transactional +class PostServiceTest { + + @Autowired + PostService postService; + @Autowired + PostRepository postRepository; + @Autowired + MemberRepository memberRepository; + + Long setupId1; + Long setupId2; + Long setupId3; + Long setupPostId2; + + @BeforeEach + void setup() { + Member member1 = new Member("김별", new Age(27), "락 부르기"); + Member member2 = new Member("윤영운", new Age(27), "저글링 돌리기"); + Member member3 = new Member("박세영", new Age(27), "산책"); + + Post post1 = new Post("별의 포스트 제목", "별의 포스트 내용", member1); + Post post2 = new Post("영운의 포스트 제목", "영운의 포스트 내용", member2); + Long memberId = member3.getId(); + + postRepository.save(post1); + postRepository.save(post2); + memberRepository.save(member3); + + setupId1 = post1.getId(); + setupId2 = post2.getId(); + setupId3 = member3.getId(); + } + + @Test + void findAllBy_pageable_PostResponses() { + } + + @Test + @DisplayName("setup에서 저장한 post를 조회하여 title과 memberName을 확인한다.") + void findById_correctPostId_PostResponse() { + //when + PostResponse response = postService.findById(setupId1); + + //then + String findTitle = response.title(); + String findMemberName = response.memberName(); + assertThat(findTitle).isEqualTo("별의 포스트 제목"); + assertThat(findMemberName).isEqualTo("김별"); + } + + @Test + @DisplayName("post 저장 후 title과 memberName을 확인한다.") + void savePost() { + //given + SaveRequest saveRequest = new SaveRequest(setupId3, "산책의 정석", "산책의 정석 내용"); + + //when + PostResponse postResponse = postService.savePost(saveRequest); + + //then + String savedTitle = postResponse.title(); + String savedMemberName = postResponse.memberName(); + assertThat(savedTitle).isEqualTo("산책의 정석"); + assertThat(savedMemberName).isEqualTo("박세영"); + } + + @Test + @DisplayName("setUp에서 저장한 post를 update후 title과 content를 확인한다.") + void updatePost() { + //given + UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupId2); + + //when + PostResponse postResponse = postService.updatePost(setupPostId2, updateRequest); + + //then + String updatedTitle = postResponse.title(); + String updatedContent = postResponse.content(); + assertThat(updatedTitle).isEqualTo("영운의 변경된 postTitle"); + assertThat(updatedContent).isEqualTo("영운의 변경된 content"); + } +} \ 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 000000000..7b30603e9 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,23 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + + jpa: + open-in-view: false + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate.format_sql: true + main: + allow-bean-definition-overriding : true + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true From a187d7e90612c28a1360e53dc2189fd27d54a85a Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 5 Aug 2023 17:01:06 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20-=20feat=20:=20restdoc=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20-=20test=20:=20controllerTest?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20-=20refactor=20:=20findAllBy=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20406=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BOOT-INF/classes/static/docs/index.html | 492 ++++++++++++++++++ build.gradle | 43 +- src/docs/asciidoc/index.adoc | 24 + .../example/jpaboard/JpaboardApplication.java | 2 - .../example/jpaboard/global/ApiResponse.java | 12 + .../example/jpaboard/global/CreatedBy.java | 16 + .../post/controller/PostController.java | 12 +- .../controller/dto/FindAllApiRequest.java | 26 +- .../jpaboard/post/domain/PostRepository.java | 5 +- src/main/resources/application.yml | 4 +- src/main/resources/static/docs/index.html | 492 ++++++++++++++++++ .../post/controller/PostControllerTest.java | 93 ++-- .../post/service/PostServiceTest.java | 7 +- 13 files changed, 1179 insertions(+), 49 deletions(-) create mode 100644 BOOT-INF/classes/static/docs/index.html create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/main/java/com/example/jpaboard/global/CreatedBy.java create mode 100644 src/main/resources/static/docs/index.html diff --git a/BOOT-INF/classes/static/docs/index.html b/BOOT-INF/classes/static/docs/index.html new file mode 100644 index 000000000..84c925dd9 --- /dev/null +++ b/BOOT-INF/classes/static/docs/index.html @@ -0,0 +1,492 @@ + + + + + + + +API Docs + + + + + +

+
+
+

게시판

+
+ +
+

/posts

+
+
Request
+

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/curl-request.adoc[] +Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-request.adoc[]

+
+
+
Response
+

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-response.adoc[] +Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/response-fields.adoc[]

+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 968ed61b3..da4e39526 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' + id "org.asciidoctor.jvm.convert" version "3.3.2" } group = 'com.example' @@ -12,8 +13,18 @@ java { } configurations { - compileOnly { - extendsFrom annotationProcessor + asciidoctorExt +} + +ext { + snippetsDir = file('build/generated-snippets') +} + +bootJar { + dependsOn asciidoctor + copy { + from "${asciidoctor.outputDir}" + into 'BOOT-INF/classes/static/docs' } } @@ -28,10 +39,36 @@ dependencies { runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' } tasks.named('test') { useJUnitPlatform() } + +test { + useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn test + inputs.dir snippetsDir +} + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +task copyDocument(type: Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static/docs") +} + +build { + dependsOn copyDocument +} + + diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..d3f9261b9 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,24 @@ += API Docs +:icons: font +:source-highlighter: highlightjs +:toc: left +:toc-title: API +:toclevels: 2 +:sectlinks: + +== 게시판 + +=== 검색 필드 및 무한 스크롤 + +=== /posts + +.Request + +include::{snippets}/post-findAllBy/curl-request.adoc[] +include::{snippets}/post-findAllBy/http-request.adoc[] + +.Response + +include::{snippets}/post-findAllBy/http-response.adoc[] +include::{snippets}/post-findAllBy/response-fields.adoc[] + diff --git a/src/main/java/com/example/jpaboard/JpaboardApplication.java b/src/main/java/com/example/jpaboard/JpaboardApplication.java index 3b53903a0..b9ea80234 100644 --- a/src/main/java/com/example/jpaboard/JpaboardApplication.java +++ b/src/main/java/com/example/jpaboard/JpaboardApplication.java @@ -2,9 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@EnableJpaAuditing @SpringBootApplication public class JpaboardApplication { diff --git a/src/main/java/com/example/jpaboard/global/ApiResponse.java b/src/main/java/com/example/jpaboard/global/ApiResponse.java index 35decbc7a..d1393f79a 100644 --- a/src/main/java/com/example/jpaboard/global/ApiResponse.java +++ b/src/main/java/com/example/jpaboard/global/ApiResponse.java @@ -11,4 +11,16 @@ public ApiResponse(final T result, SuccessCode successCode) { this.resultCode = successCode.getStatus(); this.resultMsg = successCode.getMessage(); } + + public T getResult() { + return result; + } + + public int getResultCode() { + return resultCode; + } + + public String getResultMsg() { + return resultMsg; + } } diff --git a/src/main/java/com/example/jpaboard/global/CreatedBy.java b/src/main/java/com/example/jpaboard/global/CreatedBy.java new file mode 100644 index 000000000..a6e745a7e --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/CreatedBy.java @@ -0,0 +1,16 @@ +package com.example.jpaboard.global; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CreatedBy { + + UUID mention() default "안녕하세용 ㅎ"; +} + + */ diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index f34dabc0c..aaff07368 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -10,13 +10,15 @@ import com.example.jpaboard.post.service.PostService; import com.example.jpaboard.post.service.dto.*; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; -@RestController("/posts") +@RestController +@RequestMapping("/posts") public class PostController { private final PostService postService; @@ -31,13 +33,15 @@ public PostController(PostService postService, PostApiMapper postApiMapper) { SliceResponse findAllBy(@ModelAttribute FindAllApiRequest findAllApiRequest, Pageable pageable) { FindAllRequest findAllRequest = postApiMapper.toFindAllRequest(findAllApiRequest); - return new SliceResponse<>(postService.findPostAllByFilter(findAllRequest, pageable), SuccessCode.SELECT_SUCCESS); + Slice postAllByFilter = postService.findPostAllByFilter(findAllRequest, pageable); + SliceResponse postResponseSliceResponse = new SliceResponse<>(postAllByFilter, SuccessCode.SELECT_SUCCESS); + return postResponseSliceResponse; } @PatchMapping("/{id}") - ApiResponse updatePost (@PathVariable Long postId, @RequestBody UpdateApiRequest updateApiRequest) { + ApiResponse updatePost (@PathVariable Long id, @RequestBody UpdateApiRequest updateApiRequest) { UpdateRequest updateRequest = postApiMapper.toUpdateRequest(updateApiRequest); - return new ApiResponse<>(postService.updatePost(postId, updateRequest),SuccessCode.UPDATE_SUCCESS); + return new ApiResponse<>(postService.updatePost(id, updateRequest),SuccessCode.UPDATE_SUCCESS); } @GetMapping("/{id}") diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java index 393ca8e68..c4040d1de 100644 --- a/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java +++ b/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java @@ -1,4 +1,26 @@ package com.example.jpaboard.post.controller.dto; -public record FindAllApiRequest (String title , - String content ) { } +public class FindAllApiRequest { + + private String title; + private String content; + + public FindAllApiRequest() { + title = ""; + content = ""; + } + + public FindAllApiRequest(String title, String content) { + this.title = title; + this.content = content; + } + + public String title() { + return title; + } + + public String content() { + return content; + } + +} diff --git a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java index 02fd7d065..f1d19454d 100644 --- a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java +++ b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java @@ -10,9 +10,8 @@ public interface PostRepository extends JpaRepository { @Query("SELECT p FROM Post p " + "WHERE (p.title LIKE %:title%) "+ - "AND (p.content LIKE %:contents%)") + "AND (p.content LIKE %:content%)") Slice findPostAllByFilter(@Param("title") String title, - @Param("content") String contents, + @Param("content") String content, Pageable pageable); - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 044dc3a5a..19ff75f1c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,14 +1,14 @@ spring: datasource: driver-class-name: org.h2.Driver - url: jdbc:h2:tcp://localhost/~/product;DB_CLOSE_ON_EXIT=FALSE + url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE username: sa password: jpa: open-in-view: false hibernate: - ddl-auto: create-drop + ddl-auto: create show-sql: true properties: hibernate.format_sql: true diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html new file mode 100644 index 000000000..84c925dd9 --- /dev/null +++ b/src/main/resources/static/docs/index.html @@ -0,0 +1,492 @@ + + + + + + + +API Docs + + + + + + +
+
+

게시판

+
+ +
+

/posts

+
+
Request
+

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/curl-request.adoc[] +Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-request.adoc[]

+
+
+
Response
+

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-response.adoc[] +Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/response-fields.adoc[]

+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java index 486ffda60..715752b6a 100644 --- a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -1,54 +1,85 @@ package com.example.jpaboard.post.controller; -import com.example.jpaboard.post.controller.mapper.PostApiMapper; -import com.example.jpaboard.post.service.PostService; +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.service.dto.FindAllRequest; import com.example.jpaboard.post.service.dto.PostResponse; -import com.example.jpaboard.post.service.dto.PostResponses; + +import java.util.List; + 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.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import org.springframework.restdocs.payload.JsonFieldType; + import org.springframework.test.web.servlet.MockMvc; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + -@WebMvcTest(PostController.class) +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureRestDocs class PostControllerTest { @Autowired MockMvc mockMvc; - @MockBean - PostService postService; - @MockBean - private PostController postController; - @MockBean - private PostApiMapper postApiMapper; + @Test + void findAllBy() throws Exception { + + + // API 호출 및 응답 문서화 + this.mockMvc.perform(get("/posts") + .param("title", "Your Title") + .param("content", "Your Content")) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("post-findAllBy", + responseFields( + subsectionWithPath("result").description("Result details"), + fieldWithPath("result.content[]").description("The list of content items"), + subsectionWithPath("result.pageable").description("Pagination information"), + fieldWithPath("result.pageable.sort.empty").description("Indicates if the sort is empty"), + fieldWithPath("result.pageable.sort.sorted").description("Indicates if the sort is sorted"), + fieldWithPath("result.pageable.sort.unsorted").description("Indicates if the sort is unsorted"), + fieldWithPath("result.pageable.offset").description("Offset value for pagination"), + fieldWithPath("result.pageable.pageSize").description("Page size for pagination"), + fieldWithPath("result.pageable.pageNumber").description("Page number"), + fieldWithPath("result.pageable.unpaged").description("Indicates if the page is unpaged"), + fieldWithPath("result.pageable.paged").description("Indicates if the page is paged"), + fieldWithPath("result.size").description("The page size"), + fieldWithPath("result.number").description("The current page number"), + fieldWithPath("result.sort.empty").description("Indicates if the sort is empty"), + fieldWithPath("result.sort.sorted").description("Indicates if the sort is sorted"), + fieldWithPath("result.sort.unsorted").description("Indicates if the sort is unsorted"), + fieldWithPath("result.first").description("Indicates if this is the first page"), + fieldWithPath("result.last").description("Indicates if this is the last page"), + fieldWithPath("result.numberOfElements").description("Number of elements in the current page"), + fieldWithPath("result.empty").description("Indicates if the content is empty"), + fieldWithPath("resultCode").description("The result code of the response"), + fieldWithPath("resultMsg").description("The result message of the response") + ) + )); -// @Test -// void findAllBy() { -// //given -// String title = "엄청 맛있는 맛집을 발견했어요"; -// String content = "프로그래머스 주변 어쩌구저쩌구 타코집이 맛있습니다."; -// Pageable pageable = PageRequest.of(0, 10); -// -// FindAllRequest findAllRequest = new FindAllRequest(title,content); -// Slice results = -// -// -// given(postService.findPostAllByFilter(findAllRequest,pageable)).willReturn(); - // -// -// } + } + @Test void updatePost() { @@ -61,4 +92,4 @@ void findById() { @Test void savePost() { } -} \ No newline at end of file +} diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java index 1d9699858..e49a9fbb7 100644 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -1,5 +1,5 @@ package com.example.jpaboard.post.service; - +/* import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.service.MemberRepository; @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +/* @ActiveProfiles("test") @SpringBootTest @Transactional @@ -106,4 +107,6 @@ void updatePost() { assertThat(updatedTitle).isEqualTo("영운의 변경된 postTitle"); assertThat(updatedContent).isEqualTo("영운의 변경된 content"); } -} \ No newline at end of file +} + + */ From a61bed899aa2ab18f7432fdd1877218bf10f95d3 Mon Sep 17 00:00:00 2001 From: young970 Date: Sat, 5 Aug 2023 19:46:48 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20PostServiceTest=20=EC=99=84?= =?UTF-8?q?=EC=84=B1,=20GlobalRestExceptionHandler=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 50 ++------ .../jpaboard/global/ErrorResponse.java | 9 ++ .../global/GlobalRestExceptionHandler.java | 76 +++++++++++ .../jpaboard/global/SliceResponse.java | 1 - .../post/controller/PostController.java | 3 +- .../post/controller/dto/SaveApiRequest.java | 4 +- .../example/jpaboard/post/domain/Post.java | 2 +- .../jpaboard/post/service/PostService.java | 4 +- .../post/service/dto/PostResponses.java | 7 +- src/test/java/com/example/jpaboard/Test1.java | 59 +++++++++ .../post/service/PostServiceTest.java | 118 +++++++++++++----- .../{application.yml => application-test.yml} | 0 12 files changed, 252 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/example/jpaboard/global/ErrorResponse.java create mode 100644 src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java create mode 100644 src/test/java/com/example/jpaboard/Test1.java rename src/test/resources/{application.yml => application-test.yml} (100%) diff --git a/build.gradle b/build.gradle index da4e39526..938c8ecd4 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' - id "org.asciidoctor.jvm.convert" version "3.3.2" + id 'org.asciidoctor.jvm.convert' version '3.3.2' } group = 'com.example' @@ -12,24 +12,12 @@ java { sourceCompatibility = '17' } -configurations { - asciidoctorExt +repositories { + mavenCentral() } ext { - snippetsDir = file('build/generated-snippets') -} - -bootJar { - dependsOn asciidoctor - copy { - from "${asciidoctor.outputDir}" - into 'BOOT-INF/classes/static/docs' - } -} - -repositories { - mavenCentral() + set('snippetsDir', file("build/generated-snippets")) } dependencies { @@ -37,38 +25,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' - annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'mysql:mysql-connector-java:8.0.32' } tasks.named('test') { - useJUnitPlatform() -} - -test { - useJUnitPlatform() outputs.dir snippetsDir + useJUnitPlatform() } -asciidoctor { - dependsOn test +tasks.named('asciidoctor') { inputs.dir snippetsDir + dependsOn test } - -asciidoctor.doFirst { - delete file('src/main/resources/static/docs') -} - -task copyDocument(type: Copy) { - dependsOn asciidoctor - from file("build/docs/asciidoc") - into file("src/main/resources/static/docs") -} - -build { - dependsOn copyDocument -} - - diff --git a/src/main/java/com/example/jpaboard/global/ErrorResponse.java b/src/main/java/com/example/jpaboard/global/ErrorResponse.java new file mode 100644 index 000000000..f95b95005 --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/ErrorResponse.java @@ -0,0 +1,9 @@ +package com.example.jpaboard.global; + +import java.time.LocalDateTime; + +public record ErrorResponse(int statusCode, String detail, String instance, String time) { + public ErrorResponse(int statusCode, String detail, String instance) { + this(statusCode, detail, instance, LocalDateTime.now().toString()); + } +} diff --git a/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java new file mode 100644 index 000000000..1f288b9f8 --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java @@ -0,0 +1,76 @@ +package com.example.jpaboard.global; + +import com.example.jpaboard.post.exception.EntityNotFoundException; +import com.example.jpaboard.post.exception.PermissionDeniedEditException; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@RestControllerAdvice +public class GlobalRestExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(GlobalRestExceptionHandler.class); + + @ExceptionHandler(BindException.class) + public ResponseEntity handleBindException(HttpServletRequest request, BindException e) { + StringBuilder resultStringBuilder = getResultStringBuilder(e); + int statusCode = HttpStatus.BAD_REQUEST.value(); + + return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, resultStringBuilder.toString(), request.getRequestURI())); + } + + @ExceptionHandler({MissingRequestHeaderException.class, HttpMessageNotReadableException.class, + HttpClientErrorException.BadRequest.class, NoHandlerFoundException.class}) + public ResponseEntity handleBadRequestException(HttpServletRequest request, Exception e) { + int statusCode = HttpStatus.BAD_REQUEST.value(); + + return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + } + + @ExceptionHandler(PermissionDeniedEditException.class) + public ResponseEntity handlePermissionDeniedEditException(HttpServletRequest request, PermissionDeniedEditException e) { + int statusCode = HttpStatus.FORBIDDEN.value(); + + return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleNotFoundException(HttpServletRequest request, EntityNotFoundException e) { + int statusCode = HttpStatus.NOT_FOUND.value(); + + return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(HttpServletRequest request, Exception e) { + logger.error("Sever Exception: ", e.getMessage()); + + return ResponseEntity.ok().build(); + } + + private static StringBuilder getResultStringBuilder(BindException e) { + BindingResult bindingResult = e.getBindingResult(); + StringBuilder stringBuilder = new StringBuilder(); + + for (FieldError fieldError : bindingResult.getFieldErrors()) { + stringBuilder.append(fieldError.getField()).append(":"); + stringBuilder.append(fieldError.getDefaultMessage()); + stringBuilder.append(", "); + } + return stringBuilder; + } + + private static ErrorResponse getErrorResponse(int status, String masesage, String requestURI) { + return new ErrorResponse(status, masesage, requestURI); + } +} diff --git a/src/main/java/com/example/jpaboard/global/SliceResponse.java b/src/main/java/com/example/jpaboard/global/SliceResponse.java index 69388228b..1abe3caa4 100644 --- a/src/main/java/com/example/jpaboard/global/SliceResponse.java +++ b/src/main/java/com/example/jpaboard/global/SliceResponse.java @@ -1,6 +1,5 @@ package com.example.jpaboard.global; -import com.example.jpaboard.post.service.dto.PostResponse; import org.springframework.data.domain.Slice; public class SliceResponse extends ApiResponse{ diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index aaff07368..3f6d008d3 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -9,6 +9,7 @@ import com.example.jpaboard.post.controller.mapper.PostApiMapper; import com.example.jpaboard.post.service.PostService; import com.example.jpaboard.post.service.dto.*; +import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; @@ -50,7 +51,7 @@ ApiResponse findById (@PathVariable Long id) { } @PostMapping - ResponseEntity savePost (@RequestBody SaveApiRequest saveApiRequest ) { + ResponseEntity savePost (@RequestBody @Valid SaveApiRequest saveApiRequest ) { SaveRequest saveRequest = postApiMapper.toSaveRequest(saveApiRequest); PostResponse saveResponse =postService.savePost(saveRequest); diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java index f55776687..438376eb6 100644 --- a/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java +++ b/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java @@ -1,4 +1,6 @@ package com.example.jpaboard.post.controller.dto; -public record SaveApiRequest(Long memberId, String title, String content) { +import jakarta.validation.constraints.NotNull; + +public record SaveApiRequest(@NotNull Long memberId, @NotNull String title, @NotNull String content) { } diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index 376abf16a..0c01299d2 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -17,7 +17,7 @@ public class Post { @Column(nullable = false) private String content; - @ManyToOne(cascade = CascadeType.PERSIST) + @ManyToOne private Member member; protected Post() { } diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index da5c58594..9e710ed12 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Objects; -import java.util.stream.Collectors; @Service public class PostService { @@ -42,8 +41,9 @@ public PostResponse findById(Long postId){ @Transactional public PostResponse savePost(SaveRequest request){ FindMemberResponse findMember = memberService.findById(request.memberId()); + Post post = mapper.to(request, findMember); - return new PostResponse(postRepository.save(mapper.to(request, findMember))); + return new PostResponse(postRepository.save(post)); } @Transactional diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java index c5d8b4f48..63870146c 100644 --- a/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponses.java @@ -9,10 +9,9 @@ public record PostResponses(List postResponse) { public static PostResponses of(Slice posts){ - List responses = posts.stream() - .map(PostResponse::new) - .collect(Collectors.toList()); - return new PostResponses(responses); + return new PostResponses(posts.stream() + .map(PostResponse::new) + .collect(Collectors.toList())); } } diff --git a/src/test/java/com/example/jpaboard/Test1.java b/src/test/java/com/example/jpaboard/Test1.java new file mode 100644 index 000000000..178ee0d42 --- /dev/null +++ b/src/test/java/com/example/jpaboard/Test1.java @@ -0,0 +1,59 @@ +package com.example.jpaboard; + +import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.service.MemberRepository; +import com.example.jpaboard.post.domain.Post; +import com.example.jpaboard.post.domain.PostRepository; +import com.example.jpaboard.post.service.PostService; +import com.example.jpaboard.post.service.dto.PostResponse; +import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.UpdateRequest; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@SpringBootTest +@Transactional +public class Test1 { + @Autowired + PostService postService; + @Autowired + PostRepository postRepository; + @Autowired + MemberRepository memberRepository; + + @BeforeEach + void setup() {} + + @Test + void findAllBy_pageable_PostResponses() { + } + +// @Test +// @DisplayName("post 저장 후 title과 memberName을 확인한다.") +// void savePost() { +// //given +// Member member = new Member("james", new Age(20), "기타치기"); +// memberRepository.save(member); +// +// SaveRequest saveRequest = new SaveRequest(member.getId(), "산책의 정석", "산책의 정석 내용"); +// +// //when +// PostResponse postResponse = postService.savePost(saveRequest); +// +// //then +// String savedTitle = postResponse.title(); +// String savedMemberName = postResponse.memberName(); +// assertThat(savedTitle).isEqualTo("산책의 정석"); +// assertThat(savedMemberName).isEqualTo("박세영"); +// } + +} diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java index e49a9fbb7..8fba7ef02 100644 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -1,28 +1,32 @@ package com.example.jpaboard.post.service; -/* + import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.service.MemberRepository; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; +import com.example.jpaboard.post.exception.EntityNotFoundException; +import com.example.jpaboard.post.exception.PermissionDeniedEditException; +import com.example.jpaboard.post.service.dto.FindAllRequest; import com.example.jpaboard.post.service.dto.PostResponse; import com.example.jpaboard.post.service.dto.SaveRequest; import com.example.jpaboard.post.service.dto.UpdateRequest; -import org.assertj.core.api.Assertions; 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.context.SpringBootTest; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import java.util.Random; + import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.catchException; + -/* @ActiveProfiles("test") @SpringBootTest @Transactional @@ -35,39 +39,37 @@ class PostServiceTest { @Autowired MemberRepository memberRepository; - Long setupId1; - Long setupId2; - Long setupId3; - Long setupPostId2; - @BeforeEach - void setup() { - Member member1 = new Member("김별", new Age(27), "락 부르기"); - Member member2 = new Member("윤영운", new Age(27), "저글링 돌리기"); - Member member3 = new Member("박세영", new Age(27), "산책"); + Long setupPostId1; - Post post1 = new Post("별의 포스트 제목", "별의 포스트 내용", member1); - Post post2 = new Post("영운의 포스트 제목", "영운의 포스트 내용", member2); - Long memberId = member3.getId(); + Long setupPostId2; + Long setupMemberId2; - postRepository.save(post1); - postRepository.save(post2); - memberRepository.save(member3); + Long setupMemberId3; - setupId1 = post1.getId(); - setupId2 = post2.getId(); - setupId3 = member3.getId(); + @BeforeEach + void setup() { + setupData(); } @Test + @DisplayName("setup에서 저장한 post를 필터 없이 10개 조회하여 개수를 확인한다.") void findAllBy_pageable_PostResponses() { + //given + FindAllRequest findAllRequest = new FindAllRequest("", ""); + + //when + Slice findPosts = postService.findPostAllByFilter(findAllRequest, PageRequest.of(0, 10)); + + //then + assertThat(findPosts.getContent().size()).isEqualTo(2); } @Test @DisplayName("setup에서 저장한 post를 조회하여 title과 memberName을 확인한다.") void findById_correctPostId_PostResponse() { //when - PostResponse response = postService.findById(setupId1); + PostResponse response = postService.findById(setupPostId1); //then String findTitle = response.title(); @@ -76,11 +78,21 @@ void findById_correctPostId_PostResponse() { assertThat(findMemberName).isEqualTo("김별"); } + @Test + @DisplayName("존재하지 않는 postId를 통해 post를 조회할 때 EntityNotFoundException이 발생하는지 확인한다.") + void findById_inCorrectPostId_EntityNotFoundException() { + //when + Exception exception = catchException(() -> postService.findById(setupMemberId3)); + + //then + assertThat(exception).isInstanceOf(EntityNotFoundException.class); + } + @Test @DisplayName("post 저장 후 title과 memberName을 확인한다.") - void savePost() { + void savePost_correctSaveRequest_postResponse() { //given - SaveRequest saveRequest = new SaveRequest(setupId3, "산책의 정석", "산책의 정석 내용"); + SaveRequest saveRequest = new SaveRequest(setupMemberId3, "산책의 정석", "산책의 정석 내용"); //when PostResponse postResponse = postService.savePost(saveRequest); @@ -94,9 +106,9 @@ void savePost() { @Test @DisplayName("setUp에서 저장한 post를 update후 title과 content를 확인한다.") - void updatePost() { + void updatePost_IdAndUpdateRequest_PostResponse() { //given - UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupId2); + UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); //when PostResponse postResponse = postService.updatePost(setupPostId2, updateRequest); @@ -107,6 +119,52 @@ void updatePost() { assertThat(updatedTitle).isEqualTo("영운의 변경된 postTitle"); assertThat(updatedContent).isEqualTo("영운의 변경된 content"); } -} - */ + @Test + @DisplayName("setUp에서 저장한 post를 올바르지 않은 memberId로 수정할 때 PermissionDeniedEditException이 발생하는지 확인한다.") + void updatePost_IdAndIncorrectUpdateRequest_PermissionDeniedEditException() { + //given + UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId3); + + //when + Exception exception = catchException(() -> postService.updatePost(setupPostId2, updateRequest)); + + //then + assertThat(exception).isInstanceOf(PermissionDeniedEditException.class); + } + + @Test + @DisplayName("존재하지 않는 postId로 post를 수정할 때 EntityNotFoundException이 발생하는지 확인한다.") + void updatePost_incorrectIdAndUpdateRequest_EntityNotFoundException() { + //given + UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); + Random random = new Random(); + + //when + Exception exception = catchException(() -> postService.updatePost(random.nextLong(), updateRequest)); + + //then + assertThat(exception).isInstanceOf(EntityNotFoundException.class); + } + + private void setupData() { + Member member1 = new Member("김별", new Age(27), "락 부르기"); + Member member2 = new Member("윤영운", new Age(27), "저글링 돌리기"); + Member member3 = new Member("박세영", new Age(27), "산책"); + + memberRepository.save(member1); + memberRepository.save(member2); + memberRepository.save(member3); + + Post post1 = new Post("별의 포스트 제목", "별의 포스트 내용", member1); + Post post2 = new Post("영운의 포스트 제목", "영운의 포스트 내용", member2); + + postRepository.save(post1); + postRepository.save(post2); + + setupPostId1 = post1.getId(); + setupPostId2 = post2.getId(); + setupMemberId2 = member2.getId(); + setupMemberId3 = member3.getId(); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application-test.yml similarity index 100% rename from src/test/resources/application.yml rename to src/test/resources/application-test.yml From f3f1e86409c651bdfd9dfb709a15fc51988a0b8c Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 5 Aug 2023 20:25:47 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat=20:=20BaseEntity=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20Custom=20AuditorAware=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BOOT-INF/classes/static/docs/index.html | 386 +++++++++++++++++- build.gradle | 5 +- src/docs/asciidoc/index.adoc | 42 ++ .../example/jpaboard/JpaboardApplication.java | 5 + .../jpaboard/config/AuditorAwareConfig.java | 39 +- .../example/jpaboard/global/BaseEntity.java | 6 +- .../example/jpaboard/global/CreatedBy.java | 16 - .../jpaboard/member/domain/Member.java | 3 +- .../example/jpaboard/post/domain/Post.java | 5 +- src/main/resources/application-test.yml | 23 ++ src/main/resources/application.yml | 2 +- src/main/resources/static/docs/index.html | 386 +++++++++++++++++- .../post/controller/PostControllerTest.java | 99 ++++- .../post/service/PostServiceTest.java | 112 ----- 14 files changed, 940 insertions(+), 189 deletions(-) delete mode 100644 src/main/java/com/example/jpaboard/global/CreatedBy.java create mode 100644 src/main/resources/application-test.yml delete mode 100644 src/test/java/com/example/jpaboard/post/service/PostServiceTest.java diff --git a/BOOT-INF/classes/static/docs/index.html b/BOOT-INF/classes/static/docs/index.html index 84c925dd9..0110dcbe4 100644 --- a/BOOT-INF/classes/static/docs/index.html +++ b/BOOT-INF/classes/static/docs/index.html @@ -450,6 +450,12 @@

API Docs

@@ -465,16 +471,382 @@

/posts

-
+
Request
-

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/curl-request.adoc[] -Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-request.adoc[]

+
+
$ curl 'http://localhost:8080/posts?title=Your+Title&content=Your+Content' -i -X GET
-
+
+
+
+
GET /posts?title=Your+Title&content=Your+Content HTTP/1.1
+Host: localhost:8080
+
+
+
Response
-

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-response.adoc[] -Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/response-fields.adoc[]

+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 1124
+
+{"result":{"content":[{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},{"postId":2,"title":"흑구영수팀 화이팅","content":"흑구영수팀 언제나 응원해","memberName":"\"김별\""},{"postId":3,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":4,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":5,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":6,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":7,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""}],"pageable":{"sort":{"empty":true,"unsorted":true,"sorted":false},"offset":0,"pageNumber":0,"pageSize":20,"paged":true,"unpaged":false},"size":20,"number":0,"sort":{"empty":true,"unsorted":true,"sorted":false},"first":true,"last":true,"numberOfElements":7,"empty":false},"resultCode":200,"resultMsg":"SELECT SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result

Object

Result details

result.content[]

Array

The list of content items

result.pageable

Object

Pagination information

result.pageable.sort.empty

Boolean

Indicates if the sort is empty

result.pageable.sort.sorted

Boolean

Indicates if the sort is sorted

result.pageable.sort.unsorted

Boolean

Indicates if the sort is unsorted

result.pageable.offset

Number

Offset value for pagination

result.pageable.pageSize

Number

Page size for pagination

result.pageable.pageNumber

Number

Page number

result.pageable.unpaged

Boolean

Indicates if the page is unpaged

result.pageable.paged

Boolean

Indicates if the page is paged

result.size

Number

The page size

result.number

Number

The current page number

result.sort.empty

Boolean

Indicates if the sort is empty

result.sort.sorted

Boolean

Indicates if the sort is sorted

result.sort.unsorted

Boolean

Indicates if the sort is unsorted

result.first

Boolean

Indicates if this is the first page

result.last

Boolean

Indicates if this is the last page

result.numberOfElements

Number

Number of elements in the current page

result.empty

Boolean

Indicates if the content is empty

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts/{id}

+
+
Request
+
+
$ curl 'http://localhost:8080/posts/1' -i -X PATCH \
+    -H 'Content-Type: application/json;charset=UTF-8' \
+    -d '{"title": "흑구팀 화이팅팅", "content": "언제나 응원해 흑구팀팀", "memberId": 1}'
+
+
+
+
+
PATCH /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 97
+Host: localhost:8080
+
+{"title": "흑구팀 화이팅팅", "content": "언제나 응원해 흑구팀팀", "memberId": 1}
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 173
+
+{"result":{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},"resultCode":204,"resultMsg":"UPDATE SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result.postId

Number

The id of the updated post

result.title

String

The title of the updated post

result.content

String

The content of the updated post

result.memberName

String

The name of the member who made the update

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts/{id}

+
+
Request
+
+
$ curl 'http://localhost:8080/posts/1' -i -X GET
+
+
+
+
+
GET /posts/1 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 173
+
+{"result":{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},"resultCode":200,"resultMsg":"SELECT SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result.postId

Number

The id of the detailed post

result.title

String

The title of the detailed post

result.content

String

The content of the detailed post

result.memberName

String

The name of writer

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts

+
+
Request
+
+
$ curl 'http://localhost:8080/posts' -i -X POST \
+    -H 'Content-Type: application/json;charset=UTF-8' \
+    -d '{"memberId":1,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀"}'
+
+
+
+
+
POST /posts HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 84
+Host: localhost:8080
+
+{"memberId":1,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀"}
+
+
+
+
Response
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/posts/7
+Content-Type: application/json;charset=UTF-8
+Content-Length: 108
+
+{"postId":7,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""}
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postId

Number

The id of the saved post

title

String

The title of the saved post

content

String

The content of the saved post

memberName

String

The name of writer

@@ -482,7 +854,7 @@

/posts

diff --git a/build.gradle b/build.gradle index da4e39526..ea57f0d95 100644 --- a/build.gradle +++ b/build.gradle @@ -39,8 +39,10 @@ dependencies { runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'mysql:mysql-connector-java:8.0.28' + } tasks.named('test') { @@ -55,6 +57,7 @@ test { asciidoctor { dependsOn test inputs.dir snippetsDir + attributes 'snippets': snippetsDir } asciidoctor.doFirst { diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index d3f9261b9..aabec328b 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -22,3 +22,45 @@ include::{snippets}/post-findAllBy/http-request.adoc[] include::{snippets}/post-findAllBy/http-response.adoc[] include::{snippets}/post-findAllBy/response-fields.adoc[] + +=== 게시판 업데이트 + +=== /posts/{id} + +.Request + +include::{snippets}/post-update/curl-request.adoc[] +include::{snippets}/post-update/http-request.adoc[] + +.Response + +include::{snippets}/post-update/http-response.adoc[] +include::{snippets}/post-update/response-fields.adoc[] + +=== 게시판 조회 + +=== /posts/{id} + +.Request + +include::{snippets}/post-findById/curl-request.adoc[] +include::{snippets}/post-findById/http-request.adoc[] + +.Response + +include::{snippets}/post-findById/http-response.adoc[] +include::{snippets}/post-findById/response-fields.adoc[] + +=== 게시글 등록 + +=== /posts + +.Request + +include::{snippets}/post-save/curl-request.adoc[] +include::{snippets}/post-save/http-request.adoc[] + +.Response + +include::{snippets}/post-save/http-response.adoc[] +include::{snippets}/post-save/response-fields.adoc[] diff --git a/src/main/java/com/example/jpaboard/JpaboardApplication.java b/src/main/java/com/example/jpaboard/JpaboardApplication.java index b9ea80234..1bef38ef3 100644 --- a/src/main/java/com/example/jpaboard/JpaboardApplication.java +++ b/src/main/java/com/example/jpaboard/JpaboardApplication.java @@ -2,8 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing( + auditorAwareRef = "auditorAwareConfig", + dateTimeProviderRef = "auditorAwareConfig" +) public class JpaboardApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java index f16b033a9..b9b41cf9b 100644 --- a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java +++ b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java @@ -1,39 +1,26 @@ package com.example.jpaboard.config; -import com.example.jpaboard.member.domain.Member; -import jakarta.annotation.Resource; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAccessor; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.stereotype.Component; + -/* @EnableJpaAuditing @Configuration -public class AuditorAwareConfig { - - private final Member memberInfo; - - @Autowired - public AuditorAwareConfig(Member memberInfo) { - this.memberInfo = memberInfo; +@Component +public class AuditorAwareConfig implements AuditorAware, DateTimeProvider { + @Override + public Optional getCurrentAuditor() { + return Optional.of("별앤영"); } - - @Bean - public AuditorAware auditorAware() { - return new AuditorAware<>() { - - @Override - public Optional getCurrentAuditor() { - Long userId = memberInfo.getId(); - return Optional.of(userId); - } - - }; + @Override + public Optional getNow() { + return Optional.of(LocalDateTime.now()); } - } - */ diff --git a/src/main/java/com/example/jpaboard/global/BaseEntity.java b/src/main/java/com/example/jpaboard/global/BaseEntity.java index 980338c03..8dbeaed4a 100644 --- a/src/main/java/com/example/jpaboard/global/BaseEntity.java +++ b/src/main/java/com/example/jpaboard/global/BaseEntity.java @@ -10,7 +10,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.format.annotation.DateTimeFormat; -/* @Getter @MappedSuperclass @@ -23,10 +22,7 @@ public class BaseEntity { private LocalDateTime createdAt; @CreatedBy - @DateTimeFormat(pattern = "yyyy-MM-dd/HH:mm:ss") @Column(name = "created_by") - private Long createdBy; + private String createdBy; } - - */ diff --git a/src/main/java/com/example/jpaboard/global/CreatedBy.java b/src/main/java/com/example/jpaboard/global/CreatedBy.java deleted file mode 100644 index a6e745a7e..000000000 --- a/src/main/java/com/example/jpaboard/global/CreatedBy.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.jpaboard.global; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/* -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface CreatedBy { - - UUID mention() default "안녕하세용 ㅎ"; -} - - */ diff --git a/src/main/java/com/example/jpaboard/member/domain/Member.java b/src/main/java/com/example/jpaboard/member/domain/Member.java index 0fdb6b511..f980d5994 100644 --- a/src/main/java/com/example/jpaboard/member/domain/Member.java +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -1,5 +1,6 @@ package com.example.jpaboard.member.domain; +import com.example.jpaboard.global.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -10,7 +11,7 @@ @Entity @Table(name = "members") -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index 376abf16a..d45e91afd 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -1,11 +1,12 @@ package com.example.jpaboard.post.domain; +import com.example.jpaboard.global.BaseEntity; import com.example.jpaboard.member.domain.Member; import jakarta.persistence.*; @Entity @Table(name = "posts") -public class Post { +public class Post extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -17,7 +18,7 @@ public class Post { @Column(nullable = false) private String content; - @ManyToOne(cascade = CascadeType.PERSIST) + @ManyToOne private Member member; protected Post() { } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 000000000..6a35a0c73 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,23 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost/board?serverTimezone=UTC&characterEncoding=UTF-8 + username: root + password: 7351 + + jpa: + open-in-view: false + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate.format_sql: true + main: + allow-bean-definition-overriding : true + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 19ff75f1c..6a07fc24f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,7 +8,7 @@ spring: jpa: open-in-view: false hibernate: - ddl-auto: create + ddl-auto: update show-sql: true properties: hibernate.format_sql: true diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 84c925dd9..0110dcbe4 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -450,6 +450,12 @@

API Docs

@@ -465,16 +471,382 @@

/posts

-
+
Request
-

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/curl-request.adoc[] -Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-request.adoc[]

+
+
$ curl 'http://localhost:8080/posts?title=Your+Title&content=Your+Content' -i -X GET
-
+
+
+
+
GET /posts?title=Your+Title&content=Your+Content HTTP/1.1
+Host: localhost:8080
+
+
+
Response
-

Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/http-response.adoc[] -Unresolved directive in index.adoc - include::{snippets}/post-findAllBy/response-fields.adoc[]

+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 1124
+
+{"result":{"content":[{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},{"postId":2,"title":"흑구영수팀 화이팅","content":"흑구영수팀 언제나 응원해","memberName":"\"김별\""},{"postId":3,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":4,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":5,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":6,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""},{"postId":7,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""}],"pageable":{"sort":{"empty":true,"unsorted":true,"sorted":false},"offset":0,"pageNumber":0,"pageSize":20,"paged":true,"unpaged":false},"size":20,"number":0,"sort":{"empty":true,"unsorted":true,"sorted":false},"first":true,"last":true,"numberOfElements":7,"empty":false},"resultCode":200,"resultMsg":"SELECT SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result

Object

Result details

result.content[]

Array

The list of content items

result.pageable

Object

Pagination information

result.pageable.sort.empty

Boolean

Indicates if the sort is empty

result.pageable.sort.sorted

Boolean

Indicates if the sort is sorted

result.pageable.sort.unsorted

Boolean

Indicates if the sort is unsorted

result.pageable.offset

Number

Offset value for pagination

result.pageable.pageSize

Number

Page size for pagination

result.pageable.pageNumber

Number

Page number

result.pageable.unpaged

Boolean

Indicates if the page is unpaged

result.pageable.paged

Boolean

Indicates if the page is paged

result.size

Number

The page size

result.number

Number

The current page number

result.sort.empty

Boolean

Indicates if the sort is empty

result.sort.sorted

Boolean

Indicates if the sort is sorted

result.sort.unsorted

Boolean

Indicates if the sort is unsorted

result.first

Boolean

Indicates if this is the first page

result.last

Boolean

Indicates if this is the last page

result.numberOfElements

Number

Number of elements in the current page

result.empty

Boolean

Indicates if the content is empty

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts/{id}

+
+
Request
+
+
$ curl 'http://localhost:8080/posts/1' -i -X PATCH \
+    -H 'Content-Type: application/json;charset=UTF-8' \
+    -d '{"title": "흑구팀 화이팅팅", "content": "언제나 응원해 흑구팀팀", "memberId": 1}'
+
+
+
+
+
PATCH /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 97
+Host: localhost:8080
+
+{"title": "흑구팀 화이팅팅", "content": "언제나 응원해 흑구팀팀", "memberId": 1}
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 173
+
+{"result":{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},"resultCode":204,"resultMsg":"UPDATE SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result.postId

Number

The id of the updated post

result.title

String

The title of the updated post

result.content

String

The content of the updated post

result.memberName

String

The name of the member who made the update

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts/{id}

+
+
Request
+
+
$ curl 'http://localhost:8080/posts/1' -i -X GET
+
+
+
+
+
GET /posts/1 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Content-Length: 173
+
+{"result":{"postId":1,"title":"흑구팀 화이팅팅","content":"언제나 응원해 흑구팀팀","memberName":"\"김별\""},"resultCode":200,"resultMsg":"SELECT SUCCESS"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

result.postId

Number

The id of the detailed post

result.title

String

The title of the detailed post

result.content

String

The content of the detailed post

result.memberName

String

The name of writer

resultCode

Number

The result code of the response

resultMsg

String

The result message of the response

+
+ +
+

/posts

+
+
Request
+
+
$ curl 'http://localhost:8080/posts' -i -X POST \
+    -H 'Content-Type: application/json;charset=UTF-8' \
+    -d '{"memberId":1,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀"}'
+
+
+
+
+
POST /posts HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 84
+Host: localhost:8080
+
+{"memberId":1,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀"}
+
+
+
+
Response
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/posts/7
+Content-Type: application/json;charset=UTF-8
+Content-Length: 108
+
+{"postId":7,"title":"흑구영수팀 화이팅","content":"흑구영수팀팀팀","memberName":"\"김별\""}
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postId

Number

The id of the saved post

title

String

The title of the saved post

content

String

The content of the saved post

memberName

String

The name of writer

@@ -482,7 +854,7 @@

/posts

diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java index 715752b6a..1726d9b08 100644 --- a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -2,12 +2,16 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.post.controller.dto.SaveApiRequest; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.service.dto.FindAllRequest; import com.example.jpaboard.post.service.dto.PostResponse; +import java.io.IOException; import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; @@ -19,17 +23,20 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +@ActiveProfiles("test") @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @@ -38,13 +45,12 @@ class PostControllerTest { @Autowired MockMvc mockMvc; - + @Autowired + private ObjectMapper objectMapper; @Test void findAllBy() throws Exception { - - // API 호출 및 응답 문서화 this.mockMvc.perform(get("/posts") .param("title", "Your Title") .param("content", "Your Content")) @@ -76,20 +82,91 @@ void findAllBy() throws Exception { fieldWithPath("resultMsg").description("The result message of the response") ) )); - - } - @Test - void updatePost() { + public void updatePost() throws Exception { + // Request 바디 필드 설명을 작성합니다. + FieldDescriptor[] requestFields = new FieldDescriptor[] { + fieldWithPath("title").description("Updated title of the post"), + fieldWithPath("content").description("Updated content of the post"), + fieldWithPath("memberId").description("ID of the member making the update") + }; + + // Response 바디 필드 설명을 작성합니다. + FieldDescriptor[] responseFields = new FieldDescriptor[] { + fieldWithPath("result.postId").description("The id of the updated post"), + fieldWithPath("result.title").description("The title of the updated post"), + fieldWithPath("result.content").description("The content of the updated post"), + fieldWithPath("result.memberName").description("The name of the member who made the update"), + fieldWithPath("resultCode").description("The result code of the response"), + fieldWithPath("resultMsg").description("The result message of the response") + }; + + // API 호출 및 문서 생성 + this.mockMvc.perform( + patch("/posts/{id}", 1L) // 실제 ID 값으로 대체해주세요 + .content("{\"title\": \"흑구팀 화이팅팅\", \"content\": \"언제나 응원해 흑구팀팀\", \"memberId\": 1}") // 업데이트할 내용 전달 + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo(document("post-update", + requestFields(requestFields), + responseFields(responseFields) + )); } @Test - void findById() { + void findById() throws Exception { + + // Response 바디 필드 설명을 작성합니다. + FieldDescriptor[] responseFields = new FieldDescriptor[] { + fieldWithPath("result.postId").description("The id of the detailed post"), + fieldWithPath("result.title").description("The title of the detailed post"), + fieldWithPath("result.content").description("The content of the detailed post"), + fieldWithPath("result.memberName").description("The name of writer"), + fieldWithPath("resultCode").description("The result code of the response"), + fieldWithPath("resultMsg").description("The result message of the response") + }; + + // API 호출 및 문서 생성 + this.mockMvc.perform( + get("/posts/{id}", 1L) // 실제 ID 값으로 대체해주세요 + ) + .andExpect(status().isOk()) + .andDo(document("post-findById", + responseFields(responseFields) + )); } @Test - void savePost() { + void savePost() throws Exception { + // Request 바디 필드 설명을 작성합니다. + FieldDescriptor[] requestFields = new FieldDescriptor[] { + fieldWithPath("title").description("Saved title of the post"), + fieldWithPath("content").description("Saved content of the post"), + fieldWithPath("memberId").description("ID of the member making the post") + }; + + // Response 바디 필드 설명을 작성합니다. + FieldDescriptor[] responseFields = new FieldDescriptor[] { + fieldWithPath("postId").description("The id of the saved post"), + fieldWithPath("title").description("The title of the saved post"), + fieldWithPath("content").description("The content of the saved post"), + fieldWithPath("memberName").description("The name of writer"), + }; + + SaveApiRequest saveApiRequest = new SaveApiRequest(1L,"흑구영수팀 화이팅","흑구영수팀팀팀"); + + this.mockMvc.perform( + post("/posts") + .content(objectMapper.writeValueAsString(saveApiRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(document("post-save", + requestFields(requestFields), + responseFields(responseFields) + )); + } } diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java deleted file mode 100644 index e49a9fbb7..000000000 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.example.jpaboard.post.service; -/* -import com.example.jpaboard.member.domain.Age; -import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.MemberRepository; -import com.example.jpaboard.post.domain.Post; -import com.example.jpaboard.post.domain.PostRepository; -import com.example.jpaboard.post.service.dto.PostResponse; -import com.example.jpaboard.post.service.dto.SaveRequest; -import com.example.jpaboard.post.service.dto.UpdateRequest; -import org.assertj.core.api.Assertions; -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.context.SpringBootTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -/* -@ActiveProfiles("test") -@SpringBootTest -@Transactional -class PostServiceTest { - - @Autowired - PostService postService; - @Autowired - PostRepository postRepository; - @Autowired - MemberRepository memberRepository; - - Long setupId1; - Long setupId2; - Long setupId3; - Long setupPostId2; - - @BeforeEach - void setup() { - Member member1 = new Member("김별", new Age(27), "락 부르기"); - Member member2 = new Member("윤영운", new Age(27), "저글링 돌리기"); - Member member3 = new Member("박세영", new Age(27), "산책"); - - Post post1 = new Post("별의 포스트 제목", "별의 포스트 내용", member1); - Post post2 = new Post("영운의 포스트 제목", "영운의 포스트 내용", member2); - Long memberId = member3.getId(); - - postRepository.save(post1); - postRepository.save(post2); - memberRepository.save(member3); - - setupId1 = post1.getId(); - setupId2 = post2.getId(); - setupId3 = member3.getId(); - } - - @Test - void findAllBy_pageable_PostResponses() { - } - - @Test - @DisplayName("setup에서 저장한 post를 조회하여 title과 memberName을 확인한다.") - void findById_correctPostId_PostResponse() { - //when - PostResponse response = postService.findById(setupId1); - - //then - String findTitle = response.title(); - String findMemberName = response.memberName(); - assertThat(findTitle).isEqualTo("별의 포스트 제목"); - assertThat(findMemberName).isEqualTo("김별"); - } - - @Test - @DisplayName("post 저장 후 title과 memberName을 확인한다.") - void savePost() { - //given - SaveRequest saveRequest = new SaveRequest(setupId3, "산책의 정석", "산책의 정석 내용"); - - //when - PostResponse postResponse = postService.savePost(saveRequest); - - //then - String savedTitle = postResponse.title(); - String savedMemberName = postResponse.memberName(); - assertThat(savedTitle).isEqualTo("산책의 정석"); - assertThat(savedMemberName).isEqualTo("박세영"); - } - - @Test - @DisplayName("setUp에서 저장한 post를 update후 title과 content를 확인한다.") - void updatePost() { - //given - UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupId2); - - //when - PostResponse postResponse = postService.updatePost(setupPostId2, updateRequest); - - //then - String updatedTitle = postResponse.title(); - String updatedContent = postResponse.content(); - assertThat(updatedTitle).isEqualTo("영운의 변경된 postTitle"); - assertThat(updatedContent).isEqualTo("영운의 변경된 content"); - } -} - - */ From 9aeab27d65c82a00ed61934e35a2249bdc4ca8ce Mon Sep 17 00:00:00 2001 From: young970 Date: Sun, 6 Aug 2023 00:35:28 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20findPostAllByFilter=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20join=EC=9D=84=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/jpaboard/post/domain/Post.java | 2 +- .../jpaboard/post/domain/PostRepository.java | 1 + src/main/resources/application-test.yml | 23 ------------------- src/main/resources/application.yml | 8 +++---- .../post/controller/PostControllerTest.java | 14 ----------- src/test/resources/application-test.yml | 10 ++++---- 6 files changed, 11 insertions(+), 47 deletions(-) delete mode 100644 src/main/resources/application-test.yml diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index d45e91afd..1eef274bd 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -18,7 +18,7 @@ public class Post extends BaseEntity { @Column(nullable = false) private String content; - @ManyToOne + @ManyToOne(optional = false) private Member member; protected Post() { } diff --git a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java index f1d19454d..810ce1182 100644 --- a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java +++ b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java @@ -9,6 +9,7 @@ public interface PostRepository extends JpaRepository { @Query("SELECT p FROM Post p " + + "JOIN FETCH p.member " + // Add the JOIN FETCH clause to fetch the associated member "WHERE (p.title LIKE %:title%) "+ "AND (p.content LIKE %:content%)") Slice findPostAllByFilter(@Param("title") String title, diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml deleted file mode 100644 index 6a35a0c73..000000000 --- a/src/main/resources/application-test.yml +++ /dev/null @@ -1,23 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost/board?serverTimezone=UTC&characterEncoding=UTF-8 - username: root - password: 7351 - - jpa: - open-in-view: false - hibernate: - ddl-auto: update - show-sql: true - properties: - hibernate.format_sql: true - main: - allow-bean-definition-overriding : true - -server: - servlet: - encoding: - charset: UTF-8 - enabled: true - force: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6a07fc24f..b7c3ea731 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,9 @@ spring: datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE - username: sa - password: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/board?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 12345! jpa: open-in-view: false diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java index 1726d9b08..89f20187b 100644 --- a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -1,16 +1,8 @@ package com.example.jpaboard.post.controller; -import com.example.jpaboard.member.domain.Age; -import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.post.controller.dto.SaveApiRequest; -import com.example.jpaboard.post.domain.Post; -import com.example.jpaboard.post.service.dto.FindAllRequest; -import com.example.jpaboard.post.service.dto.PostResponse; -import java.io.IOException; -import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,14 +10,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; - import org.springframework.http.MediaType; import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 7b30603e9..b7c3ea731 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,14 +1,14 @@ spring: datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:tcp://localhost/~/test;DB_CLOSE_ON_EXIT=FALSE - username: sa - password: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/board?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 12345! jpa: open-in-view: false hibernate: - ddl-auto: create-drop + ddl-auto: update show-sql: true properties: hibernate.format_sql: true From 26232b4eb432386b1169c90b8701d54e61967152 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sun, 6 Aug 2023 21:36:12 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20member=EC=99=80=20post=20?= =?UTF-8?q?=EC=A1=B0=EC=9D=B8=20=EC=BF=BC=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/jpaboard/post/domain/PostRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java index f1d19454d..a05dae180 100644 --- a/src/main/java/com/example/jpaboard/post/domain/PostRepository.java +++ b/src/main/java/com/example/jpaboard/post/domain/PostRepository.java @@ -9,6 +9,8 @@ public interface PostRepository extends JpaRepository { @Query("SELECT p FROM Post p " + + "INNER JOIN Member m " + + "ON p.id = m.id "+ "WHERE (p.title LIKE %:title%) "+ "AND (p.content LIKE %:content%)") Slice findPostAllByFilter(@Param("title") String title, From fbb474482e75d565eb5fd8fcdd3acbbdb48b447a Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Mon, 7 Aug 2023 00:28:41 +0900 Subject: [PATCH 11/14] =?UTF-8?q?style=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/jpaboard/config/AuditorAwareConfig.java | 1 - src/main/java/com/example/jpaboard/global/BaseEntity.java | 1 - .../java/com/example/jpaboard/global/ErrorResponse.java | 5 ++++- .../jpaboard/global/GlobalRestExceptionHandler.java | 3 +++ .../java/com/example/jpaboard/member/domain/Member.java | 1 + .../example/jpaboard/member/service/MemberService.java | 1 + .../jpaboard/member/service/mapper/MemberMapper.java | 1 + .../example/jpaboard/post/controller/PostController.java | 2 ++ .../jpaboard/post/controller/mapper/PostApiMapper.java | 1 + src/main/java/com/example/jpaboard/post/domain/Post.java | 1 + .../com/example/jpaboard/post/service/PostService.java | 8 ++++++-- .../example/jpaboard/post/service/mapper/PostMapper.java | 1 + .../jpaboard/member/service/MemberServiceTest.java | 2 ++ .../jpaboard/post/controller/PostControllerTest.java | 5 +---- .../example/jpaboard/post/service/PostServiceTest.java | 2 ++ 15 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java index 201374a50..cff39341e 100644 --- a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java +++ b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java @@ -10,7 +10,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.stereotype.Component; - @EnableJpaAuditing @Configuration @Component diff --git a/src/main/java/com/example/jpaboard/global/BaseEntity.java b/src/main/java/com/example/jpaboard/global/BaseEntity.java index e139cd109..283660d7b 100644 --- a/src/main/java/com/example/jpaboard/global/BaseEntity.java +++ b/src/main/java/com/example/jpaboard/global/BaseEntity.java @@ -11,7 +11,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.format.annotation.DateTimeFormat; - @MappedSuperclass @EntityListeners(value = {AuditingEntityListener.class}) public class BaseEntity { diff --git a/src/main/java/com/example/jpaboard/global/ErrorResponse.java b/src/main/java/com/example/jpaboard/global/ErrorResponse.java index 195da76df..929cd3055 100644 --- a/src/main/java/com/example/jpaboard/global/ErrorResponse.java +++ b/src/main/java/com/example/jpaboard/global/ErrorResponse.java @@ -2,7 +2,10 @@ import java.time.LocalDateTime; -public record ErrorResponse(int statusCode, String detail, String instance, String time) { +public record ErrorResponse(int statusCode, + String detail, + String instance, + String time) { public ErrorResponse(int statusCode, String detail, String instance) { this(statusCode, detail, instance, LocalDateTime.now().toString()); diff --git a/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java index b05269f35..a8aa69bfc 100644 --- a/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java +++ b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java @@ -2,9 +2,12 @@ import com.example.jpaboard.global.exception.EntityNotFoundException; import com.example.jpaboard.global.exception.PermissionDeniedEditException; + import jakarta.servlet.http.HttpServletRequest; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; diff --git a/src/main/java/com/example/jpaboard/member/domain/Member.java b/src/main/java/com/example/jpaboard/member/domain/Member.java index 96f2dbadd..89a829f21 100644 --- a/src/main/java/com/example/jpaboard/member/domain/Member.java +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -1,6 +1,7 @@ package com.example.jpaboard.member.domain; import com.example.jpaboard.global.BaseEntity; + import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/example/jpaboard/member/service/MemberService.java b/src/main/java/com/example/jpaboard/member/service/MemberService.java index f6d361004..0e706e5c7 100644 --- a/src/main/java/com/example/jpaboard/member/service/MemberService.java +++ b/src/main/java/com/example/jpaboard/member/service/MemberService.java @@ -4,6 +4,7 @@ import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.member.service.mapper.MemberMapper; + import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java index e09459dba..f943186b5 100644 --- a/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java +++ b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java @@ -3,6 +3,7 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.service.dto.CreateMemberRequest; + import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index 28c6df0b6..63bfe7d53 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -9,7 +9,9 @@ import com.example.jpaboard.post.controller.mapper.PostApiMapper; import com.example.jpaboard.post.service.PostService; import com.example.jpaboard.post.service.dto.*; + import jakarta.validation.Valid; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java index db443f60d..b21d5d3db 100644 --- a/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java +++ b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java @@ -6,6 +6,7 @@ import com.example.jpaboard.post.service.dto.FindAllRequest; import com.example.jpaboard.post.service.dto.SaveRequest; import com.example.jpaboard.post.service.dto.UpdateRequest; + import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index 4d25b361e..fcae48eee 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -2,6 +2,7 @@ import com.example.jpaboard.global.BaseEntity; import com.example.jpaboard.member.domain.Member; + import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index 2af51c043..4eb6da63a 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -4,10 +4,14 @@ import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; +import com.example.jpaboard.post.service.dto.FindAllRequest; +import com.example.jpaboard.post.service.dto.PostResponse; +import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.UpdateRequest; +import com.example.jpaboard.post.service.mapper.PostMapper; import com.example.jpaboard.global.exception.EntityNotFoundException; import com.example.jpaboard.global.exception.PermissionDeniedEditException; -import com.example.jpaboard.post.service.dto.*; -import com.example.jpaboard.post.service.mapper.PostMapper; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java index c5df3dbc3..2aec58a55 100644 --- a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java +++ b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java @@ -4,6 +4,7 @@ import com.example.jpaboard.member.service.dto.FindMemberResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.service.dto.SaveRequest; + import org.springframework.stereotype.Component; @Component diff --git a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java index 3eafe2563..7d47f8bdd 100644 --- a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java +++ b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java @@ -3,8 +3,10 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.service.dto.FindMemberResponse; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java index f7f067f44..a330edf77 100644 --- a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -2,18 +2,15 @@ import com.example.jpaboard.post.controller.dto.SaveApiRequest; - import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; - import org.springframework.http.MediaType; import org.springframework.restdocs.payload.FieldDescriptor; - -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java index 5ebed0370..233eaa160 100644 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -11,9 +11,11 @@ import com.example.jpaboard.post.service.dto.PostResponse; import com.example.jpaboard.post.service.dto.SaveRequest; import com.example.jpaboard.post.service.dto.UpdateRequest; + 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.context.SpringBootTest; import org.springframework.data.domain.PageRequest; From bd3b4e4d31f9e961f288a5593d9e836a717146eb Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Mon, 7 Aug 2023 00:30:57 +0900 Subject: [PATCH 12/14] =?UTF-8?q?gitignore=20yml=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ src/main/resources/application.yml | 23 ----------------------- src/test/resources/application-test.yml | 23 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 src/main/resources/application.yml delete mode 100644 src/test/resources/application-test.yml diff --git a/.gitignore b/.gitignore index c2065bc26..649136dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +*.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 64f4a1183..000000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,23 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/board?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: root - password: 7351 - - jpa: - open-in-view: false - hibernate: - ddl-auto: update - show-sql: true - properties: - hibernate.format_sql: true - main: - allow-bean-definition-overriding : true - -server: - servlet: - encoding: - charset: UTF-8 - enabled: true - force: true diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml deleted file mode 100644 index 841461612..000000000 --- a/src/test/resources/application-test.yml +++ /dev/null @@ -1,23 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/board-test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: root - password: 7351 - - jpa: - open-in-view: false - hibernate: - ddl-auto: create-drop - show-sql: true - properties: - hibernate.format_sql: true - main: - allow-bean-definition-overriding : true - -server: - servlet: - encoding: - charset: UTF-8 - enabled: true - force: true From 93c70bcb5e58fef0f9a59ecabcaca7f270ffa492 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 12 Aug 2023 20:25:51 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor=20:=20=E8=82=84=EB=B6=BE?= =?UTF-8?q?=EB=B1=B6=E7=94=B1=D1=89=EB=9F=AD=20=E8=AB=9B=EC=84=8F=EC=81=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 37 +++++++++--- .../jpaboard/config/AuditorAwareConfig.java | 2 - .../example/jpaboard/global/BaseEntity.java | 5 +- .../global/GlobalRestExceptionHandler.java | 56 +++++++++++-------- .../PermissionDeniedEditException.java | 9 --- .../exception/UnauthorizedEditException.java | 9 +++ .../example/jpaboard/member/domain/Age.java | 3 +- .../jpaboard/member/domain/Member.java | 14 ++--- .../example/jpaboard/member/domain/Name.java | 33 +++++++++++ .../member/service/MemberService.java | 6 +- ...rRequest.java => MemberCreateRequest.java} | 3 +- ...rResponse.java => MemberFindResponse.java} | 11 ++-- .../member/service/mapper/MemberMapper.java | 11 ++-- .../post/controller/PostController.java | 31 +++++----- ...piRequest.java => PostFindApiRequest.java} | 6 +- .../controller/dto/PostSaveApiRequest.java | 8 +++ .../controller/dto/PostUpdateApiRequest.java | 9 +++ .../post/controller/dto/SaveApiRequest.java | 7 --- .../post/controller/dto/UpdateApiRequest.java | 8 --- .../post/controller/mapper/PostApiMapper.java | 33 +++++------ .../example/jpaboard/post/domain/Post.java | 26 +++++++-- .../jpaboard/post/service/PostService.java | 32 +++++------ .../post/service/dto/FindAllRequest.java | 4 -- .../post/service/dto/PostFindRequest.java | 4 ++ .../post/service/dto/PostResponse.java | 3 +- .../post/service/dto/PostSaveRequest.java | 5 ++ .../post/service/dto/PostUpdateRequest.java | 5 ++ .../post/service/dto/SaveRequest.java | 5 -- .../post/service/dto/UpdateRequest.java | 5 -- .../post/service/mapper/PostMapper.java | 12 ++-- .../com/example/jpaboard/rpc/RpcInput.java | 23 ++++++++ .../member/service/MemberServiceTest.java | 9 +-- .../post/controller/PostControllerTest.java | 16 ++++-- .../post/service/PostServiceTest.java | 51 ++++++++--------- 34 files changed, 305 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/com/example/jpaboard/global/exception/PermissionDeniedEditException.java create mode 100644 src/main/java/com/example/jpaboard/global/exception/UnauthorizedEditException.java create mode 100644 src/main/java/com/example/jpaboard/member/domain/Name.java rename src/main/java/com/example/jpaboard/member/service/dto/{CreateMemberRequest.java => MemberCreateRequest.java} (67%) rename src/main/java/com/example/jpaboard/member/service/dto/{FindMemberResponse.java => MemberFindResponse.java} (70%) rename src/main/java/com/example/jpaboard/post/controller/dto/{FindAllApiRequest.java => PostFindApiRequest.java} (72%) create mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/PostSaveApiRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/PostUpdateApiRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/PostFindRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/PostSaveRequest.java create mode 100644 src/main/java/com/example/jpaboard/post/service/dto/PostUpdateRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java delete mode 100644 src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java create mode 100644 src/main/java/com/example/jpaboard/rpc/RpcInput.java diff --git a/build.gradle b/build.gradle index 4b718ed60..f2806b7ff 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' - id 'org.asciidoctor.jvm.convert' version '3.3.2' + id "org.asciidoctor.jvm.convert" version "3.3.2" } group = 'com.example' @@ -12,12 +12,24 @@ java { sourceCompatibility = '17' } -repositories { - mavenCentral() +configurations { + asciidoctorExt } ext { - set('snippetsDir', file("build/generated-snippets")) + snippetsDir = file('build/generated-snippets') +} + +bootJar { + dependsOn asciidoctor + copy { + from "${asciidoctor.outputDir}" + into 'BOOT-INF/classes/static/docs' + } +} + +repositories { + mavenCentral() } dependencies { @@ -25,7 +37,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -33,12 +47,21 @@ dependencies { } tasks.named('test') { - outputs.dir snippetsDir useJUnitPlatform() } -tasks.named('asciidoctor') { +test { + useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn test inputs.dir snippetsDir attributes 'snippets': snippetsDir - dependsOn test } + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + diff --git a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java index cff39341e..0aa6dca73 100644 --- a/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java +++ b/src/main/java/com/example/jpaboard/config/AuditorAwareConfig.java @@ -8,11 +8,9 @@ import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.stereotype.Component; @EnableJpaAuditing @Configuration -@Component public class AuditorAwareConfig implements AuditorAware, DateTimeProvider { @Override diff --git a/src/main/java/com/example/jpaboard/global/BaseEntity.java b/src/main/java/com/example/jpaboard/global/BaseEntity.java index 283660d7b..c275dbe89 100644 --- a/src/main/java/com/example/jpaboard/global/BaseEntity.java +++ b/src/main/java/com/example/jpaboard/global/BaseEntity.java @@ -9,7 +9,6 @@ import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.springframework.format.annotation.DateTimeFormat; @MappedSuperclass @EntityListeners(value = {AuditingEntityListener.class}) @@ -18,8 +17,7 @@ public class BaseEntity { protected BaseEntity() { } @CreatedDate - @DateTimeFormat(pattern = "yyyy-MM-dd/HH:mm:ss") - @Column(updatable = false) + @Column(updatable = false, columnDefinition = "DATE") private LocalDateTime createdAt; @CreatedBy @@ -32,4 +30,5 @@ public LocalDateTime getCreatedAt() { public String getCreatedBy() { return createdBy; } + } diff --git a/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java index a8aa69bfc..1b334fcb0 100644 --- a/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java +++ b/src/main/java/com/example/jpaboard/global/GlobalRestExceptionHandler.java @@ -1,68 +1,76 @@ package com.example.jpaboard.global; import com.example.jpaboard.global.exception.EntityNotFoundException; -import com.example.jpaboard.global.exception.PermissionDeniedEditException; +import com.example.jpaboard.global.exception.UnauthorizedEditException; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; -import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.servlet.NoHandlerFoundException; + +import static org.springframework.http.HttpStatus.*; @RestControllerAdvice public class GlobalRestExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalRestExceptionHandler.class); - @ExceptionHandler(BindException.class) - public ResponseEntity handleBindException(HttpServletRequest request, BindException e) { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) { StringBuilder resultStringBuilder = getResultStringBuilder(e); - int statusCode = HttpStatus.BAD_REQUEST.value(); - return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, resultStringBuilder.toString(), request.getRequestURI())); + ErrorResponse errorResponse = getErrorResponse(BAD_REQUEST.value(), + resultStringBuilder.toString(), + request.getRequestURI()); + return ResponseEntity + .status(BAD_REQUEST) + .body(errorResponse); } - @ExceptionHandler({IllegalArgumentException.class, MissingRequestHeaderException.class, HttpMessageNotReadableException.class, - HttpClientErrorException.class, NoHandlerFoundException.class}) - public ResponseEntity handleBadRequestException(HttpServletRequest request, Exception e) { - int statusCode = HttpStatus.BAD_REQUEST.value(); + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) { + ErrorResponse errorResponse = getErrorResponse(BAD_REQUEST.value(), + e.getMessage(), + request.getRequestURI()); - return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + return ResponseEntity.status(BAD_REQUEST).body(errorResponse); } - @ExceptionHandler(PermissionDeniedEditException.class) - public ResponseEntity handlePermissionDeniedEditException(HttpServletRequest request, PermissionDeniedEditException e) { - int statusCode = HttpStatus.FORBIDDEN.value(); + @ExceptionHandler(UnauthorizedEditException.class) + public ResponseEntity handleUnauthorizedEditException(HttpServletRequest request, UnauthorizedEditException e) { + ErrorResponse errorResponse = getErrorResponse(FORBIDDEN.value(), + e.getMessage(), + request.getRequestURI()); - return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + return ResponseEntity.status(FORBIDDEN).body(errorResponse); } @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleNotFoundException(HttpServletRequest request, EntityNotFoundException e) { - int statusCode = HttpStatus.NOT_FOUND.value(); + ErrorResponse errorResponse = getErrorResponse(NOT_FOUND.value(), + e.getMessage(), + request.getRequestURI()); - return ResponseEntity.status(statusCode).body(getErrorResponse(statusCode, e.getMessage(), request.getRequestURI())); + return ResponseEntity.status(NOT_FOUND).body(errorResponse); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(HttpServletRequest request, Exception e) { - logger.error("Sever Exception: ", e.getMessage()); + logger.error("Exception URI {}", request.getRequestURI()); + logger.error("Sever Exception", e); - return ResponseEntity.ok().build(); + return ResponseEntity.status(INTERNAL_SERVER_ERROR).build(); } - private static StringBuilder getResultStringBuilder(BindException e) { + private static StringBuilder getResultStringBuilder(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); StringBuilder stringBuilder = new StringBuilder(); diff --git a/src/main/java/com/example/jpaboard/global/exception/PermissionDeniedEditException.java b/src/main/java/com/example/jpaboard/global/exception/PermissionDeniedEditException.java deleted file mode 100644 index 6cc1ac0ed..000000000 --- a/src/main/java/com/example/jpaboard/global/exception/PermissionDeniedEditException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.jpaboard.global.exception; - -public class PermissionDeniedEditException extends RuntimeException { - - public PermissionDeniedEditException(String message) { - super(message); - } - -} diff --git a/src/main/java/com/example/jpaboard/global/exception/UnauthorizedEditException.java b/src/main/java/com/example/jpaboard/global/exception/UnauthorizedEditException.java new file mode 100644 index 000000000..70f1f8a4e --- /dev/null +++ b/src/main/java/com/example/jpaboard/global/exception/UnauthorizedEditException.java @@ -0,0 +1,9 @@ +package com.example.jpaboard.global.exception; + +public class UnauthorizedEditException extends RuntimeException { + + public UnauthorizedEditException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/example/jpaboard/member/domain/Age.java b/src/main/java/com/example/jpaboard/member/domain/Age.java index f115be416..21601d22f 100644 --- a/src/main/java/com/example/jpaboard/member/domain/Age.java +++ b/src/main/java/com/example/jpaboard/member/domain/Age.java @@ -6,6 +6,7 @@ public class Age { private static final int AGE_MIN = 0; + private static final int AGE_MAX = 150; private int age; @@ -18,7 +19,7 @@ public Age(int age) { } private void validateAge(int age) { - if (age < AGE_MIN) { + if (age < AGE_MIN || age >= AGE_MAX) { throw new IllegalArgumentException(); } } diff --git a/src/main/java/com/example/jpaboard/member/domain/Member.java b/src/main/java/com/example/jpaboard/member/domain/Member.java index 89a829f21..c0375d82f 100644 --- a/src/main/java/com/example/jpaboard/member/domain/Member.java +++ b/src/main/java/com/example/jpaboard/member/domain/Member.java @@ -8,7 +8,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; @Entity @Table(name = "members") @@ -18,23 +17,24 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @NotNull - private String name; + @Embedded + private Name name; @Embedded private Age age; private String hobby; - protected Member() { } + protected Member() { + } - public Member(String name, Age age, String hobby) { + public Member(Name name, Age age, String hobby) { this.name = name; this.age = age; this.hobby = hobby; } - public Member(Long id, String name, Age age, String hobby) { + public Member(Long id, Name name, Age age, String hobby) { this.id = id; this.name = name; this.age = age; @@ -45,7 +45,7 @@ public Long getId() { return id; } - public String getName() { + public Name getName() { return name; } diff --git a/src/main/java/com/example/jpaboard/member/domain/Name.java b/src/main/java/com/example/jpaboard/member/domain/Name.java new file mode 100644 index 000000000..d525a2313 --- /dev/null +++ b/src/main/java/com/example/jpaboard/member/domain/Name.java @@ -0,0 +1,33 @@ +package com.example.jpaboard.member.domain; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class Name { + + private String value; + + protected Name() { + } + + public Name(String value) { + validateName(value); + this.value = value; + } + + public void changeName(String value) { + validateName(value); + this.value = value; + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException(); + } + } + + public String getValue() { + return value; + } + +} diff --git a/src/main/java/com/example/jpaboard/member/service/MemberService.java b/src/main/java/com/example/jpaboard/member/service/MemberService.java index 0e706e5c7..f5ed54e05 100644 --- a/src/main/java/com/example/jpaboard/member/service/MemberService.java +++ b/src/main/java/com/example/jpaboard/member/service/MemberService.java @@ -2,7 +2,7 @@ import com.example.jpaboard.global.exception.EntityNotFoundException; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.dto.FindMemberResponse; +import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.member.service.mapper.MemberMapper; import org.springframework.stereotype.Service; @@ -18,10 +18,10 @@ public MemberService(MemberRepository memberRepository, MemberMapper memberMappe this.memberMapper = memberMapper; } - public FindMemberResponse findById(Long id) { + public MemberFindResponse findById(Long id) { Member member = memberRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("존재하지 않은 고객입니다.")); - return new FindMemberResponse(member); + return new MemberFindResponse(member); } } diff --git a/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java b/src/main/java/com/example/jpaboard/member/service/dto/MemberCreateRequest.java similarity index 67% rename from src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java rename to src/main/java/com/example/jpaboard/member/service/dto/MemberCreateRequest.java index 1ccd8791e..fb8212f3e 100644 --- a/src/main/java/com/example/jpaboard/member/service/dto/CreateMemberRequest.java +++ b/src/main/java/com/example/jpaboard/member/service/dto/MemberCreateRequest.java @@ -1,7 +1,8 @@ package com.example.jpaboard.member.service.dto; import com.example.jpaboard.member.domain.Age; +import com.example.jpaboard.member.domain.Name; -public record CreateMemberRequest(String name, +public record MemberCreateRequest(Name name, Age age, String hobby) { } diff --git a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java b/src/main/java/com/example/jpaboard/member/service/dto/MemberFindResponse.java similarity index 70% rename from src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java rename to src/main/java/com/example/jpaboard/member/service/dto/MemberFindResponse.java index 0d24e204e..fbf2043fa 100644 --- a/src/main/java/com/example/jpaboard/member/service/dto/FindMemberResponse.java +++ b/src/main/java/com/example/jpaboard/member/service/dto/MemberFindResponse.java @@ -2,22 +2,23 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.domain.Name; -public class FindMemberResponse { +public class MemberFindResponse { private final Long id; - private final String name; + private final Name name; private final Age age; private final String hobby; - public FindMemberResponse(Long id, String name, Age age, String hobby) { + public MemberFindResponse(Long id, Name name, Age age, String hobby) { this.id = id; this.name = name; this.age = age; this.hobby = hobby; } - public FindMemberResponse(Member member) { + public MemberFindResponse(Member member) { this(member.getId(), member.getName(), member.getAge(), member.getHobby()); } @@ -25,7 +26,7 @@ public Long getId() { return id; } - public String getName() { + public Name getName() { return name; } diff --git a/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java index f943186b5..d12a6de20 100644 --- a/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java +++ b/src/main/java/com/example/jpaboard/member/service/mapper/MemberMapper.java @@ -2,7 +2,8 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.dto.CreateMemberRequest; +import com.example.jpaboard.member.domain.Name; +import com.example.jpaboard.member.service.dto.MemberCreateRequest; import org.springframework.stereotype.Component; @@ -11,10 +12,10 @@ public class MemberMapper { private MemberMapper() { } - public Member to(CreateMemberRequest createMemberRequest) { - String name = createMemberRequest.name(); - Age age = createMemberRequest.age(); - String hobby = createMemberRequest.hobby(); + public Member to(MemberCreateRequest memberCreateRequest) { + Name name = memberCreateRequest.name(); + Age age = memberCreateRequest.age(); + String hobby = memberCreateRequest.hobby(); return new Member(name, age, hobby); } diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index 63bfe7d53..d04b237b9 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -3,9 +3,9 @@ import com.example.jpaboard.global.ApiResponse; import com.example.jpaboard.global.SliceResponse; import com.example.jpaboard.global.SuccessCode; -import com.example.jpaboard.post.controller.dto.FindAllApiRequest; -import com.example.jpaboard.post.controller.dto.SaveApiRequest; -import com.example.jpaboard.post.controller.dto.UpdateApiRequest; +import com.example.jpaboard.post.controller.dto.PostFindApiRequest; +import com.example.jpaboard.post.controller.dto.PostSaveApiRequest; +import com.example.jpaboard.post.controller.dto.PostUpdateApiRequest; import com.example.jpaboard.post.controller.mapper.PostApiMapper; import com.example.jpaboard.post.service.PostService; import com.example.jpaboard.post.service.dto.*; @@ -14,6 +14,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -33,29 +34,29 @@ public PostController(PostService postService, PostApiMapper postApiMapper) { } @GetMapping - SliceResponse findAllBy(@ModelAttribute FindAllApiRequest findAllApiRequest, Pageable pageable) { - FindAllRequest findAllRequest = postApiMapper.toFindAllRequest(findAllApiRequest); + public SliceResponse findAllBy(@ModelAttribute PostFindApiRequest postRetrieveApiRequest, Pageable pageable) { + PostFindRequest postFindRequest = postApiMapper.toFindAllRequest(postRetrieveApiRequest); - Slice postAllByFilter = postService.findAllByFilter(findAllRequest, pageable); + Slice postAllByFilter = postService.findAllByFilter(postFindRequest, pageable); SliceResponse postResponseSliceResponse = new SliceResponse<>(postAllByFilter, SuccessCode.SELECT_SUCCESS); return postResponseSliceResponse; } - @PatchMapping("/{id}") - ApiResponse updatePost(@PathVariable Long id, @RequestBody @Valid UpdateApiRequest updateApiRequest) { - UpdateRequest updateRequest = postApiMapper.toUpdateRequest(updateApiRequest); - return new ApiResponse<>(postService.updatePost(id, updateRequest), SuccessCode.UPDATE_SUCCESS); + @PatchMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + public ApiResponse updatePost(@PathVariable Long id, @RequestBody @Valid PostUpdateApiRequest postUpdateApiRequest) { + PostUpdateRequest postUpdateRequest = postApiMapper.toUpdateRequest(postUpdateApiRequest); + return new ApiResponse<>(postService.updatePost(id, postUpdateRequest), SuccessCode.UPDATE_SUCCESS); } @GetMapping("/{id}") - ApiResponse findById(@PathVariable Long id) { + public ApiResponse findById(@PathVariable Long id) { return new ApiResponse<>(postService.findById(id), SuccessCode.SELECT_SUCCESS); } - @PostMapping - ResponseEntity savePost(@RequestBody @Valid SaveApiRequest saveApiRequest) { - SaveRequest saveRequest = postApiMapper.toSaveRequest(saveApiRequest); - PostResponse saveResponse = postService.savePost(saveRequest); + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity savePost(@RequestBody @Valid PostSaveApiRequest postSaveApiRequest) { + PostSaveRequest postSaveRequest = postApiMapper.toSaveRequest(postSaveApiRequest); + PostResponse saveResponse = postService.savePost(postSaveRequest); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}") diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/PostFindApiRequest.java similarity index 72% rename from src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java rename to src/main/java/com/example/jpaboard/post/controller/dto/PostFindApiRequest.java index c4040d1de..3a1c38e0e 100644 --- a/src/main/java/com/example/jpaboard/post/controller/dto/FindAllApiRequest.java +++ b/src/main/java/com/example/jpaboard/post/controller/dto/PostFindApiRequest.java @@ -1,16 +1,16 @@ package com.example.jpaboard.post.controller.dto; -public class FindAllApiRequest { +public class PostFindApiRequest { private String title; private String content; - public FindAllApiRequest() { + public PostFindApiRequest() { title = ""; content = ""; } - public FindAllApiRequest(String title, String content) { + public PostFindApiRequest(String title, String content) { this.title = title; this.content = content; } diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/PostSaveApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/PostSaveApiRequest.java new file mode 100644 index 000000000..5c77b6ced --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/dto/PostSaveApiRequest.java @@ -0,0 +1,8 @@ +package com.example.jpaboard.post.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PostSaveApiRequest(@NotNull(message = "memberId 값이 입력되지 않았습니다.") Long memberId, + @NotBlank(message = "title 값이 입력되지 않았습니다.") String title, + @NotBlank(message = "content 값이 입력되지 않았습니다.") String content) { } diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/PostUpdateApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/PostUpdateApiRequest.java new file mode 100644 index 000000000..06e394efa --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/controller/dto/PostUpdateApiRequest.java @@ -0,0 +1,9 @@ +package com.example.jpaboard.post.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PostUpdateApiRequest(@NotBlank(message = "title 값이 입력되지 않았습니다.") String title, + @NotBlank(message = "content 값이 입력되지 않았습니다.") String content, + @NotNull(message = "memberId 값이 입력되지 않았습니다.") Long memberId) { } + diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java deleted file mode 100644 index 0cd09e564..000000000 --- a/src/main/java/com/example/jpaboard/post/controller/dto/SaveApiRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.jpaboard.post.controller.dto; - -import jakarta.validation.constraints.NotNull; - -public record SaveApiRequest(@NotNull(message = "memberId 값이 입력되지 않았습니다.") Long memberId, - @NotNull(message = "title 값이 입력되지 않았습니다.") String title, - @NotNull(message = "content 값이 입력되지 않았습니다.") String content) { } diff --git a/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java b/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java deleted file mode 100644 index 27a8e1038..000000000 --- a/src/main/java/com/example/jpaboard/post/controller/dto/UpdateApiRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.jpaboard.post.controller.dto; - -import jakarta.validation.constraints.NotNull; - -public record UpdateApiRequest(@NotNull(message = "title 값이 입력되지 않았습니다.") String title, - @NotNull(message = "content 값이 입력되지 않았습니다.") String content, - @NotNull(message = "memberId 값이 입력되지 않았습니다.") Long memberId) { } - diff --git a/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java index b21d5d3db..c1becfe8c 100644 --- a/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java +++ b/src/main/java/com/example/jpaboard/post/controller/mapper/PostApiMapper.java @@ -1,32 +1,33 @@ package com.example.jpaboard.post.controller.mapper; -import com.example.jpaboard.post.controller.dto.FindAllApiRequest; -import com.example.jpaboard.post.controller.dto.SaveApiRequest; -import com.example.jpaboard.post.controller.dto.UpdateApiRequest; -import com.example.jpaboard.post.service.dto.FindAllRequest; -import com.example.jpaboard.post.service.dto.SaveRequest; -import com.example.jpaboard.post.service.dto.UpdateRequest; +import com.example.jpaboard.post.controller.dto.PostFindApiRequest; +import com.example.jpaboard.post.controller.dto.PostSaveApiRequest; +import com.example.jpaboard.post.controller.dto.PostUpdateApiRequest; +import com.example.jpaboard.post.service.dto.PostFindRequest; +import com.example.jpaboard.post.service.dto.PostSaveRequest; +import com.example.jpaboard.post.service.dto.PostUpdateRequest; import org.springframework.stereotype.Component; @Component public class PostApiMapper { - public FindAllRequest toFindAllRequest(FindAllApiRequest findAllApiRequest) { + public PostFindRequest toFindAllRequest(PostFindApiRequest postRetrieveApiRequest) { - return new FindAllRequest(findAllApiRequest.title(), findAllApiRequest.content()); + return new PostFindRequest(postRetrieveApiRequest.title(), postRetrieveApiRequest.content()); } - public UpdateRequest toUpdateRequest(UpdateApiRequest updateApiRequest) { - return new UpdateRequest(updateApiRequest.title(), - updateApiRequest.content(), - updateApiRequest.memberId()); + public PostUpdateRequest toUpdateRequest(PostUpdateApiRequest postUpdateApiRequest) { + return new PostUpdateRequest(postUpdateApiRequest.title(), + postUpdateApiRequest.content(), + postUpdateApiRequest.memberId()); } - public SaveRequest toSaveRequest(SaveApiRequest saveApiRequest) { - return new SaveRequest(saveApiRequest.memberId(), - saveApiRequest.title(), - saveApiRequest.content()); + public PostSaveRequest toSaveRequest(PostSaveApiRequest postSaveApiRequest) { + return new PostSaveRequest(postSaveApiRequest.memberId(), + postSaveApiRequest.title(), + postSaveApiRequest.content()); + } } diff --git a/src/main/java/com/example/jpaboard/post/domain/Post.java b/src/main/java/com/example/jpaboard/post/domain/Post.java index fcae48eee..b62b6deb3 100644 --- a/src/main/java/com/example/jpaboard/post/domain/Post.java +++ b/src/main/java/com/example/jpaboard/post/domain/Post.java @@ -5,6 +5,9 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; +import org.springframework.util.Assert; + +import java.util.Objects; @Entity @Table(name = "posts") @@ -17,22 +20,35 @@ public class Post extends BaseEntity { private String title; @Lob - @NotNull + @NotNull // 3 + @Column(nullable = false) // 2 private String content; @NotNull - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) // LAZY 반영 + @JoinColumn(name = "member_id") // 흑구멘토님 의견 반영 관계 조건 확실하게 private Member member; - protected Post() { - } + protected Post() { } - public Post(String title, String content, Member member) { + Post(String title, String content, Member member) { // this.title = title; this.content = content; this.member = member; } + public static Post create(String title, String content, Member member) { + Assert.notNull(title, "not null"); + Assert.notNull(content, "not null"); + Assert.notNull(member, "not null"); + + return new Post(title, content, member); + } + + public boolean isNotOwner(Long memberId) { + return !Objects.equals(member.getId(), memberId); + } + public Long getId() { return id; } diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index 4eb6da63a..32e8ac714 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -1,24 +1,23 @@ package com.example.jpaboard.post.service; import com.example.jpaboard.member.service.MemberService; -import com.example.jpaboard.member.service.dto.FindMemberResponse; +import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; -import com.example.jpaboard.post.service.dto.FindAllRequest; +import com.example.jpaboard.post.service.dto.PostFindRequest; import com.example.jpaboard.post.service.dto.PostResponse; -import com.example.jpaboard.post.service.dto.SaveRequest; -import com.example.jpaboard.post.service.dto.UpdateRequest; +import com.example.jpaboard.post.service.dto.PostSaveRequest; +import com.example.jpaboard.post.service.dto.PostUpdateRequest; import com.example.jpaboard.post.service.mapper.PostMapper; import com.example.jpaboard.global.exception.EntityNotFoundException; -import com.example.jpaboard.global.exception.PermissionDeniedEditException; +import com.example.jpaboard.global.exception.UnauthorizedEditException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Objects; - +@Transactional(readOnly = true) @Service public class PostService { @@ -32,8 +31,8 @@ public PostService(PostRepository postRepository, MemberService memberService, P this.mapper = mapper; } - public Slice findAllByFilter(FindAllRequest findAllRequest, Pageable pageable) { - Slice results = postRepository.findPostAllByFilter(findAllRequest.title(), findAllRequest.content(), pageable); + public Slice findAllByFilter(PostFindRequest postFindRequest, Pageable pageable) { + Slice results = postRepository.findPostAllByFilter(postFindRequest.title(), postFindRequest.content(), pageable); return results.map(PostResponse::new); } @@ -44,21 +43,20 @@ public PostResponse findById(Long postId) { } @Transactional - public PostResponse savePost(SaveRequest request) { - FindMemberResponse findMember = memberService.findById(request.memberId()); + public PostResponse savePost(PostSaveRequest request) { + MemberFindResponse findMember = memberService.findById(request.memberId()); Post post = mapper.to(request, findMember); return new PostResponse(postRepository.save(post)); } @Transactional - public PostResponse updatePost(Long id, UpdateRequest request) { + public PostResponse updatePost(Long id, PostUpdateRequest request) { Post findPost = postRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("해당 post가 존재하지 않습니다.")); - Long postOwnerId = findPost.getMember().getId(); - if (isNotOwner(request.memberId(), postOwnerId)){ - throw new PermissionDeniedEditException("해당 게시글을 수정할 권한이 없습니다."); + if (findPost.isNotOwner(request.memberId())){ + throw new UnauthorizedEditException("해당 게시글을 수정할 권한이 없습니다."); } findPost.changTitle(request.title()); @@ -67,8 +65,4 @@ public PostResponse updatePost(Long id, UpdateRequest request) { return new PostResponse(findPost); } - private static boolean isNotOwner(Long memberId, Long postOwnerId) { - return !Objects.equals(postOwnerId, memberId); - } - } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java deleted file mode 100644 index bdd5a455f..000000000 --- a/src/main/java/com/example/jpaboard/post/service/dto/FindAllRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.jpaboard.post.service.dto; - -public record FindAllRequest(String title , - String content) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostFindRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/PostFindRequest.java new file mode 100644 index 000000000..322da2536 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostFindRequest.java @@ -0,0 +1,4 @@ +package com.example.jpaboard.post.service.dto; + +public record PostFindRequest(String title , + String content) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java index eddb32048..293682ed8 100644 --- a/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostResponse.java @@ -1,11 +1,12 @@ package com.example.jpaboard.post.service.dto; +import com.example.jpaboard.member.domain.Name; import com.example.jpaboard.post.domain.Post; public record PostResponse(Long postId, String title, String content, - String memberName) { + Name memberName) { public PostResponse(Post post) { this(post.getId(), post.getTitle(), post.getContent(), post.getMember().getName()); diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostSaveRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/PostSaveRequest.java new file mode 100644 index 000000000..fbd082306 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostSaveRequest.java @@ -0,0 +1,5 @@ +package com.example.jpaboard.post.service.dto; + +public record PostSaveRequest(Long memberId, + String title, + String content) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/PostUpdateRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/PostUpdateRequest.java new file mode 100644 index 000000000..b1263a765 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/dto/PostUpdateRequest.java @@ -0,0 +1,5 @@ +package com.example.jpaboard.post.service.dto; + +public record PostUpdateRequest(String title, + String content, + Long memberId) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java deleted file mode 100644 index 3c681f795..000000000 --- a/src/main/java/com/example/jpaboard/post/service/dto/SaveRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.jpaboard.post.service.dto; - -public record SaveRequest(Long memberId, - String title, - String content) { } diff --git a/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java b/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java deleted file mode 100644 index 6c094ed95..000000000 --- a/src/main/java/com/example/jpaboard/post/service/dto/UpdateRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.jpaboard.post.service.dto; - -public record UpdateRequest(String title, - String content, - Long memberId) { } diff --git a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java index 2aec58a55..b621a845d 100644 --- a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java +++ b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java @@ -1,22 +1,22 @@ package com.example.jpaboard.post.service.mapper; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.dto.FindMemberResponse; +import com.example.jpaboard.member.domain.Name; +import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.post.domain.Post; -import com.example.jpaboard.post.service.dto.SaveRequest; +import com.example.jpaboard.post.service.dto.PostSaveRequest; import org.springframework.stereotype.Component; @Component public class PostMapper { - private PostMapper() { - } + private PostMapper() { } - public Post to(SaveRequest request, FindMemberResponse memberResponse) { + public Post to(PostSaveRequest request, MemberFindResponse memberResponse) { Member member = new Member(memberResponse.getId(), memberResponse.getName(), memberResponse.getAge(), memberResponse.getHobby()); - return new Post(request.title(), + return Post.create(request.title(), request.content(), member); } diff --git a/src/main/java/com/example/jpaboard/rpc/RpcInput.java b/src/main/java/com/example/jpaboard/rpc/RpcInput.java new file mode 100644 index 000000000..d6b63361a --- /dev/null +++ b/src/main/java/com/example/jpaboard/rpc/RpcInput.java @@ -0,0 +1,23 @@ +package com.example.jpaboard.rpc; + +import com.example.jpaboard.post.domain.Post; +import com.example.jpaboard.post.service.PostService; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@RequiredArgsConstructor +public class RpcInput { + + private final PostService postService; + + public void create(Map request) { + + var title = request.get("title"); + + var content = request.get("content"); + + Post.create(title, content, null); + } + +} diff --git a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java index 7d47f8bdd..e82a7f9e8 100644 --- a/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java +++ b/src/test/java/com/example/jpaboard/member/service/MemberServiceTest.java @@ -2,7 +2,8 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.service.dto.FindMemberResponse; +import com.example.jpaboard.member.domain.Name; +import com.example.jpaboard.member.service.dto.MemberFindResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +29,7 @@ class MemberServiceTest { @BeforeEach void setUp() { - Member member = new Member("김별", new Age(26), "산책"); + Member member = new Member(new Name("김별"), new Age(26), "산책"); memberRepository.save(member); id = member.getId(); } @@ -36,10 +37,10 @@ void setUp() { @Test void findById_Member_Equals() { //when - FindMemberResponse response = memberService.findById(id); + MemberFindResponse response = memberService.findById(id); //then - assertThat(response.getName()).isEqualTo("김별"); + assertThat(response.getName().getValue()).isEqualTo("김별"); assertThat(response.getHobby()).isEqualTo("산책"); assertThat(response.getAge()).usingRecursiveComparison().isEqualTo(new Age(26)); } diff --git a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java index a330edf77..f3e598e97 100644 --- a/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java +++ b/src/test/java/com/example/jpaboard/post/controller/PostControllerTest.java @@ -1,7 +1,8 @@ package com.example.jpaboard.post.controller; -import com.example.jpaboard.post.controller.dto.SaveApiRequest; +import com.example.jpaboard.post.controller.dto.PostSaveApiRequest; +import com.example.jpaboard.post.controller.dto.PostUpdateApiRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -84,9 +85,12 @@ public void updatePost() throws Exception { fieldWithPath("resultMsg").description("The result message of the response") }; + + String requestBody = objectMapper.writeValueAsString(new PostUpdateApiRequest("흑구팀 화이팅팅", "언제나 응원해 흑구팀팀", 1L)); + this.mockMvc.perform( - patch("/posts/{id}", 1L) // 실제 ID 값으로 대체해주세요 - .content("{\"title\": \"흑구팀 화이팅팅\", \"content\": \"언제나 응원해 흑구팀팀\", \"memberId\": 1}") // 업데이트할 내용 전달 + patch("/posts/{id}", 1L) + .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ) .andExpect(status().isOk()) @@ -108,7 +112,7 @@ void findById() throws Exception { }; this.mockMvc.perform( - get("/posts/{id}", 1L) // 실제 ID 값으로 대체해주세요 + get("/posts/{id}", 1L) ) .andExpect(status().isOk()) .andDo(document("post-findById", @@ -118,7 +122,7 @@ void findById() throws Exception { @Test void savePost() throws Exception { - SaveApiRequest saveApiRequest = new SaveApiRequest(1L,"흑구영수팀 화이팅","흑구영수팀팀팀"); + PostSaveApiRequest postSaveApiRequest = new PostSaveApiRequest(1L,"흑구영수팀 화이팅","흑구영수팀팀팀"); FieldDescriptor[] requestFields = new FieldDescriptor[] { fieldWithPath("title").description("Saved title of the post"), @@ -135,7 +139,7 @@ void savePost() throws Exception { this.mockMvc.perform( post("/posts") - .content(objectMapper.writeValueAsString(saveApiRequest)) + .content(objectMapper.writeValueAsString(postSaveApiRequest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andDo(document("post-save", diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java index 233eaa160..c6a1c42d0 100644 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -2,15 +2,16 @@ import com.example.jpaboard.member.domain.Age; import com.example.jpaboard.member.domain.Member; +import com.example.jpaboard.member.domain.Name; import com.example.jpaboard.member.service.MemberRepository; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; import com.example.jpaboard.global.exception.EntityNotFoundException; -import com.example.jpaboard.global.exception.PermissionDeniedEditException; -import com.example.jpaboard.post.service.dto.FindAllRequest; +import com.example.jpaboard.global.exception.UnauthorizedEditException; +import com.example.jpaboard.post.service.dto.PostFindRequest; import com.example.jpaboard.post.service.dto.PostResponse; -import com.example.jpaboard.post.service.dto.SaveRequest; -import com.example.jpaboard.post.service.dto.UpdateRequest; +import com.example.jpaboard.post.service.dto.PostSaveRequest; +import com.example.jpaboard.post.service.dto.PostUpdateRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -30,7 +31,7 @@ @ActiveProfiles("test") -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional class PostServiceTest { @@ -58,10 +59,10 @@ void setup() { @DisplayName("setup에서 저장한 post를 필터 없이 10개 조회하여 개수를 확인한다.") void findAllBy_pageable_PostResponses() { //given - FindAllRequest findAllRequest = new FindAllRequest("", ""); + PostFindRequest postFindRequest = new PostFindRequest("", ""); //when - Slice findPosts = postService.findAllByFilter(findAllRequest, PageRequest.of(0, 10)); + Slice findPosts = postService.findAllByFilter(postFindRequest, PageRequest.of(0, 10)); //then assertThat(findPosts.getContent().size()).isEqualTo(2); @@ -75,9 +76,9 @@ void findById_correctPostId_PostResponse() { //then String findTitle = response.title(); - String findMemberName = response.memberName(); + Name findMemberName = response.memberName(); assertThat(findTitle).isEqualTo("별의 포스트 제목"); - assertThat(findMemberName).isEqualTo("김별"); + assertThat(findMemberName.getValue()).isEqualTo("김별"); } @Test @@ -94,26 +95,26 @@ void findById_inCorrectPostId_EntityNotFoundException() { @DisplayName("post 저장 후 title과 memberName을 확인한다.") void savePost_correctSaveRequest_postResponse() { //given - SaveRequest saveRequest = new SaveRequest(setupMemberId3, "산책의 정석", "산책의 정석 내용"); + PostSaveRequest postSaveRequest = new PostSaveRequest(setupMemberId3, "산책의 정석", "산책의 정석 내용"); //when - PostResponse postResponse = postService.savePost(saveRequest); + PostResponse postResponse = postService.savePost(postSaveRequest); //then String savedTitle = postResponse.title(); - String savedMemberName = postResponse.memberName(); + Name savedMemberName = postResponse.memberName(); assertThat(savedTitle).isEqualTo("산책의 정석"); - assertThat(savedMemberName).isEqualTo("박세영"); + assertThat(savedMemberName.getValue()).isEqualTo("박세영"); } @Test @DisplayName("setUp에서 저장한 post를 update후 title과 content를 확인한다.") void updatePost_IdAndUpdateRequest_PostResponse() { //given - UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); //when - PostResponse postResponse = postService.updatePost(setupPostId2, updateRequest); + PostResponse postResponse = postService.updatePost(setupPostId2, postUpdateRequest); //then String updatedTitle = postResponse.title(); @@ -126,40 +127,40 @@ void updatePost_IdAndUpdateRequest_PostResponse() { @DisplayName("setUp에서 저장한 post를 올바르지 않은 memberId로 수정할 때 PermissionDeniedEditException이 발생하는지 확인한다.") void updatePost_IdAndIncorrectUpdateRequest_PermissionDeniedEditException() { //given - UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId3); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId3); //when - Exception exception = catchException(() -> postService.updatePost(setupPostId2, updateRequest)); + Exception exception = catchException(() -> postService.updatePost(setupPostId2, postUpdateRequest)); //then - assertThat(exception).isInstanceOf(PermissionDeniedEditException.class); + assertThat(exception).isInstanceOf(UnauthorizedEditException.class); } @Test @DisplayName("존재하지 않는 postId로 post를 수정할 때 EntityNotFoundException이 발생하는지 확인한다.") void updatePost_incorrectIdAndUpdateRequest_EntityNotFoundException() { //given - UpdateRequest updateRequest = new UpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("영운의 변경된 postTitle", "영운의 변경된 content", setupMemberId2); Random random = new Random(); //when - Exception exception = catchException(() -> postService.updatePost(random.nextLong(), updateRequest)); + Exception exception = catchException(() -> postService.updatePost(random.nextLong(), postUpdateRequest)); //then assertThat(exception).isInstanceOf(EntityNotFoundException.class); } private void setupData() { - Member member1 = new Member("김별", new Age(27), "락 부르기"); - Member member2 = new Member("윤영운", new Age(27), "저글링 돌리기"); - Member member3 = new Member("박세영", new Age(27), "산책"); + Member member1 = new Member(new Name("김별"), new Age(27), "락 부르기"); + Member member2 = new Member(new Name("윤영운"), new Age(27), "저글링 돌리기"); + Member member3 = new Member(new Name("박세영"), new Age(27), "산책"); memberRepository.save(member1); memberRepository.save(member2); memberRepository.save(member3); - Post post1 = new Post("별의 포스트 제목", "별의 포스트 내용", member1); - Post post2 = new Post("영운의 포스트 제목", "영운의 포스트 내용", member2); + Post post1 = Post.create("별의 포스트 제목", "별의 포스트 내용", member1); + Post post2 = Post.create("영운의 포스트 제목", "영운의 포스트 내용", member2); postRepository.save(post1); postRepository.save(post2); From 5f939889d7575cc1cad62c2e89eb93aa8562556c Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sun, 13 Aug 2023 01:44:48 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat=20:=20=ED=8D=BC=EC=82=AC=EB=93=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostController.java | 16 ++++---- .../jpaboard/post/service/PostFacade.java | 41 +++++++++++++++++++ .../jpaboard/post/service/PostService.java | 10 ++--- .../post/service/mapper/PostMapper.java | 1 - .../post/service/PostServiceTest.java | 4 +- 5 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/example/jpaboard/post/service/PostFacade.java diff --git a/src/main/java/com/example/jpaboard/post/controller/PostController.java b/src/main/java/com/example/jpaboard/post/controller/PostController.java index d04b237b9..15b070760 100644 --- a/src/main/java/com/example/jpaboard/post/controller/PostController.java +++ b/src/main/java/com/example/jpaboard/post/controller/PostController.java @@ -7,7 +7,7 @@ import com.example.jpaboard.post.controller.dto.PostSaveApiRequest; import com.example.jpaboard.post.controller.dto.PostUpdateApiRequest; import com.example.jpaboard.post.controller.mapper.PostApiMapper; -import com.example.jpaboard.post.service.PostService; +import com.example.jpaboard.post.service.PostFacade; import com.example.jpaboard.post.service.dto.*; import jakarta.validation.Valid; @@ -25,19 +25,19 @@ @RequestMapping("/posts") public class PostController { - private final PostService postService; private final PostApiMapper postApiMapper; + private final PostFacade postFacade; - public PostController(PostService postService, PostApiMapper postApiMapper) { - this.postService = postService; + public PostController( PostApiMapper postApiMapper, PostFacade postFacade) { this.postApiMapper = postApiMapper; + this.postFacade = postFacade; } @GetMapping public SliceResponse findAllBy(@ModelAttribute PostFindApiRequest postRetrieveApiRequest, Pageable pageable) { PostFindRequest postFindRequest = postApiMapper.toFindAllRequest(postRetrieveApiRequest); - Slice postAllByFilter = postService.findAllByFilter(postFindRequest, pageable); + Slice postAllByFilter = postFacade.findAllByFilter(postFindRequest, pageable); SliceResponse postResponseSliceResponse = new SliceResponse<>(postAllByFilter, SuccessCode.SELECT_SUCCESS); return postResponseSliceResponse; } @@ -45,18 +45,18 @@ public SliceResponse findAllBy(@ModelAttribute PostFindApiRequest @PatchMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) public ApiResponse updatePost(@PathVariable Long id, @RequestBody @Valid PostUpdateApiRequest postUpdateApiRequest) { PostUpdateRequest postUpdateRequest = postApiMapper.toUpdateRequest(postUpdateApiRequest); - return new ApiResponse<>(postService.updatePost(id, postUpdateRequest), SuccessCode.UPDATE_SUCCESS); + return new ApiResponse<>(postFacade.updatePost(id, postUpdateRequest), SuccessCode.UPDATE_SUCCESS); } @GetMapping("/{id}") public ApiResponse findById(@PathVariable Long id) { - return new ApiResponse<>(postService.findById(id), SuccessCode.SELECT_SUCCESS); + return new ApiResponse<>(postFacade.findById(id), SuccessCode.SELECT_SUCCESS); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity savePost(@RequestBody @Valid PostSaveApiRequest postSaveApiRequest) { PostSaveRequest postSaveRequest = postApiMapper.toSaveRequest(postSaveApiRequest); - PostResponse saveResponse = postService.savePost(postSaveRequest); + PostResponse saveResponse = postFacade.createPost(postSaveRequest); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}") diff --git a/src/main/java/com/example/jpaboard/post/service/PostFacade.java b/src/main/java/com/example/jpaboard/post/service/PostFacade.java new file mode 100644 index 000000000..061130685 --- /dev/null +++ b/src/main/java/com/example/jpaboard/post/service/PostFacade.java @@ -0,0 +1,41 @@ +package com.example.jpaboard.post.service; + +import com.example.jpaboard.member.service.MemberService; +import com.example.jpaboard.member.service.dto.MemberFindResponse; +import com.example.jpaboard.post.service.dto.PostFindRequest; +import com.example.jpaboard.post.service.dto.PostResponse; +import com.example.jpaboard.post.service.dto.PostSaveRequest; +import com.example.jpaboard.post.service.dto.PostUpdateRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; + +@Service +public class PostFacade { + private final MemberService memberService; + private final PostService postService; + + public PostFacade(MemberService memberService, PostService postService){ + this.memberService = memberService; + this.postService = postService; + } + + public PostResponse createPost(PostSaveRequest postSaveRequest) { + MemberFindResponse member = memberService.findById(postSaveRequest.memberId()); + + return postService.savePost(member,postSaveRequest); + } + + public PostResponse findById(Long postId) { + return postService.findById(postId); + } + + public Slice findAllByFilter(PostFindRequest postFindRequest, Pageable pageable) { + return postService.findAllByFilter(postFindRequest, pageable); + } + + public PostResponse updatePost(Long id, PostUpdateRequest request) { + return postService.updatePost(id, request); + } + +} diff --git a/src/main/java/com/example/jpaboard/post/service/PostService.java b/src/main/java/com/example/jpaboard/post/service/PostService.java index 32e8ac714..382687519 100644 --- a/src/main/java/com/example/jpaboard/post/service/PostService.java +++ b/src/main/java/com/example/jpaboard/post/service/PostService.java @@ -1,6 +1,5 @@ package com.example.jpaboard.post.service; -import com.example.jpaboard.member.service.MemberService; import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; @@ -22,12 +21,10 @@ public class PostService { private final PostRepository postRepository; - private final MemberService memberService; private final PostMapper mapper; - public PostService(PostRepository postRepository, MemberService memberService, PostMapper mapper) { + public PostService(PostRepository postRepository, PostMapper mapper) { this.postRepository = postRepository; - this.memberService = memberService; this.mapper = mapper; } @@ -43,9 +40,8 @@ public PostResponse findById(Long postId) { } @Transactional - public PostResponse savePost(PostSaveRequest request) { - MemberFindResponse findMember = memberService.findById(request.memberId()); - Post post = mapper.to(request, findMember); + public PostResponse savePost(MemberFindResponse member ,PostSaveRequest request) { + Post post = mapper.to(request, member); return new PostResponse(postRepository.save(post)); } diff --git a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java index b621a845d..34d2714c2 100644 --- a/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java +++ b/src/main/java/com/example/jpaboard/post/service/mapper/PostMapper.java @@ -1,7 +1,6 @@ package com.example.jpaboard.post.service.mapper; import com.example.jpaboard.member.domain.Member; -import com.example.jpaboard.member.domain.Name; import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.service.dto.PostSaveRequest; diff --git a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java index c6a1c42d0..efa018c46 100644 --- a/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java +++ b/src/test/java/com/example/jpaboard/post/service/PostServiceTest.java @@ -4,6 +4,7 @@ import com.example.jpaboard.member.domain.Member; import com.example.jpaboard.member.domain.Name; import com.example.jpaboard.member.service.MemberRepository; +import com.example.jpaboard.member.service.dto.MemberFindResponse; import com.example.jpaboard.post.domain.Post; import com.example.jpaboard.post.domain.PostRepository; import com.example.jpaboard.global.exception.EntityNotFoundException; @@ -96,9 +97,10 @@ void findById_inCorrectPostId_EntityNotFoundException() { void savePost_correctSaveRequest_postResponse() { //given PostSaveRequest postSaveRequest = new PostSaveRequest(setupMemberId3, "산책의 정석", "산책의 정석 내용"); + MemberFindResponse member = new MemberFindResponse(setupMemberId3,new Name("박세영"), new Age(27), "산책"); //when - PostResponse postResponse = postService.savePost(postSaveRequest); + PostResponse postResponse = postService.savePost(member, postSaveRequest); //then String savedTitle = postResponse.title();